diff --git a/DESIGN.md b/DESIGN.md
index 5546b28f0..5c486ed70 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -619,6 +619,16 @@ ### Terminals
shell, theme picker, compact header controls, and outline `browser-terminal`
Reicon. Hide a container switcher when only one container exists.
+The themed console shell belongs to an open session. Before a target is
+selected, the global Terminal page stays a normal top-level destination: a
+full-width layer card titled `Start a terminal session`, its filter input in
+the card header actions, and grouped `Servers` / `Containers` rows reusing the
+command-palette row classes. Do not render an empty full-height console canvas
+just to host the target picker, and do not offer the console theme selector
+before a session owns that canvas. Rows show the target name, a muted server
+column that only appears when the team has more than one server, and the shared
+chevron. Group headers stick to the top of the scrolling list and carry a count.
+
### Logs
Runtime and deployment logs should feel like a clean terminal surface:
diff --git a/resources/css/app.css b/resources/css/app.css
index 67562aa08..ed125c79e 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -727,6 +727,86 @@ .terminal-target-picker {
backdrop-filter: blur(16px);
}
+/* Pre-session target list: a normal page card, not the themed console canvas. */
+.terminal-target-card .application-settings-section-body {
+ overflow: hidden;
+}
+
+/* The header inset is tuned for 32px buttons. A full-width filter needs the
+ same gutter on both sides once the actions row wraps below the title.
+ Matches the base header selector's specificity so the shorthand cannot win. */
+@media (max-width: 640px) {
+ .application-settings-section.terminal-target-card > :is(header, .application-settings-section-header) {
+ padding-right: 1rem;
+ }
+}
+
+.terminal-target-card-list {
+ max-height: min(70vh, 34rem);
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: var(--coollabs-fill) transparent;
+}
+
+.terminal-target-card-list::-webkit-scrollbar {
+ width: 8px;
+}
+
+.terminal-target-card-list::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.terminal-target-card-list::-webkit-scrollbar-thumb {
+ border: 2px solid transparent;
+ border-radius: 9999px;
+ background-color: var(--coollabs-fill);
+ background-clip: padding-box;
+}
+
+/* Group headers stay readable while scrolling long container lists. */
+.terminal-target-group-label {
+ display: flex;
+ position: sticky;
+ z-index: 1;
+ top: 0;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0.5rem 0.5rem 0.375rem;
+ background: var(--coollabs-base);
+ font-size: 0.6875rem;
+ font-weight: 500;
+ letter-spacing: 0.01em;
+ color: var(--coollabs-subtle);
+}
+
+.terminal-target-group-count {
+ border-radius: 9999px;
+ background: var(--coollabs-fill);
+ padding: 0 0.375rem;
+ font-size: 0.625rem;
+ line-height: 1rem;
+ font-variant-numeric: tabular-nums;
+}
+
+/* The server column only earns its space once the row is wide enough. */
+.terminal-target-item-server {
+ display: none;
+ max-width: 14rem;
+ flex-shrink: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 0.6875rem;
+ line-height: 1rem;
+ color: var(--coollabs-subtle);
+}
+
+@media (min-width: 640px) {
+ .terminal-target-item-server {
+ display: block;
+ }
+}
+
.terminal-session-panel {
border: 0;
border-radius: 0.75rem;
diff --git a/resources/views/livewire/terminal/index.blade.php b/resources/views/livewire/terminal/index.blade.php
index 8694aa0bf..a4a42bdfc 100644
--- a/resources/views/livewire/terminal/index.blade.php
+++ b/resources/views/livewire/terminal/index.blade.php
@@ -22,6 +22,8 @@
$terminalOptions[] = [
'value' => $server->uuid,
'label' => $server->name.' · Server',
+ 'name' => $server->name,
+ 'server' => $server->name,
'type' => 'server',
];
@@ -30,6 +32,8 @@
$terminalOptions[] = [
'value' => $container['uuid'],
'label' => $server->name.' · '.$container['name'],
+ 'name' => $container['name'],
+ 'server' => $server->name,
'type' => 'container',
];
}
@@ -41,7 +45,8 @@
?? 'Select a server or container';
@endphp
-
+
Terminal | Coolify
@@ -57,11 +62,12 @@
-
@if ($selected_uuid === 'default')
-
-
-
-
-
-
-
Start a terminal session
-
- {{ $isLoadingContainers ? 'Finding available servers and containers…' : 'Choose a server or container. The terminal will open after you select a target.' }}
-
- @if (! $isLoadingContainers && $servers->isNotEmpty())
-
+
-
- @if ($isLoadingContainers)
-
-
-
- Loading servers and containers…
-
-
- @elseif ($servers->isEmpty())
-
-
No terminal targets available
-
Connect a reachable server and enable terminal access.
-
- @else
+
+ @endif
+
+ @if ($isLoadingContainers)
+
+
+ Finding available servers and containers…
+
+ @elseif ($servers->isEmpty())
+
+ @else
+
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+ class="px-3 py-8 text-center text-[13px] text-neutral-500 dark:text-fg-dim">
No matching targets
- @endif
-
-
-
+
+ @endif
+
+
@else
-
@@ -301,22 +300,22 @@ class="pointer-events-none absolute top-1/2 left-2 size-3 -translate-y-1/2 text-
class="h-8! w-full rounded-md! border-white/[0.08]! bg-white/[0.05]! py-0! pr-2! pl-7! text-[11px]! text-white! shadow-none! placeholder:text-white/30 focus:border-accent! focus:ring-0!">
-
+
diff --git a/tests/Feature/RealtimeTerminalPackagingTest.php b/tests/Feature/RealtimeTerminalPackagingTest.php
index 91c5ae453..289c281e7 100644
--- a/tests/Feature/RealtimeTerminalPackagingTest.php
+++ b/tests/Feature/RealtimeTerminalPackagingTest.php
@@ -98,7 +98,7 @@
$styles = file_get_contents(resource_path('css/app.css'));
expect(substr_count($resourceView.$terminalView.$globalView, 'terminal-loading-label'))
- ->toBeGreaterThanOrEqual(3)
+ ->toBeGreaterThanOrEqual(2)
->and($styles)
->toContain('.terminal-loading-label')
->toContain('font-size: 0.875rem;')
diff --git a/tests/Feature/TerminalPageHeaderTest.php b/tests/Feature/TerminalPageHeaderTest.php
index 6e1308608..39ce185ab 100644
--- a/tests/Feature/TerminalPageHeaderTest.php
+++ b/tests/Feature/TerminalPageHeaderTest.php
@@ -11,25 +11,60 @@
->toContain('this.targetChosen = true;');
});
-it('shows a centered themed target canvas before loading xterm', function () {
+it('shows the pre-session target list as a page card instead of a full-height console canvas', function () {
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
+ $pickerBranch = str($view)
+ ->after("@if (\$selected_uuid === 'default')")
+ ->before("\n @else")
+ ->toString();
+
expect($view)
->toContain("@if (\$selected_uuid === 'default')")
->toContain('data-terminal-target-picker="page"')
->toContain('data-terminal-target-canvas')
- ->toContain('items-center justify-center')
- ->toContain(':data-console-theme="consoleTheme"')
- ->toContain("@else\n toContain("@else\n
and($pickerBranch)
+ ->toContain('toContain('title="Start a terminal session"')
+ // The themed console canvas belongs to an open session, not to target selection.
+ ->not->toContain('application-console-shell')
+ ->not->toContain(':data-console-theme="consoleTheme"')
+ ->not->toContain('toContain('data-terminal-target-picker="page"')
- ->toContain('flex max-h-full w-full max-w-lg flex-col')
- ->toContain('class="terminal-target-list min-h-0 flex-1 overflow-y-auto p-2"');
+ ->toContain('class="application-settings-workspace flex w-full min-w-0 flex-col"')
+ ->toContain('class="terminal-target-card-list"')
+ ->and($styles)
+ ->toContain('.terminal-target-card .application-settings-section-body')
+ ->toMatch('/\.terminal-target-card-list\s*\{[^}]*max-height:\s*min\(70vh, 34rem\);/s')
+ ->toMatch('/\.terminal-target-group-label\s*\{[^}]*position:\s*sticky;/s')
+ // The wrapped filter needs the same gutter on both sides of the header, and the
+ // override must match the base header selector's specificity to win.
+ ->toMatch('/\.application-settings-section\.terminal-target-card > :is\(header, \.application-settings-section-header\)\s*\{\s*padding-right:\s*1rem;/s');
+});
+
+it('splits target rows into a name and a server column that only shows with several servers', function () {
+ $view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
+ $styles = file_get_contents(resource_path('css/app.css'));
+
+ expect($view)
+ ->toContain("'name' => \$container['name'],")
+ ->toContain("'server' => \$server->name,")
+ ->toContain('multipleServers: @js($servers->count() > 1)')
+ ->toContain('x-text="target.name"')
+ ->toContain('class="terminal-target-item-server" x-text="target.server"')
+ ->toContain('x-show="multipleServers"')
+ // The label keeps both parts so filtering still matches the server name.
+ ->toContain("'label' => \$server->name.' · '.\$container['name'],")
+ ->and($styles)
+ ->toContain('.terminal-target-item-server');
});
it('loads targets inside the themed session picker with an accent scrollbar', function () {
@@ -100,12 +135,12 @@
->not->toContain('application-console-header flex h-[30px]');
});
-it('keeps the theme selector identical before and during a terminal session', function () {
+it('offers the console theme selector only while a session owns the canvas', function () {
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
$selector = file_get_contents(resource_path('views/components/terminal/theme-selector.blade.php'));
expect(substr_count($view, 'toBe(2)
+ ->toBe(1)
->and($selector)
->toContain('terminal-theme-trigger flex h-8 items-center gap-2 rounded-md px-2.5 text-xs font-medium')
->and($view)
@@ -130,19 +165,20 @@
$selector = file_get_contents(resource_path('views/components/terminal/theme-selector.blade.php'));
expect(substr_count($globalView.$resourceView, 'toBe(3)
+ ->toBe(2)
->and($selector)
->toContain('terminal-theme-trigger')
->toContain('viewBox="0 0 12 12"')
->toContain('m3.5 4.75 2.5 2.5 2.5-2.5');
});
-it('keeps the pre-session theme selector out of the centered picker layout', function () {
+it('keeps the theme selector in the floating session toolbar', function () {
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
expect($view)
- ->toContain('class="absolute top-3 right-3 z-20"')
- ->toContain('toContain('terminal-session-toolbar absolute top-3 right-3 left-3')
+ ->toContain('not->toContain('class="absolute top-3 right-3 z-20"');
});
it('updates the owning terminal theme directly so its label and canvas stay synchronized', function () {
@@ -174,8 +210,9 @@
$appCss = file_get_contents(resource_path('css/app.css'));
expect($view)
- ->toContain('class="terminal-page application-settings-form"')
- ->toContain('class="terminal-page-console min-h-0 w-full flex-1 overflow-hidden"')
+ // The viewport lock only applies once a session owns the canvas.
+ ->toContain("class=\"{{ \$selected_uuid === 'default' ? '' : 'terminal-page' }} application-settings-form\"")
+ ->toContain("'terminal-page-console min-h-0 w-full flex-1 overflow-hidden'")
->not->toContain('h-[calc(100dvh-11rem)]')
->not->toContain('min-h-[32rem]')
->and($appCss)