From ff82da37c4f46e9fa1186aa0c6d38e80035337e0 Mon Sep 17 00:00:00 2001 From: murashit Date: Fri, 29 May 2026 06:50:07 +0900 Subject: [PATCH] Extract chat view state controller --- src/features/chat/view-state-controller.ts | 27 ++++++++++++ src/features/chat/view.ts | 32 +++++++++----- .../chat/view-state-controller.test.ts | 43 +++++++++++++++++++ 3 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 src/features/chat/view-state-controller.ts create mode 100644 tests/features/chat/view-state-controller.test.ts diff --git a/src/features/chat/view-state-controller.ts b/src/features/chat/view-state-controller.ts new file mode 100644 index 00000000..1d073a93 --- /dev/null +++ b/src/features/chat/view-state-controller.ts @@ -0,0 +1,27 @@ +import { parseRestoredThreadState } from "./view-snapshot"; +import type { RestoredThreadState } from "./view-lifecycle"; + +export interface ChatViewStateControllerHost { + invalidateResumeWork: () => void; + clearRestoredThreadLifecycle: () => void; + clearDeferredRestoredThreadHydration: () => void; + scheduleDeferredAppServerWarmup: () => void; + restoreThreadPlaceholder: (restoredThread: RestoredThreadState) => void; +} + +export class ChatViewStateController { + constructor(private readonly host: ChatViewStateControllerHost) {} + + applyState(state: unknown): void { + const restoredThread = parseRestoredThreadState(state); + if (!restoredThread) { + this.host.invalidateResumeWork(); + this.host.clearRestoredThreadLifecycle(); + this.host.clearDeferredRestoredThreadHydration(); + this.host.scheduleDeferredAppServerWarmup(); + return; + } + + this.host.restoreThreadPlaceholder(restoredThread); + } +} diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index 87107c4c..2c61aa9f 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -39,7 +39,7 @@ import { statusSummaryLines as buildStatusSummaryLines, toolbarViewModel as buildToolbarViewModel, } from "./view-model"; -import { composerSlotSnapshot, openPanelTurnLifecycle, parseRestoredThreadState } from "./view-snapshot"; +import { composerSlotSnapshot, openPanelTurnLifecycle } from "./view-snapshot"; import { ChatConnectionWorkTracker, ChatResumeWorkTracker, @@ -60,6 +60,7 @@ import { ChatViewRenderController } from "./view-render-controller"; import { ChatViewOpenCloseController } from "./view-open-close-controller"; import { PlanImplementationController } from "./plan-implementation-controller"; import { ThreadSelectionController } from "./thread-selection-controller"; +import { ChatViewStateController } from "./view-state-controller"; export interface CodexChatHost { readonly settings: CodexPanelSettings; @@ -101,6 +102,7 @@ export class CodexChatView extends ItemView { private readonly messageRenderer: ChatMessageRenderer; private readonly renderController: ChatViewRenderController; private readonly openCloseController: ChatViewOpenCloseController; + private readonly viewStateController: ChatViewStateController; private readonly messageScroll: ChatMessageScrollController; private readonly turnSubmission: TurnSubmissionController; private readonly slashCommands: SlashCommandController; @@ -565,6 +567,23 @@ export class CodexChatView extends ItemView { this.refreshTabHeader(); }, }); + this.viewStateController = new ChatViewStateController({ + invalidateResumeWork: () => { + this.invalidateResumeWork(); + }, + clearRestoredThreadLifecycle: () => { + this.clearRestoredThreadLifecycle(); + }, + clearDeferredRestoredThreadHydration: () => { + this.clearDeferredRestoredThreadHydration(); + }, + scheduleDeferredAppServerWarmup: () => { + this.scheduleDeferredAppServerWarmup(); + }, + restoreThreadPlaceholder: (restoredThread) => { + this.restoreThreadPlaceholder(restoredThread); + }, + }); this.threadResume = new ThreadResumeController({ stateStore: this.chatState, vaultPath: this.plugin.vaultPath, @@ -683,16 +702,7 @@ export class CodexChatView extends ItemView { override async setState(state: unknown, result: ViewStateResult): Promise { await super.setState(state, result); - const restoredThread = parseRestoredThreadState(state); - if (!restoredThread) { - this.invalidateResumeWork(); - this.clearRestoredThreadLifecycle(); - this.clearDeferredRestoredThreadHydration(); - this.scheduleDeferredAppServerWarmup(); - return; - } - - this.restoreThreadPlaceholder(restoredThread); + this.viewStateController.applyState(state); } refreshSettings(): void { diff --git a/tests/features/chat/view-state-controller.test.ts b/tests/features/chat/view-state-controller.test.ts new file mode 100644 index 00000000..a07cc267 --- /dev/null +++ b/tests/features/chat/view-state-controller.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "vitest"; + +import { ChatViewStateController, type ChatViewStateControllerHost } from "../../../src/features/chat/view-state-controller"; + +function createController(overrides: Partial = {}) { + const host: ChatViewStateControllerHost = { + invalidateResumeWork: vi.fn(), + clearRestoredThreadLifecycle: vi.fn(), + clearDeferredRestoredThreadHydration: vi.fn(), + scheduleDeferredAppServerWarmup: vi.fn(), + restoreThreadPlaceholder: vi.fn(), + ...overrides, + }; + return { controller: new ChatViewStateController(host), host }; +} + +describe("ChatViewStateController", () => { + it("restores a thread placeholder from persisted view state", () => { + const { controller, host } = createController(); + + controller.applyState({ threadId: "thread", threadTitle: "Title" }); + + expect(host.restoreThreadPlaceholder).toHaveBeenCalledWith({ + threadId: "thread", + title: "Title", + explicitName: null, + }); + expect(host.invalidateResumeWork).not.toHaveBeenCalled(); + expect(host.scheduleDeferredAppServerWarmup).not.toHaveBeenCalled(); + }); + + it("clears restored lifecycle and schedules warmup when no thread is restored", () => { + const { controller, host } = createController(); + + controller.applyState({ version: 1 }); + + expect(host.invalidateResumeWork).toHaveBeenCalledOnce(); + expect(host.clearRestoredThreadLifecycle).toHaveBeenCalledOnce(); + expect(host.clearDeferredRestoredThreadHydration).toHaveBeenCalledOnce(); + expect(host.scheduleDeferredAppServerWarmup).toHaveBeenCalledOnce(); + expect(host.restoreThreadPlaceholder).not.toHaveBeenCalled(); + }); +});