Clarify chat panel composition boundaries

This commit is contained in:
murashit 2026-06-10 12:36:14 +09:00
parent fc5f80125d
commit 7a065a6e87
14 changed files with 193 additions and 169 deletions

View file

@ -1,14 +1,20 @@
import type { SharedAppServerMetadata } from "../../../app-server/shared-cache-state";
import type { Thread } from "../../../domain/threads/model";
import type { ChatServerMetadataActions } from "../server-actions/metadata-actions";
import type { ChatServerThreadActions } from "../server-actions/thread-actions";
import type { ChatPanelContext } from "./context";
export interface CachedSharedAppServerStateSource {
cachedThreadList: () => readonly Thread[] | null;
cachedAppServerMetadata: () => SharedAppServerMetadata | null;
}
export function applyCachedSharedAppServerState(
context: ChatPanelContext,
source: CachedSharedAppServerStateSource,
serverThreads: ChatServerThreadActions,
serverMetadata: ChatServerMetadataActions,
): void {
const threads = context.plugin.cachedThreadList();
const threads = source.cachedThreadList();
if (threads) serverThreads.applyThreadList(threads);
const metadata = context.plugin.cachedAppServerMetadata();
const metadata = source.cachedAppServerMetadata();
if (metadata) serverMetadata.applyAppServerMetadata(metadata);
}

View file

