diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 9aa5c00a853..46d1963ba31 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -13487,6 +13487,22 @@ describe('OrcaRuntimeService', () => { expect(read.tail).toEqual(['after reload']) }) + it('notifies remote clients to refetch projects after the renderer graph reloads', () => { + const runtime = new OrcaRuntimeService(store) + const events: unknown[] = [] + runtime.onClientEvent((event) => events.push(event)) + + syncSinglePty(runtime) + expect(events).toEqual([]) + + runtime.markRendererReloading(1) + syncSinglePty(runtime) + expect(events).toEqual([{ type: 'reposChanged' }]) + + syncSinglePty(runtime) + expect(events).toEqual([{ type: 'reposChanged' }]) + }) + it('keeps preallocated terminal handles valid when a reload graph omits the live leaf', async () => { const runtime = new OrcaRuntimeService(store) const handle = runtime.preAllocateHandleForPty('pty-1') diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 293075176e9..4c46e573616 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -3081,6 +3081,7 @@ export class OrcaRuntimeService { throw new Error('Runtime graph publisher does not match the authoritative window') } + const restoresReloadedRendererGraph = this.graphStatus === 'reloading' this.tabs = new Map(graph.tabs.map((tab) => [tab.tabId, tab])) this.syncMobileSessionTabs(graph.mobileSessionTabs) const nextLeaves = new Map() @@ -3198,6 +3199,11 @@ export class OrcaRuntimeService { this.notifyMobileSessionTabSnapshots() this.graphStatus = 'ready' this.refreshWritableFlags() + if (restoresReloadedRendererGraph) { + // Why: remote clients can discard project snapshots while this runtime is + // unavailable; the first rebuilt graph must tell them to fetch again. + this.emitClientEvent({ type: 'reposChanged' }) + } for (const leaf of this.leaves.values()) { this.adoptPreAllocatedHandle(leaf) } diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index 6c72e259c72..ae1576f04df 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -26,6 +26,7 @@ import { resetAgentStartupDelayedDeliveryForTests } from '@/lib/agent-startup-delayed-delivery' import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent' +import type { createPaneForegroundAgentTracker } from './pane-foreground-agent-tracker' // Repro command: // pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts -t "OpenTUI-style small ANSI redraw" @@ -257,6 +258,12 @@ let mockStoreState: StoreState let transportFactoryQueue: MockTransport[] = [] let createdTransportOptions: Record[] = [] let storeSubscribers: ((state: StoreState) => void)[] = [] +type PaneForegroundAgentTrackerFactory = typeof createPaneForegroundAgentTracker +type PaneForegroundAgentTracker = ReturnType +type PaneForegroundAgentTrackerModule = { + createPaneForegroundAgentTracker: PaneForegroundAgentTrackerFactory +} +const paneForegroundAgentTrackers = new Set() vi.mock('@/runtime/sync-runtime-graph', () => ({ scheduleRuntimeGraphSync @@ -278,6 +285,20 @@ vi.mock('./terminal-webgl-atlas-recovery', () => ({ scheduleTerminalWebglAtlasRecovery })) +vi.mock('./pane-foreground-agent-tracker', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + createPaneForegroundAgentTracker: ( + ...args: Parameters + ) => { + const tracker = actual.createPaneForegroundAgentTracker(...args) + paneForegroundAgentTrackers.add(tracker) + return tracker + } + } +}) + function notifyStoreSubscribers(): void { for (const listener of storeSubscribers.slice()) { listener(mockStoreState) @@ -894,6 +915,12 @@ describe('connectPanePty', () => { }) afterEach(() => { + // Why: most cases do not retain their pane binding; cancel its delayed reads + // before they can publish into the next case's freshly reset store mock. + for (const tracker of paneForegroundAgentTrackers) { + tracker.dispose() + } + paneForegroundAgentTrackers.clear() vi.useRealTimers() if (originalRequestAnimationFrame) { globalThis.requestAnimationFrame = originalRequestAnimationFrame