diff --git a/plugins/code-server/app/App.vue b/plugins/code-server/app/App.vue
index bb6f0b71..01b87ad3 100644
--- a/plugins/code-server/app/App.vue
+++ b/plugins/code-server/app/App.vue
@@ -6,7 +6,7 @@ import LauncherView from './components/LauncherView.vue'
import { useCodeServer } from './composables/code-server'
import { connection, connect as connectRpc } from './composables/rpc'
-const { detection, server, connect, busy, phase, launch, stop, recheck, bootstrap } = useCodeServer()
+const { detection, server, connect, busy, phase, canViewInTerminal, launch, stop, recheck, viewInTerminal, bootstrap } = useCodeServer()
// Keep the editor iframe mounted for as long as we hold a connect descriptor,
// so a transient restart (running → starting → running) hides it via `v-show`
@@ -41,8 +41,10 @@ defineExpose({ stop })
:detection="detection"
:server="server"
:busy="busy"
+ :can-view-in-terminal="canViewInTerminal"
@launch="launch"
@recheck="recheck"
+ @view-in-terminal="viewInTerminal"
/>
diff --git a/plugins/code-server/app/components/LauncherView.stories.ts b/plugins/code-server/app/components/LauncherView.stories.ts
index a87a6584..ec757b7a 100644
--- a/plugins/code-server/app/components/LauncherView.stories.ts
+++ b/plugins/code-server/app/components/LauncherView.stories.ts
@@ -109,6 +109,21 @@ export const Starting: Story = {
},
}
+/** Starting inside a hub with the terminals dock: offer a jump to the session. */
+export const StartingViewInTerminal: Story = {
+ args: {
+ phase: 'starting',
+ detection: localCodeServer,
+ server: { status: 'starting', port: 8080, terminalSessionId: 'devframes_plugin_code-server' },
+ busy: true,
+ canViewInTerminal: true,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ await expect(canvas.getByRole('button', { name: 'View in terminal' })).toBeInTheDocument()
+ },
+}
+
/** Tunnel mode, waiting on the interactive device-login prompt. */
export const TunnelLogin: Story = {
args: {
diff --git a/plugins/code-server/app/components/LauncherView.vue b/plugins/code-server/app/components/LauncherView.vue
index ad162551..b51d4d40 100644
--- a/plugins/code-server/app/components/LauncherView.vue
+++ b/plugins/code-server/app/components/LauncherView.vue
@@ -10,11 +10,14 @@ const props = defineProps<{
detection: CodeServerDetection
server: CodeServerServerInfo
busy: boolean
+ /** Whether a hosting hub can jump to this editor's terminal session. */
+ canViewInTerminal?: boolean
}>()
const emit = defineEmits<{
(e: 'launch'): void
(e: 'recheck'): void
+ (e: 'viewInTerminal'): void
}>()
const DOCS_URL = 'https://coder.com/docs/code-server/latest/install'
@@ -67,6 +70,15 @@ const errorText = computed(() => (props.server.status === 'error' ? props.server
{{ detection.mode === 'tunnel' ? 'Opening the tunnel…' : 'Starting the editor…' }}
+
+ View in terminal
+
({ status: 'stopped' })
const connect = shallowRef(undefined)
const busy = ref(false)
+ /** Whether a hub with the terminals plugin dock is hosting this panel. */
+ const terminalsDockAvailable = ref(false)
+
+ /**
+ * The editor's launch can be watched live in the terminals dock, but only
+ * when the process runs as a hub terminal session (`terminalSessionId`) and
+ * that dock is actually mounted.
+ */
+ const canViewInTerminal = computed(() =>
+ terminalsDockAvailable.value && !!server.terminalSessionId)
const phase = computed(() => {
if (server.status === 'running')
@@ -101,6 +120,18 @@ export function useCodeServer() {
}
}
+ /**
+ * Jump the host shell to this editor's session in the terminals dock. Inert
+ * unless a hub with the terminals dock is hosting the panel and the process
+ * runs as a hub terminal session.
+ */
+ async function viewInTerminal(): Promise {
+ const sessionId = server.terminalSessionId
+ if (!sessionId)
+ return
+ await call(DOCKS_ACTIVATE, { dockId: TERMINALS_DOCK_ID, params: { sessionId } })
+ }
+
async function recheck(): Promise {
if (busy.value)
return
@@ -142,7 +173,35 @@ export function useCodeServer() {
connect.value = undefined
})
onScopeDispose(() => off?.())
+
+ await watchTerminalsDock(client)
+ }
+
+ /**
+ * Track whether the hosting hub has the terminals plugin dock mounted, so the
+ * launcher only offers "view in terminal" when the jump can land somewhere.
+ * Reads the hub's `devframe:docks` slot; outside a hub it never resolves and
+ * the action stays hidden.
+ */
+ async function watchTerminalsDock(client: NonNullable['value']>): Promise {
+ interface DockEntry { id?: string }
+ const hasTerminals = (docks: readonly DockEntry[] | undefined): boolean =>
+ Array.isArray(docks) && docks.some(d => d?.id === TERMINALS_DOCK_ID)
+ try {
+ const slot = await client.sharedState.get(DOCKS_STATE_KEY, { initialValue: [] }) as {
+ value: () => readonly DockEntry[]
+ on: (event: string, cb: (v: readonly DockEntry[]) => void) => (() => void) | void
+ }
+ terminalsDockAvailable.value = hasTerminals(slot.value())
+ const off = slot.on('updated', (docks) => {
+ terminalsDockAvailable.value = hasTerminals(docks)
+ })
+ onScopeDispose(() => off?.())
+ }
+ catch {
+ // No hub or shared state; the terminals jump simply stays unavailable.
+ }
}
- return { detection, server, connect, busy, phase, launch, stop, recheck, bootstrap }
+ return { detection, server, connect, busy, phase, canViewInTerminal, launch, stop, recheck, viewInTerminal, bootstrap }
}
diff --git a/plugins/code-server/src/node/supervisor.ts b/plugins/code-server/src/node/supervisor.ts
index 36aeef0f..6d903474 100644
--- a/plugins/code-server/src/node/supervisor.ts
+++ b/plugins/code-server/src/node/supervisor.ts
@@ -207,11 +207,23 @@ export class CodeServerSupervisor {
status(): CodeServerStatusResult {
return {
detection: { ...this.detection },
- server: { ...this.server },
+ server: this.serverInfo(),
connect: this.connectInfo(),
}
}
+ /**
+ * The server info projected to clients and shared state, tagged with the
+ * live hub terminal session id when one exists so the launcher can offer a
+ * "view in terminal" jump. Standalone runtimes hold no session and leave it
+ * undefined.
+ */
+ private serverInfo(): CodeServerServerInfo {
+ return this.session
+ ? { ...this.server, terminalSessionId: this.sessionId }
+ : { ...this.server }
+ }
+
/**
* Launch the editor (if not already up) and resolve once it is reachable.
* Idempotent while starting/running; returns the live status instead of
@@ -669,7 +681,7 @@ export class CodeServerSupervisor {
private publish(): void {
this.state?.mutate((draft) => {
draft.detection = { ...this.detection }
- draft.server = { ...this.server }
+ draft.server = this.serverInfo()
})
}
diff --git a/plugins/code-server/src/node/types.ts b/plugins/code-server/src/node/types.ts
index c2cfab5e..f7960aa5 100644
--- a/plugins/code-server/src/node/types.ts
+++ b/plugins/code-server/src/node/types.ts
@@ -71,6 +71,14 @@ export interface CodeServerServerInfo {
error?: string
/** Device-login prompt while a tunnel is authenticating. */
login?: CodeServerLogin
+ /**
+ * Id of the hub terminal session mirroring this process, present only when
+ * the editor was launched through the hub's terminals subsystem
+ * (`ctx.terminals`). The launcher uses it to jump the user to that session
+ * in the terminals dock via `hub:docks:activate`; standalone runtimes have
+ * no hub session and leave it undefined.
+ */
+ terminalSessionId?: string
}
/**
diff --git a/plugins/code-server/test/code-server.test.ts b/plugins/code-server/test/code-server.test.ts
index 8e31af6e..a679edd0 100644
--- a/plugins/code-server/test/code-server.test.ts
+++ b/plugins/code-server/test/code-server.test.ts
@@ -71,6 +71,8 @@ describe('@devframes/plugin-code-server', () => {
const result = await supervisor.start()
expect(result.server.status).toBe('running')
expect(result.server.port).toBeGreaterThan(0)
+ // No hub: the process runs directly, so there is no terminal session to jump to.
+ expect(result.server.terminalSessionId).toBeUndefined()
expect(result.connect?.cookie?.name).toBe('code-server-session')
expect(result.connect?.cookie?.value).toMatch(/^[a-f0-9]{64}$/)
expect(result.connect?.path).toBe('/')
@@ -212,6 +214,8 @@ describe('@devframes/plugin-code-server', () => {
const result = await supervisor.start()
expect(result.server.status).toBe('running')
+ // The launcher jumps to this session in the terminals dock.
+ expect(result.server.terminalSessionId).toBe(PLUGIN_ID)
// Launched through the hub, surfaced as exactly one session.
expect(terminals.sessions.size).toBe(1)
@@ -243,9 +247,11 @@ describe('@devframes/plugin-code-server', () => {
await supervisor.start()
const first = terminals.sessions.get(PLUGIN_ID)
- supervisor.stop()
+ const stopped = supervisor.stop()
// The session stays visible, marked stopped.
expect(terminals.sessions.get(PLUGIN_ID)?.status).toBe('stopped')
+ // With no live session, the launcher has nothing to jump to.
+ expect(stopped.server.terminalSessionId).toBeUndefined()
// A fresh start replaces the stale session under the same stable id.
await supervisor.start()