fix(orcad): a restarted managed server keeps the tab titles of terminals it adopts (#29623)

This commit is contained in:
Neil
2026-10-16 04:14:00 -07:00
committed by GitHub
parent d81b81d57b
commit b051ba90e6
5 changed files with 34 additions and 3 deletions
@@ -154,6 +154,16 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => {
})
})
// A restarted server adopts surviving sessions this way and must not reset their tab titles.
it('returns the session title from attach-only adoption', async () => {
const sessionId = 'attach-title-handoff'
await adapter.spawn({ cols: 80, rows: 24, sessionId })
lastSubprocess._simulateData('\x1b]0;first\x07\x1b]0;⠋ Claude\x07')
await new Promise((r) => setTimeout(r, 50))
await expect(adapter.attach(sessionId)).resolves.toMatchObject({ lastTitle: '⠋ Claude' })
})
it('returns plain result for new sessionId', async () => {
const result = await adapter.spawn({ cols: 80, rows: 24, sessionId: 'brand-new' })
expect(result.id).toBe('brand-new')
@@ -16,7 +16,7 @@ export const LIVENESS_PROBE_TIMEOUT_MS = 2_000
const MAX_TOMBSTONES = 1000
export abstract class DaemonPtySessionControl extends DaemonPtySessionInput {
async attach(id: string): Promise<Pick<PtySpawnResult, 'providerSequence'> | void> {
async attach(id: string): Promise<Pick<PtySpawnResult, 'providerSequence' | 'lastTitle'> | void> {
await this.ensureConnected()
if (!this.canDelegateBackgroundToDaemon) {
this.setPtyBackgrounded(id, false)
@@ -41,7 +41,13 @@ export abstract class DaemonPtySessionControl extends DaemonPtySessionInput {
if (result.exitedBeforeSpawnReply) {
throw new SessionNotFoundError(id)
}
return result.providerSequence ? { providerSequence: result.providerSequence } : undefined
// Why lastTitle: a runtime adopting a surviving session (e.g. a restarted server) never saw it.
return result.providerSequence || result.lastTitle
? {
...(result.providerSequence ? { providerSequence: result.providerSequence } : {}),
...(result.lastTitle ? { lastTitle: result.lastTitle } : {})
}
: undefined
}
hasPty(id: string): boolean {
@@ -186,6 +186,18 @@ describe('registerPtyHandlers', () => {
0
)
})
it('seeds the title of a session adopted by subscriber-driven attach', async () => {
const daemonPtyId = 'repo-1::/tmp/wt@@title-handoff'
const localProvider = createAgentClaimProvider({})
localProvider.attach.mockResolvedValueOnce({ lastTitle: '⠋ Claude' })
setLocalPtyProvider(localProvider as never)
const seedTerminalRestoreTail = vi.fn()
const controller = registerAgentClaimController({ seedTerminalRestoreTail })
await expect(controller.attach(daemonPtyId)).resolves.toBe(true)
expect(seedTerminalRestoreTail).toHaveBeenCalledWith(daemonPtyId, { lastTitle: '⠋ Claude' })
})
it('does not dispatch a runtime PTY spawn after its client disconnects', async () => {
const provider = createAgentClaimProvider({})
setLocalPtyProvider(provider as never)
+3
View File
@@ -139,6 +139,9 @@ export async function attachPtyFromRuntimeController(
sequenceBeforeProviderAttach
)
}
if (attachResult?.lastTitle) {
deps.runtime?.seedTerminalRestoreTail?.(ptyId, { lastTitle: attachResult.lastTitle })
}
return true
} catch {
return false
+1 -1
View File
@@ -147,7 +147,7 @@ export type IPtyProvider = {
providesAgentSessionOwnerListings?: (ptyId: string) => boolean
/** Whether fresh structured creates can replay one spawn across a lost relay response. */
supportsAgentSessionCreateOperations?: (options?: PtyProbeOptions) => boolean | Promise<boolean>
attach(id: string): Promise<Pick<PtySpawnResult, 'providerSequence'> | void>
attach(id: string): Promise<Pick<PtySpawnResult, 'providerSequence' | 'lastTitle'> | void>
hasPty?: (id: string) => boolean
/** Exact provider readback: false only when the provider answered that the PTY is absent. */
probePtyLiveness?: (id: string) => Promise<boolean | null>