@ -10,7 +10,6 @@ import type { ChatThreadActions } from "../threads/thread-actions";
import type { ThreadHistoryController } from "../threads/thread-history-controller";
import type { ThreadRenameController } from "../threads/thread-rename-controller";
import type { ToolbarPanelController } from "./toolbar-controller";
import type { AppServerWarmupActions } from "../session/app-server-warmup-controller";
import type { ChatConnectionController } from "../session/connection-controller";
import type { ChatReconnectActions } from "../session/reconnect-actions";
import type { PendingRequestController } from "../requests/pending-request-controller";
@ -20,11 +19,9 @@ import type { RestoredThreadController } from "../threads/restored-thread-contro
import type { ThreadIdentityActions } from "../threads/thread-identity-actions";
import type { ThreadResumeController } from "../threads/thread-resume-controller";
import type { ThreadSelectionActions } from "../threads/thread-selection-controller";
import type { ChatViewOpenCloseActions } from "./open-close-actions";
import type { ChatViewRenderController } from "./view-render-controller";
import type { ChatViewStateActions } from "./view-state-controller";
import type { ChatMessageRenderer } from "../ui/message-stream";
import type { ChatPanelContext } from "./context";
import type { ChatControllerCompositionPorts } from "./controller-ports";
import {
createChatServerActionControllers,
createChatConnectionControllers,
@ -40,7 +37,7 @@ export interface ChatViewControllers {
manager: ConnectionManager;
controller: ChatConnectionController;
reconnect: ChatReconnectActions;
warmup: AppServerWarmupActions;
scheduleWarmup: () => void;
};
inbound: {
controller: ChatInboundController;
@ -76,12 +73,13 @@ export interface ChatViewControllers {
render: {
controller: ChatViewRenderController;
messages: ChatMessageRenderer;
openClose: ChatViewOpenCloseActions;
viewState: ChatViewStateActions;
openView: () => void;
closeView: () => void;
applyViewState: (state: unknown) => void;
};
}
export function createChatViewControllers(ports: ChatPanelContext): ChatViewControllers {
export function createChatViewControllers(ports: ChatControllerCompositionPorts): ChatViewControllers {
const connection = new ConnectionManager(() => ports.plugin.settings.codexPath, ports.plugin.vaultPath);
const { renderController } = createViewRenderControllerGroup(ports, { connection });
const runtimeSettings = createChatRuntimeSettingsActions({
@ -94,7 +92,7 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
const { history, threadActions, goals, restoredThread, threadResume, threadIdentity, threadRename } = createThreadControllerGroup(ports, {
connection,
});
const { toolbarPanels, viewStateController } = createPanelUiControllerGroup(ports, {
const { toolbarPanels, applyViewState } = createPanelUiControllerGroup(ports, {
threadActions,
});
const { threadSelection } = createThreadSelectionControllerGroup(ports, {
@ -152,7 +150,7 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
goals,
history,
});
const { appServerWarmup, openCloseController } = createConnectionLifecycleControllerGroup(ports, {
const { scheduleAppServerWarmup, openView, closeView } = createConnectionLifecycleControllerGroup(ports, {
connection,
composerController,
messageRenderer,
@ -165,7 +163,7 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
manager: connection,
controller: connectionController,
reconnect: reconnectActions,
warmup: appServerWarmup,
scheduleWarmup: scheduleAppServerWarmup,
},
inbound: {
controller,
@ -201,8 +199,9 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
render: {
controller: renderController,
messages: messageRenderer,
openClose: openCloseController,
viewState: viewStateController,
openView,
closeView,
applyViewState,
},
};
}

View file

@ -16,7 +16,7 @@ import type {
} from "./lifecycle";
import type { ComposerMetaViewModel } from "./model";
export interface ChatPanelContext {
export interface ChatControllerCompositionPorts {
obsidian: ChatPanelObsidianContext;
plugin: CodexChatHost;
state: ChatPanelStateContext;

View file

@ -3,18 +3,20 @@ import type { ChatServerMetadataActions } from "../server-actions/metadata-actio
import type { ChatServerThreadActions } from "../server-actions/thread-actions";
import type { ChatComposerController } from "../composer/controller";
import type { ChatThreadActions } from "../threads/thread-actions";
import { createAppServerWarmupActions } from "../session/app-server-warmup-controller";
import { createChatViewOpenCloseActions } from "./open-close-actions";
import { scheduleAppServerWarmup } from "../session/app-server-warmup-controller";
import { closeChatView, openChatView, type ChatViewLifecycleHost } from "./view-lifecycle";
import { ToolbarPanelController } from "./toolbar-controller";
import { ChatViewRenderController } from "./view-render-controller";
import { createChatViewStateActions } from "./view-state-controller";
import { applyChatViewState } from "./view-state-controller";
import type { ChatMessageRenderer } from "../ui/message-stream";
import { applyCachedSharedAppServerState } from "./cached-app-server-state";
import type { ChatPanelContext } from "./context";
import type { ChatControllerCompositionPorts } from "./controller-ports";
import { createChatShellRenderPort } from "./shell-render";
type ViewRenderControllerGroupPorts = Pick<ChatControllerCompositionPorts, "lifecycle" | "plugin" | "render" | "state">;
export function createViewRenderControllerGroup(
context: ChatPanelContext,
context: ViewRenderControllerGroupPorts,
refs: {
connection: ConnectionManager;
},
@ -42,8 +44,13 @@ export function createViewRenderControllerGroup(
};
}
type ConnectionLifecycleControllerGroupPorts = Pick<
ChatControllerCompositionPorts,
"client" | "lifecycle" | "liveState" | "obsidian" | "plugin" | "render"
>;
export function createConnectionLifecycleControllerGroup(
context: ChatPanelContext,
context: ConnectionLifecycleControllerGroupPorts,
refs: {
connection: ConnectionManager;
composerController: ChatComposerController;
@ -52,76 +59,93 @@ export function createConnectionLifecycleControllerGroup(
serverMetadata: ChatServerMetadataActions;
},
) {
const { obsidian, lifecycle, render, liveState, client } = context;
const { obsidian, plugin, lifecycle, render, liveState, client } = context;
const { deferredTasks } = lifecycle;
const warmupHost = {
deferredTasks,
opened: lifecycle.getOpened,
closing: lifecycle.getClosing,
connected: () => refs.connection.isConnected(),
ensureConnected: client.ensureConnected,
};
const viewLifecycleHost: ChatViewLifecycleHost = {
setOpened: lifecycle.setOpened,
setClosing: lifecycle.setClosing,
registerEvent: obsidian.registerEvent,
registerComposerNoteIndexInvalidation: (register) => {
refs.composerController.registerNoteIndexInvalidation(register);
},
registerPointerDown: obsidian.registerPointerDown,
registerActiveLeafChange: obsidian.registerActiveLeafChange,
handleActiveLeafChange: obsidian.handleActiveLeafChange,
applyCachedSharedAppServerState: () => {
applyCachedSharedAppServerState(plugin, refs.serverThreads, refs.serverMetadata);
},
render: render.now,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
scheduleDeferredRestoredThreadHydration: lifecycle.scheduleDeferredRestoredThreadHydration,
closeToolbarPanelOnOutsidePointer: render.closeToolbarPanelOnOutsidePointer,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredTasks: () => {
deferredTasks.clearAll();
},
panelRoot: render.panelRoot,
disposeMessages: () => {
refs.messageRenderer.dispose();
},
disposeComposer: () => {
refs.composerController.dispose();
},
disconnect: () => {
refs.connection.disconnect();
},
clearClient: client.clear,
refreshLiveState: liveState.refresh,
deferRefreshLiveState: liveState.deferRefresh,
};
return {
appServerWarmup: createAppServerWarmupActions({
deferredTasks,
opened: lifecycle.getOpened,
closing: lifecycle.getClosing,
connected: () => refs.connection.isConnected(),
ensureConnected: client.ensureConnected,
}),
openCloseController: createChatViewOpenCloseActions({
setOpened: lifecycle.setOpened,
setClosing: lifecycle.setClosing,
registerEvent: obsidian.registerEvent,
registerComposerNoteIndexInvalidation: (register) => {
refs.composerController.registerNoteIndexInvalidation(register);
},
registerPointerDown: obsidian.registerPointerDown,
registerActiveLeafChange: obsidian.registerActiveLeafChange,
handleActiveLeafChange: obsidian.handleActiveLeafChange,
applyCachedSharedAppServerState: () => {
applyCachedSharedAppServerState(context, refs.serverThreads, refs.serverMetadata);
},
render: render.now,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
scheduleDeferredRestoredThreadHydration: lifecycle.scheduleDeferredRestoredThreadHydration,
closeToolbarPanelOnOutsidePointer: render.closeToolbarPanelOnOutsidePointer,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredTasks: () => {
deferredTasks.clearAll();
},
panelRoot: render.panelRoot,
disposeMessages: () => {
refs.messageRenderer.dispose();
},
disposeComposer: () => {
refs.composerController.dispose();
},
disconnect: () => {
refs.connection.disconnect();
},
clearClient: client.clear,
refreshLiveState: liveState.refresh,
deferRefreshLiveState: liveState.deferRefresh,
}),
scheduleAppServerWarmup: () => {
scheduleAppServerWarmup(warmupHost);
},
openView: () => {
openChatView(viewLifecycleHost);
},
closeView: () => {
closeChatView(viewLifecycleHost);
},
};
}
type PanelUiControllerGroupPorts = Pick<ChatControllerCompositionPorts, "lifecycle" | "render" | "state" | "thread">;
export function createPanelUiControllerGroup(
context: ChatPanelContext,
context: PanelUiControllerGroupPorts,
refs: {
threadActions: ChatThreadActions;
},
) {
const { lifecycle, render, thread } = context;
const toolbarPanels = new ToolbarPanelController({
stateStore: context.state.stateStore,
threadActions: refs.threadActions,
scheduleRender: render.schedule,
});
const viewStateController = createChatViewStateActions({
const viewStateHost = {
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearRestoredThreadLifecycle: thread.clearRestoredLifecycle,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
restoreThreadPlaceholder: thread.restorePlaceholder,
});
};
return { toolbarPanels, viewStateController };
const toolbarPanels = new ToolbarPanelController({
stateStore: context.state.stateStore,
threadActions: refs.threadActions,
scheduleRender: render.schedule,
});
const applyViewState = (state: unknown) => {
applyChatViewState(viewStateHost, state);
};
return { toolbarPanels, applyViewState };
}

View file

@ -2,7 +2,7 @@ import type { EventRef, WorkspaceLeaf } from "obsidian";
import { unmountChatPanelShell } from "../ui/shell";
export interface ChatViewOpenCloseActionsHost {
export interface ChatViewLifecycleHost {
setOpened: (opened: boolean) => void;
setClosing: (closing: boolean) => void;
registerEvent: (eventRef: EventRef) => void;
@ -27,23 +27,7 @@ export interface ChatViewOpenCloseActionsHost {
deferRefreshLiveState: () => void;
}
export interface ChatViewOpenCloseActions {
open: () => void;
close: () => void;
}
export function createChatViewOpenCloseActions(host: ChatViewOpenCloseActionsHost): ChatViewOpenCloseActions {
return {
open: () => {
openChatView(host);
},
close: () => {
closeChatView(host);
},
};
}
function openChatView(host: ChatViewOpenCloseActionsHost): void {
export function openChatView(host: ChatViewLifecycleHost): void {
host.setOpened(true);
host.setClosing(false);
host.registerComposerNoteIndexInvalidation((eventRef) => {
@ -59,7 +43,7 @@ function openChatView(host: ChatViewOpenCloseActionsHost): void {
host.scheduleDeferredRestoredThreadHydration();
}
function closeChatView(host: ChatViewOpenCloseActionsHost): void {
export function closeChatView(host: ChatViewLifecycleHost): void {
host.setOpened(false);
host.setClosing(true);
host.invalidateConnectionWork();

View file

@ -9,23 +9,15 @@ export interface ChatViewStateControllerHost {
restoreThreadPlaceholder: (restoredThread: RestoredThreadState) => void;
}
export interface ChatViewStateActions {
applyState(state: unknown): void;
}
export function applyChatViewState(host: ChatViewStateControllerHost, state: unknown): void {
const restoredThread = parseRestoredThreadState(state);
if (!restoredThread) {
host.invalidateResumeWork();
host.clearRestoredThreadLifecycle();
host.clearDeferredRestoredThreadHydration();
host.scheduleDeferredAppServerWarmup();
return;
}
export function createChatViewStateActions(host: ChatViewStateControllerHost): ChatViewStateActions {
return {
applyState(state) {
const restoredThread = parseRestoredThreadState(state);
if (!restoredThread) {
host.invalidateResumeWork();
host.clearRestoredThreadLifecycle();
host.clearDeferredRestoredThreadHydration();
host.scheduleDeferredAppServerWarmup();
return;
}
host.restoreThreadPlaceholder(restoredThread);
},
};
host.restoreThreadPlaceholder(restoredThread);
}

View file

@ -8,20 +8,13 @@ export interface AppServerWarmupControllerHost {
ensureConnected: () => Promise<void>;
}
export interface AppServerWarmupActions {
schedule(): void;
}
export function createAppServerWarmupActions(host: AppServerWarmupControllerHost): AppServerWarmupActions {
export function scheduleAppServerWarmup(host: AppServerWarmupControllerHost): void {
const shouldWarmup = (): boolean => host.opened() && !host.connected();
return {
schedule() {
if (!shouldWarmup()) return;
host.deferredTasks.scheduleAppServerWarmup(() => {
if (!shouldWarmup() || host.closing()) return;
void host.ensureConnected();
});
},
};
if (!shouldWarmup()) return;
host.deferredTasks.scheduleAppServerWarmup(() => {
if (!shouldWarmup() || host.closing()) return;
void host.ensureConnected();
});
}

View file

@ -10,10 +10,12 @@ import type { ServerRequestActions } from "../requests/server-request-actions";
import type { ChatThreadGoalActions } from "../threads/thread-goal-actions";
import type { ThreadRenameController } from "../threads/thread-rename-controller";
import { ChatInboundController } from "../inbound/controller";
import type { ChatPanelContext } from "../panel/context";
import type { ChatControllerCompositionPorts } from "../panel/controller-ports";
type ChatServerActionControllerPorts = Pick<ChatControllerCompositionPorts, "plugin" | "runtime" | "state">;
export function createChatServerActionControllers(
context: ChatPanelContext,
context: ChatServerActionControllerPorts,
refs: {
connection: ConnectionManager;
goals: ChatThreadGoalActions;
@ -53,8 +55,10 @@ export function createChatServerActionControllers(
return { serverThreads, serverMetadata, serverDiagnostics };
}
type ChatInboundControllerPorts = Pick<ChatControllerCompositionPorts, "plugin" | "render" | "state" | "thread">;
export function createChatInboundController(
context: ChatPanelContext,
context: ChatInboundControllerPorts,
refs: {
serverMetadata: ChatServerMetadataActions;
serverDiagnostics: ChatServerDiagnosticsActions;
@ -87,8 +91,13 @@ export function createChatInboundController(
});
}
type ChatConnectionControllerPorts = Pick<
ChatControllerCompositionPorts,
"client" | "lifecycle" | "liveState" | "plugin" | "render" | "state" | "status" | "thread"
>;
export function createChatConnectionControllers(
context: ChatPanelContext,
context: ChatConnectionControllerPorts,
refs: {
connection: ConnectionManager;
serverMetadata: ChatServerMetadataActions;
@ -130,8 +139,13 @@ export function createChatConnectionControllers(
};
}
type ChatReconnectControllerGroupPorts = Pick<
ChatControllerCompositionPorts,
"client" | "lifecycle" | "render" | "state" | "status" | "thread"
>;
export function createChatReconnectControllerGroup(
context: ChatPanelContext,
context: ChatReconnectControllerGroupPorts,
refs: {
connection: ConnectionManager;
},

View file

@ -9,10 +9,15 @@ import { ThreadResumeController } from "./thread-resume-controller";
import { createThreadSelectionActions } from "./thread-selection-controller";
import { RestoredThreadController } from "./restored-thread-controller";
import type { ToolbarPanelController } from "../panel/toolbar-controller";
import type { ChatPanelContext } from "../panel/context";
import type { ChatControllerCompositionPorts } from "../panel/controller-ports";
type ThreadControllerGroupPorts = Pick<
ChatControllerCompositionPorts,
"client" | "composer" | "lifecycle" | "liveState" | "obsidian" | "plugin" | "render" | "scroll" | "state" | "status" | "thread"
>;
export function createThreadControllerGroup(
context: ChatPanelContext,
context: ThreadControllerGroupPorts,
refs: {
connection: ConnectionManager;
},
@ -132,8 +137,10 @@ export function createThreadControllerGroup(
};
}
type ThreadSelectionControllerGroupPorts = Pick<ChatControllerCompositionPorts, "plugin" | "state" | "status" | "thread">;
export function createThreadSelectionControllerGroup(
context: ChatPanelContext,
context: ThreadSelectionControllerGroupPorts,
refs: {
toolbarPanels: ToolbarPanelController;
},

View file

@ -15,10 +15,15 @@ import type { ThreadRenameController } from "../threads/thread-rename-controller
import type { ChatInboundController } from "../inbound/controller";
import { currentModel } from "../runtime/effective-settings";
import { ChatMessageRenderer } from "../ui/message-stream";
import type { ChatPanelContext } from "../panel/context";
import type { ChatControllerCompositionPorts } from "../panel/controller-ports";
type ConversationSurfaceControllerGroupPorts = Pick<
ChatControllerCompositionPorts,
"client" | "lifecycle" | "liveState" | "obsidian" | "plugin" | "render" | "runtime" | "scroll" | "state" | "status" | "thread"
>;
export function createConversationSurfaceControllerGroup(
context: ChatPanelContext,
context: ConversationSurfaceControllerGroupPorts,
refs: {
controller: ChatInboundController;
serverThreads: ChatServerThreadActions;

View file

@ -30,7 +30,7 @@ import {
type ChatViewRenderScheduleOptions,
} from "./panel/lifecycle";
import { ChatMessageScrollIntentController } from "./panel/message-scroll-intent-controller";
import type { ChatPanelContext } from "./panel/context";
import type { ChatControllerCompositionPorts } from "./panel/controller-ports";
import { createChatViewControllers, type ChatViewControllers } from "./panel/composition";
import { activeComposerThreadName, composerMetaViewModel, composerPlaceholder, renderComposerSlot } from "./panel/slots/composer";
import { renderGoalSlot } from "./panel/slots/goal";
@ -65,7 +65,7 @@ export class CodexChatView extends ItemView {
this.slotPorts = this.createSlotRendererPorts();
}
private createControllerPorts(): ChatPanelContext {
private createControllerPorts(): ChatControllerCompositionPorts {
// Some callbacks are late-bound to controllers assigned immediately after this object is created.
// Controller constructors must not invoke those callbacks synchronously during composition.
return {
@ -398,7 +398,7 @@ export class CodexChatView extends ItemView {
override async setState(state: unknown, result: ViewStateResult): Promise<void> {
await super.setState(state, result);
this.controllers.render.viewState.applyState(state);
this.controllers.render.applyViewState(state);
}
refreshSettings(): void {
@ -464,11 +464,11 @@ export class CodexChatView extends ItemView {
}
override async onOpen(): Promise<void> {
this.controllers.render.openClose.open();
this.controllers.render.openView();
}
override async onClose(): Promise<void> {
this.controllers.render.openClose.close();
this.controllers.render.closeView();
}
setComposerText(text: string): void {
@ -549,7 +549,7 @@ export class CodexChatView extends ItemView {
}
private scheduleDeferredAppServerWarmup(): void {
this.controllers.connection.warmup.schedule();
this.controllers.connection.scheduleWarmup();
}
private activeThreadTitle(): string | null {

View file

@ -3,16 +3,16 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { EventRef } from "obsidian";
import { createChatViewOpenCloseActions, type ChatViewOpenCloseActionsHost } from "../../../../src/features/chat/panel/open-close-actions";
import { closeChatView, openChatView, type ChatViewLifecycleHost } from "../../../../src/features/chat/panel/view-lifecycle";
import { unmountChatPanelShell } from "../../../../src/features/chat/ui/shell";
vi.mock("../../../../src/features/chat/ui/shell", () => ({
unmountChatPanelShell: vi.fn(),
}));
function createHost(overrides: Partial<ChatViewOpenCloseActionsHost> = {}) {
function createHost(overrides: Partial<ChatViewLifecycleHost> = {}) {
const root = document.createElement("div");
const host: ChatViewOpenCloseActionsHost = {
const host: ChatViewLifecycleHost = {
setOpened: vi.fn(),
setClosing: vi.fn(),
registerEvent: vi.fn(),
@ -39,18 +39,18 @@ function createHost(overrides: Partial<ChatViewOpenCloseActionsHost> = {}) {
deferRefreshLiveState: vi.fn(),
...overrides,
};
return { controller: createChatViewOpenCloseActions(host), host, root };
return { host, root };
}
describe("createChatViewOpenCloseActions", () => {
describe("chat view lifecycle", () => {
beforeEach(() => {
vi.mocked(unmountChatPanelShell).mockClear();
});
it("registers open events and schedules startup work", () => {
const { controller, host } = createHost();
const { host } = createHost();
controller.open();
openChatView(host);
expect(host.setOpened).toHaveBeenCalledWith(true);
expect(host.setClosing).toHaveBeenCalledWith(false);
@ -64,9 +64,9 @@ describe("createChatViewOpenCloseActions", () => {
});
it("disposes mounted resources and refreshes live state on close", () => {
const { controller, host, root } = createHost();
const { host, root } = createHost();
controller.close();
closeChatView(host);
expect(host.setOpened).toHaveBeenCalledWith(false);
expect(host.setClosing).toHaveBeenCalledWith(true);

View file

@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { createChatViewStateActions, type ChatViewStateControllerHost } from "../../../../src/features/chat/panel/view-state-controller";
import { applyChatViewState, type ChatViewStateControllerHost } from "../../../../src/features/chat/panel/view-state-controller";
function createController(overrides: Partial<ChatViewStateControllerHost> = {}) {
const host: ChatViewStateControllerHost = {
@ -11,14 +11,14 @@ function createController(overrides: Partial<ChatViewStateControllerHost> = {})
restoreThreadPlaceholder: vi.fn(),
...overrides,
};
return { controller: createChatViewStateActions(host), host };
return { host };
}
describe("ChatViewStateController", () => {
it("restores a thread placeholder from persisted view state", () => {
const { controller, host } = createController();
const { host } = createController();
controller.applyState({ threadId: "thread", threadTitle: "Title" });
applyChatViewState(host, { threadId: "thread", threadTitle: "Title" });
expect(host.restoreThreadPlaceholder).toHaveBeenCalledWith({
threadId: "thread",
@ -30,9 +30,9 @@ describe("ChatViewStateController", () => {
});
it("clears restored lifecycle and schedules warmup when no thread is restored", () => {
const { controller, host } = createController();
const { host } = createController();
controller.applyState({ version: 1 });
applyChatViewState(host, { version: 1 });
expect(host.invalidateResumeWork).toHaveBeenCalledOnce();
expect(host.clearRestoredThreadLifecycle).toHaveBeenCalledOnce();

View file

@ -2,7 +2,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createAppServerWarmupActions } from "../../../../src/features/chat/session/app-server-warmup-controller";
import { scheduleAppServerWarmup } from "../../../../src/features/chat/session/app-server-warmup-controller";
import { ChatViewDeferredTasks } from "../../../../src/features/chat/panel/lifecycle";
function createController({
@ -11,14 +11,14 @@ function createController({
connected = false,
}: { opened?: boolean; closing?: boolean; connected?: boolean } = {}) {
const ensureConnected = vi.fn().mockResolvedValue(undefined);
const controller = createAppServerWarmupActions({
const host = {
deferredTasks: new ChatViewDeferredTasks(() => window),
opened: () => opened,
closing: () => closing,
connected: () => connected,
ensureConnected,
});
return { controller, ensureConnected };
};
return { host, ensureConnected };
}
describe("AppServerWarmupController", () => {
@ -27,9 +27,9 @@ describe("AppServerWarmupController", () => {
});
it("connects on the next tick when the opened view is disconnected", async () => {
const { controller, ensureConnected } = createController();
const { host, ensureConnected } = createController();
controller.schedule();
scheduleAppServerWarmup(host);
await vi.advanceTimersByTimeAsync(0);
expect(ensureConnected).toHaveBeenCalledOnce();
@ -39,8 +39,8 @@ describe("AppServerWarmupController", () => {
const closed = createController({ opened: false });
const connected = createController({ connected: true });
closed.controller.schedule();
connected.controller.schedule();
scheduleAppServerWarmup(closed.host);
scheduleAppServerWarmup(connected.host);
await vi.advanceTimersByTimeAsync(0);
@ -49,9 +49,9 @@ describe("AppServerWarmupController", () => {
});
it("skips a scheduled warmup if the view is closing", async () => {
const { controller, ensureConnected } = createController({ closing: true });
const { host, ensureConnected } = createController({ closing: true });
controller.schedule();
scheduleAppServerWarmup(host);
await vi.advanceTimersByTimeAsync(0);
expect(ensureConnected).not.toHaveBeenCalled();