diff --git a/src/main/daemon/providers/adapter/daemon-pty-session-inventory.ts b/src/main/daemon/providers/adapter/daemon-pty-session-inventory.ts index 45eb38722ce..f053d3eb482 100644 --- a/src/main/daemon/providers/adapter/daemon-pty-session-inventory.ts +++ b/src/main/daemon/providers/adapter/daemon-pty-session-inventory.ts @@ -56,7 +56,8 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti ...(session.pid ? { rootProcessId: session.pid } : {}), // Spawn cwd remains authoritative until the daemon reports live cwd. cwd: session.cwd ?? this.initialCwds.get(session.sessionId) ?? '', - title: 'shell', + // The listing carries no title; attach seeds the live one from the daemon snapshot. + title: '', ...(session.foregroundProcess ? { foregroundProcess: session.foregroundProcess } : {}), diff --git a/src/main/daemon/providers/adapter/testing/daemon-pty-adapter-session-adoption.test.ts b/src/main/daemon/providers/adapter/testing/daemon-pty-adapter-session-adoption.test.ts index 7b6e966c3df..0beb3198ad3 100644 --- a/src/main/daemon/providers/adapter/testing/daemon-pty-adapter-session-adoption.test.ts +++ b/src/main/daemon/providers/adapter/testing/daemon-pty-adapter-session-adoption.test.ts @@ -297,6 +297,12 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => { ) }) + it('reports no title, since a made-up one would read as the terminal’s own', async () => { + await adapter.spawn({ cols: 80, rows: 24 }) + + expect((await adapter.listProcesses()).map((row) => row.title)).toEqual(['']) + }) + // Reconcile missed disconnect-time exits so cached hasPty results cannot block absence proofs. it('drops cached session ids an authoritative inventory omits', async () => { const { id } = await adapter.spawn({ cols: 80, rows: 24 }) diff --git a/src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts b/src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts index 77e13a45b90..46f496709f3 100644 --- a/src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts +++ b/src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts @@ -3,6 +3,7 @@ import './rpc/unused-default-rpc-methods.test-fixture' * Exercise real runtime/multiplex with controller data gated on attach; no renderer is present. */ import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' +import { deriveRemoteRuntimeTerminalCreateHandle } from './terminal/identity/remote-runtime-terminal-create-identity' import { RpcDispatcher } from './rpc/dispatcher' import type { RpcRequest } from './rpc/core' import { TERMINAL_METHODS } from './rpc/methods/terminal' @@ -31,6 +32,7 @@ type RuntimeInternals = { ) => Promise // Optional so this suite still runs, and fails on behavior, against a runtime without it. notePtyEndpointLoss?: (ptyId: string) => void + resolveTerminalWorkspaceLaunchScope: () => Promise<{ id: string; connectionId: null }> } function internals(runtime: OrcaRuntimeService): RuntimeInternals { @@ -42,6 +44,8 @@ type DaemonSessionModel = { rows: number attached: boolean screen: string + terminalHandle?: string + lastTitle?: string } /** Models the daemon provider boundary: inventory of live sessions, data @@ -70,6 +74,7 @@ function createDaemonProviderModel(opts: { snapshotCapable: boolean }) { cwd: '/tmp/wt', worktreeId: WORKTREE_ID, title: '', + ...(session.terminalHandle ? { terminalHandle: session.terminalHandle } : {}), cols: session.cols, rows: session.rows })), @@ -98,6 +103,10 @@ function createDaemonProviderModel(opts: { snapshotCapable: boolean }) { return false } session.attached = true + // The runtime controller seeds the attach snapshot's title, as attachPtyFromRuntimeController does. + if (session.lastTitle) { + runtime?.seedTerminalRestoreTail(ptyId, { lastTitle: session.lastTitle }) + } return true }, serializeProviderBuffer: async (ptyId: string) => { @@ -448,6 +457,49 @@ describe('subscriber-driven daemon attach (never-activated tab)', () => { expect(read.tail.join('\n')).toContain('worker still printing') }) + it('streams a survivor a terminal.create retry adopts after a restart', async () => { + const runtime = new OrcaRuntimeService() + const model = createDaemonProviderModel({ snapshotCapable: false }) + model.bind(runtime) + runtime.setPtyController(model.controller as never) + vi.spyOn(internals(runtime), 'resolveTerminalWorkspaceLaunchScope').mockResolvedValue({ + id: WORKTREE_ID, + connectionId: null + }) + model.sessions.set(PTY_ID, { + cols: 80, + rows: 24, + attached: false, + screen: '', + terminalHandle: deriveRemoteRuntimeTerminalCreateHandle( + 'device-a', + WORKTREE_ID, + 'mutation-1' + ), + lastTitle: '✳ Claude Code' + }) + const spawn = vi.fn() + + const adopted = await runtime.dedupeTerminalCreate( + 'device-a', + `id:${WORKTREE_ID}`, + 'mutation-1', + true, + spawn + ) + + expect(spawn).not.toHaveBeenCalled() + await vi.waitFor(() => expect(model.attachCalls).toEqual([PTY_ID])) + expect(model.emitData(PTY_ID, 'still working\r\n')).toBe(true) + const [listed] = (await runtime.listTerminals()).terminals + expect(listed).toMatchObject({ + handle: adopted.handle, + title: '✳ Claude Code', + preview: 'still working' + }) + expect(listed.lastOutputAt).not.toBeNull() + }) + it('does not subscriber-attach or provider-read a session this app already spawned', async () => { const { runtime, model, handle } = setupNeverAttachedDaemonSession({ snapshotCapable: true, diff --git a/src/main/runtime/terminal/identity/pty-worktree-record.ts b/src/main/runtime/terminal/identity/pty-worktree-record.ts index c6ca08d2156..c402cfdf2c4 100644 --- a/src/main/runtime/terminal/identity/pty-worktree-record.ts +++ b/src/main/runtime/terminal/identity/pty-worktree-record.ts @@ -132,7 +132,7 @@ export abstract class OrcaRuntimeWithRecordPtyWorktree extends RuntimeWorkspaceR managementTitle: null, managementTitleAt: null, controllerTitle: null, - title: state.title ?? null, + title: state.title || null, titleUpdatedAt: titleObservedAt, titleObservedAtEpochMs: titleObservedAt !== null ? Date.now() : null, lastOutputAt: state.lastOutputAt ?? null, diff --git a/src/main/runtime/terminal/launch/terminal-create-deduplication.ts b/src/main/runtime/terminal/launch/terminal-create-deduplication.ts index 8b6b06a7465..a26ef398aa7 100644 --- a/src/main/runtime/terminal/launch/terminal-create-deduplication.ts +++ b/src/main/runtime/terminal/launch/terminal-create-deduplication.ts @@ -144,6 +144,8 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun if (adoptedHandle !== terminalHandle) { throw new Error('terminal_create_identity_conflict') } + // Same as inventory adoption: the stream carries this survivor's title and output. + this.mainDaemonAttachments.reconcile(session.id) return { handle: adoptedHandle, ptyId: session.id,