Extract chat view state controller

This commit is contained in:
murashit 2026-05-29 06:50:07 +09:00
parent 47eb02aeb3
commit ff82da37c4
3 changed files with 91 additions and 11 deletions

View file

@ -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);
}
}

View file

@ -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<void> {
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 {

View file

@ -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<ChatViewStateControllerHost> = {}) {
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();
});
});