diff --git a/src/features/chat/connection-controller.ts b/src/features/chat/connection-controller.ts index 64df2859..269613ce 100644 --- a/src/features/chat/connection-controller.ts +++ b/src/features/chat/connection-controller.ts @@ -17,12 +17,15 @@ export interface ChatConnectionControllerHost { connectionWork: ChatConnectionWorkTracker; appServer: ChatAppServerController; setClient: (client: AppServerClient | null) => void; + invalidateResumeWork: () => void; loadSharedThreadList: () => Promise; scheduleDeferredDiagnostics: () => void; clearDeferredDiagnostics: () => void; refreshTabHeader: () => void; + resetThreadTurnPresence: (hadTurns: boolean) => void; setStatus: (status: string) => void; addSystemMessage: (text: string) => void; + refreshLiveState: () => void; render: () => void; scheduleRender: () => void; notifyConnectionFailed: () => void; @@ -54,6 +57,17 @@ export class ChatConnectionController { this.host.connectionWork.invalidate(); } + handleExit(): void { + this.invalidate(); + this.host.invalidateResumeWork(); + this.host.setStatus("Codex app-server stopped."); + this.dispatch({ type: "connection/scoped-cleared" }); + this.host.resetThreadTurnPresence(false); + this.host.setClient(null); + this.host.refreshLiveState(); + this.host.render(); + } + async refreshThreads(): Promise { this.host.setClient(this.host.connection.currentClient()); if (!this.host.connection.currentClient()) return; diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index 483164bf..2ea6f59b 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -267,14 +267,7 @@ export class CodexChatView extends ItemView { this.render(); }, onExit: () => { - this.invalidateConnectionWork(); - this.invalidateResumeWork(); - this.setStatus("Codex app-server stopped."); - this.chatState.dispatch({ type: "connection/scoped-cleared" }); - this.threadRename.resetThreadTurnPresence(false); - this.client = null; - this.plugin.refreshThreadsViewLiveState(); - this.render(); + this.connectionController.handleExit(); }, }); this.openCloseController = new ChatViewOpenCloseController({ @@ -401,6 +394,9 @@ export class CodexChatView extends ItemView { setClient: (client) => { this.client = client; }, + invalidateResumeWork: () => { + this.invalidateResumeWork(); + }, loadSharedThreadList: () => this.loadSharedThreadList(), scheduleDeferredDiagnostics: () => { this.scheduleDeferredDiagnostics(); @@ -411,12 +407,18 @@ export class CodexChatView extends ItemView { refreshTabHeader: () => { this.refreshTabHeader(); }, + resetThreadTurnPresence: (hadTurns) => { + this.threadRename.resetThreadTurnPresence(hadTurns); + }, setStatus: (status) => { this.setStatus(status); }, addSystemMessage: (text) => { this.addSystemMessage(text); }, + refreshLiveState: () => { + this.plugin.refreshThreadsViewLiveState(); + }, render: () => { this.render(); }, diff --git a/tests/features/chat/connection-controller.test.ts b/tests/features/chat/connection-controller.test.ts index 1a263edb..54289b13 100644 --- a/tests/features/chat/connection-controller.test.ts +++ b/tests/features/chat/connection-controller.test.ts @@ -35,12 +35,15 @@ function createController({ connected = false, client = {} as AppServerClient } connectionWork: new ChatConnectionWorkTracker(), appServer, setClient, + invalidateResumeWork: vi.fn(), loadSharedThreadList: vi.fn().mockResolvedValue(undefined), scheduleDeferredDiagnostics: vi.fn(), clearDeferredDiagnostics: vi.fn(), refreshTabHeader: vi.fn(), + resetThreadTurnPresence: vi.fn(), setStatus: vi.fn(), addSystemMessage: vi.fn(), + refreshLiveState: vi.fn(), render: vi.fn(), scheduleRender: vi.fn(), notifyConnectionFailed: vi.fn(), @@ -84,4 +87,31 @@ describe("ChatConnectionController", () => { expect(refreshPublishedCapabilityDiagnostics).toHaveBeenCalledOnce(); expect(host.render).toHaveBeenCalledOnce(); }); + + it("clears connection-scoped state on server exit", () => { + const { controller, host, stateStore } = createController({ connected: true }); + stateStore.dispatch({ + type: "thread/list-applied", + threads: [{ id: "thread-1", title: "Thread 1" } as never], + threadsLoaded: true, + availableModels: [{ id: "model-1" } as never], + availableSkills: [{ name: "skill-1" } as never], + }); + + controller.handleExit(); + + expect(host.invalidateResumeWork).toHaveBeenCalledOnce(); + expect(host.setStatus).toHaveBeenCalledWith("Codex app-server stopped."); + expect(host.resetThreadTurnPresence).toHaveBeenCalledWith(false); + expect(host.setClient).toHaveBeenCalledWith(null); + expect(host.refreshLiveState).toHaveBeenCalledOnce(); + expect(host.render).toHaveBeenCalledOnce(); + expect(stateStore.getState()).toMatchObject({ + listedThreads: [], + threadsLoaded: false, + availableModels: [], + availableSkills: [], + runtimePicker: null, + }); + }); });