mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Make chat controller composition explicit
This commit is contained in:
parent
92d98ed767
commit
36d039d977
15 changed files with 837 additions and 230 deletions
|
|
@ -19,9 +19,10 @@ 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 { ChatViewRenderController } from "./view-render-controller";
|
||||
import type { ChatViewRenderController, ChatViewSlotRenderers } from "./view-render-controller";
|
||||
import type { ChatMessageRenderer } from "../ui/message-stream";
|
||||
import type { ChatControllerCompositionPorts } from "./controller-ports";
|
||||
import { createChatControllerCompositionActions, requireCompositionRef, type ChatControllerCompositionRefs } from "./controller-wiring";
|
||||
import {
|
||||
createChatServerActionControllers,
|
||||
createChatConnectionControllers,
|
||||
|
|
@ -73,6 +74,7 @@ export interface ChatViewControllers {
|
|||
render: {
|
||||
controller: ChatViewRenderController;
|
||||
messages: ChatMessageRenderer;
|
||||
attachSlotRenderers: (slotRenderers: ChatViewSlotRenderers) => void;
|
||||
openView: () => void;
|
||||
closeView: () => void;
|
||||
applyViewState: (state: unknown) => void;
|
||||
|
|
@ -82,82 +84,191 @@ export interface ChatViewControllers {
|
|||
export function createChatViewControllers(ports: ChatControllerCompositionPorts): ChatViewControllers {
|
||||
const connection = new ConnectionManager(() => ports.plugin.settings.codexPath, ports.plugin.vaultPath);
|
||||
const { renderController } = createViewRenderControllerGroup(ports, { connection });
|
||||
const refs: ChatControllerCompositionRefs = {
|
||||
renderController,
|
||||
controller: null,
|
||||
connectionController: null,
|
||||
threadSelection: null,
|
||||
threadRename: null,
|
||||
threadResume: null,
|
||||
restoredThread: null,
|
||||
serverMetadata: null,
|
||||
serverDiagnostics: null,
|
||||
messageRenderer: null,
|
||||
composerController: null,
|
||||
};
|
||||
const actions = createChatControllerCompositionActions(ports, refs);
|
||||
const runtimeSettings = createChatRuntimeSettingsActions({
|
||||
stateStore: ports.state.stateStore,
|
||||
currentClient: ports.client.getClient,
|
||||
runtimeSnapshot: ports.runtime.runtimeSnapshot,
|
||||
collaborationModeLabel: ports.runtime.collaborationModeLabel,
|
||||
addSystemMessage: ports.status.addSystemMessage,
|
||||
addSystemMessage: actions.status.addSystemMessage,
|
||||
});
|
||||
const { history, threadActions, goals, restoredThread, threadResume, threadIdentity, threadRename } = createThreadControllerGroup(ports, {
|
||||
connection,
|
||||
});
|
||||
const { toolbarPanels, applyViewState } = createPanelUiControllerGroup(ports, {
|
||||
threadActions,
|
||||
});
|
||||
const { threadSelection } = createThreadSelectionControllerGroup(ports, {
|
||||
toolbarPanels,
|
||||
});
|
||||
const { reconnectActions } = createChatReconnectControllerGroup(ports, {
|
||||
connection,
|
||||
});
|
||||
const { serverThreads, serverMetadata, serverDiagnostics } = createChatServerActionControllers(ports, {
|
||||
const threadControllers = createThreadControllerGroup(
|
||||
{
|
||||
...ports,
|
||||
client: actions.client,
|
||||
render: actions.render,
|
||||
status: actions.status,
|
||||
thread: actions.thread,
|
||||
scroll: actions.scroll,
|
||||
composer: actions.composer,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
},
|
||||
);
|
||||
const { history, threadActions, goals, threadIdentity } = threadControllers;
|
||||
refs.restoredThread = threadControllers.restoredThread;
|
||||
refs.threadResume = threadControllers.threadResume;
|
||||
refs.threadRename = threadControllers.threadRename;
|
||||
const threadRename = requireCompositionRef(refs.threadRename, "thread rename controller");
|
||||
const lifecycleActions = {
|
||||
...ports.lifecycle,
|
||||
invalidateResumeWork: threadControllers.invalidateResumeWork,
|
||||
};
|
||||
const { toolbarPanels, applyViewState } = createPanelUiControllerGroup(
|
||||
{
|
||||
...ports,
|
||||
lifecycle: lifecycleActions,
|
||||
render: actions.render,
|
||||
thread: actions.thread,
|
||||
},
|
||||
{
|
||||
threadActions,
|
||||
},
|
||||
);
|
||||
refs.threadSelection = createThreadSelectionControllerGroup(
|
||||
{
|
||||
...ports,
|
||||
thread: actions.thread,
|
||||
status: actions.status,
|
||||
},
|
||||
{
|
||||
toolbarPanels,
|
||||
},
|
||||
).threadSelection;
|
||||
const { reconnectActions } = createChatReconnectControllerGroup(
|
||||
{
|
||||
...ports,
|
||||
client: actions.client,
|
||||
lifecycle: lifecycleActions,
|
||||
render: actions.render,
|
||||
status: actions.status,
|
||||
thread: actions.thread,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
},
|
||||
);
|
||||
const serverActionControllers = createChatServerActionControllers(ports, {
|
||||
connection,
|
||||
goals,
|
||||
});
|
||||
const { serverThreads, serverMetadata, serverDiagnostics } = serverActionControllers;
|
||||
refs.serverMetadata = serverMetadata;
|
||||
refs.serverDiagnostics = serverDiagnostics;
|
||||
const serverRequestHost = {
|
||||
currentClient: ports.client.getClient,
|
||||
};
|
||||
const controller = createChatInboundController(ports, {
|
||||
serverMetadata,
|
||||
serverDiagnostics,
|
||||
threadRename,
|
||||
respondToServerRequest: (requestId, result) => respondToServerRequest(serverRequestHost, requestId, result),
|
||||
rejectServerRequest: (requestId, code, message) => rejectServerRequest(serverRequestHost, requestId, code, message),
|
||||
});
|
||||
const { connectionController } = createChatConnectionControllers(ports, {
|
||||
connection,
|
||||
serverMetadata,
|
||||
serverDiagnostics,
|
||||
});
|
||||
refs.controller = createChatInboundController(
|
||||
{
|
||||
...ports,
|
||||
render: actions.render,
|
||||
thread: actions.thread,
|
||||
},
|
||||
{
|
||||
serverMetadata,
|
||||
serverDiagnostics,
|
||||
threadRename,
|
||||
respondToServerRequest: (requestId, result) => respondToServerRequest(serverRequestHost, requestId, result),
|
||||
rejectServerRequest: (requestId, code, message) => rejectServerRequest(serverRequestHost, requestId, code, message),
|
||||
},
|
||||
);
|
||||
refs.connectionController = createChatConnectionControllers(
|
||||
{
|
||||
...ports,
|
||||
client: actions.client,
|
||||
lifecycle: lifecycleActions,
|
||||
thread: actions.thread,
|
||||
status: actions.status,
|
||||
liveState: ports.liveState,
|
||||
render: actions.render,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
serverMetadata,
|
||||
serverDiagnostics,
|
||||
},
|
||||
).connectionController;
|
||||
const inboundController = requireCompositionRef(refs.controller, "inbound controller");
|
||||
const connectionController = requireCompositionRef(refs.connectionController, "connection controller");
|
||||
|
||||
connection.setHandlers({
|
||||
onNotification: (notification) => {
|
||||
controller.handleNotification(notification);
|
||||
inboundController.handleNotification(notification);
|
||||
ports.liveState.refresh();
|
||||
ports.render.schedule();
|
||||
actions.render.schedule();
|
||||
},
|
||||
onServerRequest: (request) => {
|
||||
controller.handleServerRequest(request);
|
||||
inboundController.handleServerRequest(request);
|
||||
ports.liveState.refresh();
|
||||
ports.render.now();
|
||||
actions.render.now();
|
||||
},
|
||||
onLog: (message) => {
|
||||
controller.handleAppServerLog(message);
|
||||
ports.render.now();
|
||||
inboundController.handleAppServerLog(message);
|
||||
actions.render.now();
|
||||
},
|
||||
onExit: () => {
|
||||
connectionController.handleExit();
|
||||
},
|
||||
});
|
||||
|
||||
const { pendingRequests, messageRenderer, composerController, composerSubmission } = createConversationSurfaceControllerGroup(ports, {
|
||||
controller,
|
||||
serverThreads,
|
||||
runtimeSettings,
|
||||
threadActions,
|
||||
threadRename,
|
||||
reconnectActions,
|
||||
goals,
|
||||
history,
|
||||
});
|
||||
const { scheduleAppServerWarmup, openView, closeView } = createConnectionLifecycleControllerGroup(ports, {
|
||||
connection,
|
||||
composerController,
|
||||
messageRenderer,
|
||||
serverThreads,
|
||||
serverMetadata,
|
||||
});
|
||||
const conversationControllers = createConversationSurfaceControllerGroup(
|
||||
{
|
||||
...ports,
|
||||
client: actions.client,
|
||||
render: actions.render,
|
||||
runtime: actions.runtime,
|
||||
thread: actions.thread,
|
||||
status: actions.status,
|
||||
scroll: actions.scroll,
|
||||
},
|
||||
{
|
||||
controller: inboundController,
|
||||
serverThreads,
|
||||
runtimeSettings,
|
||||
threadActions,
|
||||
threadRename,
|
||||
reconnectActions,
|
||||
goals,
|
||||
history,
|
||||
},
|
||||
);
|
||||
const { pendingRequests, composerSubmission } = conversationControllers;
|
||||
refs.messageRenderer = conversationControllers.messageRenderer;
|
||||
refs.composerController = conversationControllers.composerController;
|
||||
const messageRenderer = requireCompositionRef(refs.messageRenderer, "message renderer");
|
||||
const composerController = requireCompositionRef(refs.composerController, "composer controller");
|
||||
const { scheduleAppServerWarmup, openView, closeView } = createConnectionLifecycleControllerGroup(
|
||||
{
|
||||
...ports,
|
||||
client: actions.client,
|
||||
lifecycle: lifecycleActions,
|
||||
render: actions.render,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
composerController,
|
||||
messageRenderer,
|
||||
serverThreads,
|
||||
serverMetadata,
|
||||
},
|
||||
);
|
||||
const threadResume = requireCompositionRef(refs.threadResume, "thread resume controller");
|
||||
const restoredThread = requireCompositionRef(refs.restoredThread, "restored thread controller");
|
||||
const threadSelection = requireCompositionRef(refs.threadSelection, "thread selection controller");
|
||||
|
||||
return {
|
||||
connection: {
|
||||
|
|
@ -167,7 +278,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
scheduleWarmup: scheduleAppServerWarmup,
|
||||
},
|
||||
inbound: {
|
||||
controller,
|
||||
controller: inboundController,
|
||||
},
|
||||
serverActions: {
|
||||
threads: serverThreads,
|
||||
|
|
@ -200,6 +311,9 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
render: {
|
||||
controller: renderController,
|
||||
messages: messageRenderer,
|
||||
attachSlotRenderers: (slotRenderers) => {
|
||||
renderController.setSlotRenderers(slotRenderers);
|
||||
},
|
||||
openView,
|
||||
closeView,
|
||||
applyViewState,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,7 @@ import type { ChatState, ChatStateStore } from "../chat-state";
|
|||
import type { CodexChatHost } from "../chat-host";
|
||||
import type { ChatMessageScrollIntentController } from "../panel/message-scroll-intent-controller";
|
||||
import type { DisplayDetailSection, DisplayItem } from "../display/types";
|
||||
import type {
|
||||
ChatConnectionWorkTracker,
|
||||
ChatResumeWorkTracker,
|
||||
ChatViewDeferredTasks,
|
||||
ChatViewRenderScheduleOptions,
|
||||
RestoredThreadState,
|
||||
} from "./lifecycle";
|
||||
import type { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks, ChatViewRenderScheduleOptions } from "./lifecycle";
|
||||
import type { ComposerMetaViewModel } from "./model";
|
||||
|
||||
export interface ChatControllerCompositionPorts {
|
||||
|
|
@ -28,7 +22,6 @@ export interface ChatControllerCompositionPorts {
|
|||
liveState: ChatPanelLiveStateContext;
|
||||
scroll: ChatPanelScrollContext;
|
||||
status: ChatPanelStatusContext;
|
||||
composer: ChatPanelComposerContext;
|
||||
}
|
||||
|
||||
interface ChatPanelObsidianContext {
|
||||
|
|
@ -52,7 +45,6 @@ interface ChatPanelClientContext {
|
|||
getClient: () => AppServerClient | null;
|
||||
setClient: (client: AppServerClient | null) => void;
|
||||
clear: () => void;
|
||||
ensureConnected: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface ChatPanelLifecycleContext {
|
||||
|
|
@ -65,7 +57,6 @@ interface ChatPanelLifecycleContext {
|
|||
getClosing: () => boolean;
|
||||
setClosing: (closing: boolean) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
scheduleDeferredDiagnostics: () => void;
|
||||
clearDeferredDiagnostics: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
|
|
@ -75,17 +66,11 @@ interface ChatPanelLifecycleContext {
|
|||
|
||||
interface ChatPanelRenderContext {
|
||||
panelRoot: () => HTMLElement | null;
|
||||
renderToolbar: (toolbar: HTMLElement) => void;
|
||||
renderGoal: (goal: HTMLElement) => void;
|
||||
renderMessages: (parent: HTMLElement) => void;
|
||||
renderComposer: (parent: HTMLElement) => void;
|
||||
pendingRequestsSignature: () => string;
|
||||
activeComposerThreadName: () => string | null;
|
||||
composerPlaceholder: () => string;
|
||||
composerMetaViewModel: () => ComposerMetaViewModel;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
now: () => void;
|
||||
shellSlots: () => void;
|
||||
schedule: (options?: ChatViewRenderScheduleOptions) => void;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +78,6 @@ interface ChatPanelRuntimeContext {
|
|||
runtimeSnapshot: () => RuntimeSnapshot;
|
||||
collaborationModeLabel: () => string;
|
||||
connectionDiagnosticDetails: () => DisplayDetailSection[];
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
statusSummaryLines: () => string[];
|
||||
|
|
@ -102,16 +86,8 @@ interface ChatPanelRuntimeContext {
|
|||
interface ChatThreadContext {
|
||||
ensureRestoredThreadLoaded: () => Promise<boolean>;
|
||||
startNewThread: () => Promise<void>;
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
publishAppServerMetadataSnapshot: () => void;
|
||||
loadSharedThreadList: () => Promise<void>;
|
||||
notifyIdentityChanged: () => void;
|
||||
resetTurnPresence: (hadTurns: boolean) => void;
|
||||
restorePlaceholder: (restoredThread: RestoredThreadState) => void;
|
||||
clearRestoredLifecycle: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -127,10 +103,4 @@ interface ChatPanelScrollContext {
|
|||
|
||||
interface ChatPanelStatusContext {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
|
||||
}
|
||||
|
||||
interface ChatPanelComposerContext {
|
||||
setText: (text: string) => void;
|
||||
}
|
||||
|
|
|
|||
143
src/features/chat/panel/controller-wiring.ts
Normal file
143
src/features/chat/panel/controller-wiring.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import type { ChatServerDiagnosticsActions } from "../server-actions/diagnostics-actions";
|
||||
import type { ChatServerMetadataActions } from "../server-actions/metadata-actions";
|
||||
import type { ChatComposerController } from "../composer/controller";
|
||||
import type { ChatInboundController } from "../inbound/controller";
|
||||
import type { RestoredThreadController } from "../threads/restored-thread-controller";
|
||||
import type { ChatConnectionController } from "../session/connection-controller";
|
||||
import type { ThreadRenameController } from "../threads/thread-rename-controller";
|
||||
import type { ThreadResumeController } from "../threads/thread-resume-controller";
|
||||
import type { ThreadSelectionActions } from "../threads/thread-selection-controller";
|
||||
import type { ChatMessageRenderer } from "../ui/message-stream";
|
||||
import type { ChatControllerCompositionPorts } from "./controller-ports";
|
||||
import type { ChatViewRenderController } from "./view-render-controller";
|
||||
|
||||
export interface ChatControllerCompositionRefs {
|
||||
renderController: ChatViewRenderController;
|
||||
controller: ChatInboundController | null;
|
||||
connectionController: ChatConnectionController | null;
|
||||
threadSelection: ThreadSelectionActions | null;
|
||||
threadRename: ThreadRenameController | null;
|
||||
threadResume: ThreadResumeController | null;
|
||||
restoredThread: RestoredThreadController | null;
|
||||
serverMetadata: ChatServerMetadataActions | null;
|
||||
serverDiagnostics: ChatServerDiagnosticsActions | null;
|
||||
messageRenderer: ChatMessageRenderer | null;
|
||||
composerController: ChatComposerController | null;
|
||||
}
|
||||
|
||||
export interface ChatControllerCompositionActions {
|
||||
client: ChatControllerCompositionPorts["client"] & {
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
render: ChatControllerCompositionPorts["render"] & {
|
||||
now: () => void;
|
||||
shellSlots: () => void;
|
||||
};
|
||||
status: ChatControllerCompositionPorts["status"] & {
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: Parameters<ChatInboundController["addStructuredSystemMessage"]>[1]) => void;
|
||||
};
|
||||
scroll: ChatControllerCompositionPorts["scroll"];
|
||||
thread: ChatControllerCompositionPorts["thread"] & {
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
publishAppServerMetadataSnapshot: () => void;
|
||||
resetTurnPresence: (hadTurns: boolean) => void;
|
||||
restorePlaceholder: (restoredThreadState: Parameters<RestoredThreadController["restore"]>[0]) => void;
|
||||
clearRestoredLifecycle: () => void;
|
||||
};
|
||||
runtime: ChatControllerCompositionPorts["runtime"] & {
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
};
|
||||
composer: {
|
||||
setText: (text: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatControllerCompositionActions(
|
||||
ports: ChatControllerCompositionPorts,
|
||||
refs: ChatControllerCompositionRefs,
|
||||
): ChatControllerCompositionActions {
|
||||
const render = {
|
||||
...ports.render,
|
||||
now: () => {
|
||||
refs.renderController.render();
|
||||
},
|
||||
shellSlots: () => {
|
||||
refs.renderController.renderShellSlots();
|
||||
},
|
||||
};
|
||||
const status = {
|
||||
...ports.status,
|
||||
addSystemMessage: (text: string) => {
|
||||
requireCompositionRef(refs.controller, "inbound controller").addSystemMessage(text);
|
||||
render.now();
|
||||
},
|
||||
addStructuredSystemMessage: (text: string, details: Parameters<ChatInboundController["addStructuredSystemMessage"]>[1]) => {
|
||||
requireCompositionRef(refs.controller, "inbound controller").addStructuredSystemMessage(text, details);
|
||||
render.now();
|
||||
},
|
||||
};
|
||||
|
||||
const threadNavigation = {
|
||||
selectThread: (threadId: string) => requireCompositionRef(refs.threadSelection, "thread selection controller").selectThread(threadId),
|
||||
resumeThread: (threadId: string) => requireCompositionRef(refs.threadResume, "thread resume controller").resumeThread(threadId),
|
||||
};
|
||||
const threadRefresh = {
|
||||
refreshThreads: () => requireCompositionRef(refs.connectionController, "connection controller").refreshThreads(),
|
||||
refreshSkills: (forceReload?: boolean) =>
|
||||
requireCompositionRef(refs.connectionController, "connection controller").refreshSkills(forceReload),
|
||||
publishAppServerMetadataSnapshot: () => {
|
||||
requireCompositionRef(refs.serverMetadata, "server metadata actions").publishAppServerMetadataSnapshot();
|
||||
},
|
||||
};
|
||||
const threadLifecycle = {
|
||||
resetTurnPresence: (hadTurns: boolean) => {
|
||||
requireCompositionRef(refs.threadRename, "thread rename controller").resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
restorePlaceholder: (restoredThreadState: Parameters<RestoredThreadController["restore"]>[0]) => {
|
||||
requireCompositionRef(refs.restoredThread, "restored thread controller").restore(restoredThreadState);
|
||||
},
|
||||
clearRestoredLifecycle: () => {
|
||||
requireCompositionRef(refs.restoredThread, "restored thread controller").clear();
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
client: {
|
||||
...ports.client,
|
||||
ensureConnected: () => requireCompositionRef(refs.connectionController, "connection controller").ensureConnected(),
|
||||
},
|
||||
render,
|
||||
status,
|
||||
scroll: {
|
||||
...ports.scroll,
|
||||
forceBottom: () => {
|
||||
ports.scroll.forceBottom();
|
||||
requireCompositionRef(refs.messageRenderer, "message renderer").forceMessagesToBottom();
|
||||
},
|
||||
},
|
||||
thread: {
|
||||
...ports.thread,
|
||||
...threadNavigation,
|
||||
...threadRefresh,
|
||||
...threadLifecycle,
|
||||
},
|
||||
runtime: {
|
||||
...ports.runtime,
|
||||
mcpStatusLines: () => requireCompositionRef(refs.serverDiagnostics, "server diagnostics actions").mcpStatusLines(),
|
||||
},
|
||||
composer: {
|
||||
setText: (text) => {
|
||||
requireCompositionRef(refs.composerController, "composer controller").setDraft(text, { focus: true, renderIfDetached: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function requireCompositionRef<T>(value: T | null, name: string): T {
|
||||
if (!value) throw new Error(`Chat controller composition did not initialize ${name}.`);
|
||||
return value;
|
||||
}
|
||||
|
|
@ -124,18 +124,14 @@ export class ChatConnectionWorkTracker {
|
|||
export class ChatResumeWorkTracker {
|
||||
private state: ChatResumeLifecycleState = { kind: "idle" };
|
||||
|
||||
constructor(private readonly onInvalidate: () => void) {}
|
||||
|
||||
begin(threadId: string): ActiveChatResume {
|
||||
const resume: ActiveChatResume = { kind: "resuming", threadId };
|
||||
this.state = transitionChatResumeLifecycle(this.state, { type: "started", resume });
|
||||
this.onInvalidate();
|
||||
return resume;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.state = transitionChatResumeLifecycle(this.state, { type: "invalidated" });
|
||||
this.onInvalidate();
|
||||
}
|
||||
|
||||
isStale(resume: ActiveChatResume): boolean {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { ConnectionManager } from "../../../app-server/connection-manager";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
import type { CodexChatHost } from "../chat-host";
|
||||
import type { ChatServerMetadataActions } from "../server-actions/metadata-actions";
|
||||
import type { ChatServerThreadActions } from "../server-actions/thread-actions";
|
||||
import type { ChatComposerController } from "../composer/controller";
|
||||
|
|
@ -9,11 +11,24 @@ import { ToolbarPanelController } from "./toolbar-controller";
|
|||
import { ChatViewRenderController } from "./view-render-controller";
|
||||
import { applyChatViewState } from "./view-state-controller";
|
||||
import type { ChatMessageRenderer } from "../ui/message-stream";
|
||||
import { applyCachedSharedAppServerState } from "./cached-app-server-state";
|
||||
import type { ChatControllerCompositionPorts } from "./controller-ports";
|
||||
import { applyCachedSharedAppServerState, type CachedSharedAppServerStateSource } from "./cached-app-server-state";
|
||||
import type { ChatViewDeferredTasks, ChatViewRenderScheduleOptions, RestoredThreadState } from "./lifecycle";
|
||||
import { createChatShellRenderPort } from "./shell-render";
|
||||
|
||||
type ViewRenderControllerGroupPorts = Pick<ChatControllerCompositionPorts, "lifecycle" | "plugin" | "render" | "state">;
|
||||
interface ViewRenderControllerGroupPorts {
|
||||
plugin: Pick<CodexChatHost, "settings">;
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
lifecycle: {
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
};
|
||||
render: {
|
||||
panelRoot: () => HTMLElement | null;
|
||||
pendingRequestsSignature: () => string;
|
||||
activeComposerThreadName: () => string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export function createViewRenderControllerGroup(
|
||||
context: ViewRenderControllerGroupPorts,
|
||||
|
|
@ -33,10 +48,6 @@ export function createViewRenderControllerGroup(
|
|||
activeComposerThreadName: render.activeComposerThreadName,
|
||||
}),
|
||||
panelRoot: render.panelRoot,
|
||||
renderToolbar: render.renderToolbar,
|
||||
renderGoal: render.renderGoal,
|
||||
renderMessages: render.renderMessages,
|
||||
renderComposer: render.renderComposer,
|
||||
clearScheduledRender: () => {
|
||||
deferredTasks.clearRender();
|
||||
},
|
||||
|
|
@ -44,10 +55,34 @@ export function createViewRenderControllerGroup(
|
|||
};
|
||||
}
|
||||
|
||||
type ConnectionLifecycleControllerGroupPorts = Pick<
|
||||
ChatControllerCompositionPorts,
|
||||
"client" | "lifecycle" | "liveState" | "obsidian" | "plugin" | "render"
|
||||
>;
|
||||
interface ConnectionLifecycleControllerGroupPorts {
|
||||
obsidian: Pick<ChatViewLifecycleHost, "handleActiveLeafChange" | "registerActiveLeafChange" | "registerEvent" | "registerPointerDown">;
|
||||
plugin: CachedSharedAppServerStateSource;
|
||||
client: {
|
||||
clear: () => void;
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
lifecycle: {
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
getOpened: () => boolean;
|
||||
setOpened: (opened: boolean) => void;
|
||||
getClosing: () => boolean;
|
||||
setClosing: (closing: boolean) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
};
|
||||
render: {
|
||||
panelRoot: () => HTMLElement | null;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
now: () => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
deferRefresh: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createConnectionLifecycleControllerGroup(
|
||||
context: ConnectionLifecycleControllerGroupPorts,
|
||||
|
|
@ -120,7 +155,23 @@ export function createConnectionLifecycleControllerGroup(
|
|||
};
|
||||
}
|
||||
|
||||
type PanelUiControllerGroupPorts = Pick<ChatControllerCompositionPorts, "lifecycle" | "render" | "state" | "thread">;
|
||||
interface PanelUiControllerGroupPorts {
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
lifecycle: {
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredRestoredThreadHydration: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
};
|
||||
render: {
|
||||
schedule: (options?: ChatViewRenderScheduleOptions) => void;
|
||||
};
|
||||
thread: {
|
||||
clearRestoredLifecycle: () => void;
|
||||
restorePlaceholder: (restoredThread: RestoredThreadState) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createPanelUiControllerGroup(
|
||||
context: PanelUiControllerGroupPorts,
|
||||
|
|
|
|||
|
|
@ -4,18 +4,26 @@ import type { ChatShellRenderPort } from "./shell-render";
|
|||
export interface ChatViewRenderControllerHost {
|
||||
shell: ChatShellRenderPort;
|
||||
panelRoot: () => HTMLElement | null;
|
||||
clearScheduledRender: () => void;
|
||||
}
|
||||
|
||||
export interface ChatViewSlotRenderers {
|
||||
renderToolbar: (toolbar: HTMLElement) => void;
|
||||
renderGoal: (goal: HTMLElement) => void;
|
||||
renderMessages: (parent: HTMLElement) => void;
|
||||
renderComposer: (parent: HTMLElement) => void;
|
||||
clearScheduledRender: () => void;
|
||||
}
|
||||
|
||||
export class ChatViewRenderController {
|
||||
private shellRenderVersion = 0;
|
||||
private slotRenderers: ChatViewSlotRenderers | null = null;
|
||||
|
||||
constructor(private readonly host: ChatViewRenderControllerHost) {}
|
||||
|
||||
setSlotRenderers(slotRenderers: ChatViewSlotRenderers): void {
|
||||
this.slotRenderers = slotRenderers;
|
||||
}
|
||||
|
||||
render(options: ChatViewRenderScheduleOptions = {}): void {
|
||||
this.host.clearScheduledRender();
|
||||
const root = this.host.panelRoot();
|
||||
|
|
@ -34,18 +42,18 @@ export class ChatViewRenderController {
|
|||
}
|
||||
|
||||
private readonly renderToolbarSlot = (toolbar: HTMLElement): void => {
|
||||
this.host.renderToolbar(toolbar);
|
||||
this.slotRenderers?.renderToolbar(toolbar);
|
||||
};
|
||||
|
||||
private readonly renderGoalSlot = (goal: HTMLElement): void => {
|
||||
this.host.renderGoal(goal);
|
||||
this.slotRenderers?.renderGoal(goal);
|
||||
};
|
||||
|
||||
private readonly renderMessagesSlot = (parent: HTMLElement): void => {
|
||||
this.host.renderMessages(parent);
|
||||
this.slotRenderers?.renderMessages(parent);
|
||||
};
|
||||
|
||||
private readonly renderComposerSlot = (parent: HTMLElement): void => {
|
||||
this.host.renderComposer(parent);
|
||||
this.slotRenderers?.renderComposer(parent);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { Notice } from "obsidian";
|
||||
|
||||
import type { ConnectionManager } from "../../../app-server/connection-manager";
|
||||
import type { RuntimeSnapshot } from "../runtime/effective-settings";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
import type { CodexChatHost } from "../chat-host";
|
||||
import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../server-actions/diagnostics-actions";
|
||||
import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../server-actions/metadata-actions";
|
||||
import { createChatServerThreadActions } from "../server-actions/thread-actions";
|
||||
|
|
@ -10,9 +13,17 @@ import type { rejectServerRequest, respondToServerRequest } from "../requests/se
|
|||
import type { ChatThreadGoalActions } from "../threads/thread-goal-actions";
|
||||
import type { ThreadRenameController } from "../threads/thread-rename-controller";
|
||||
import { ChatInboundController } from "../inbound/controller";
|
||||
import type { ChatControllerCompositionPorts } from "../panel/controller-ports";
|
||||
import type { ChatConnectionWorkTracker, ChatViewRenderScheduleOptions } from "../panel/lifecycle";
|
||||
|
||||
type ChatServerActionControllerPorts = Pick<ChatControllerCompositionPorts, "plugin" | "runtime" | "state">;
|
||||
interface ChatServerActionControllerPorts {
|
||||
plugin: Pick<CodexChatHost, "applyThreadListSnapshot" | "publishAppServerMetadata" | "vaultPath">;
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
runtime: {
|
||||
runtimeSnapshot: () => RuntimeSnapshot;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatServerActionControllers(
|
||||
context: ChatServerActionControllerPorts,
|
||||
|
|
@ -55,7 +66,20 @@ export function createChatServerActionControllers(
|
|||
return { serverThreads, serverMetadata, serverDiagnostics };
|
||||
}
|
||||
|
||||
type ChatInboundControllerPorts = Pick<ChatControllerCompositionPorts, "plugin" | "render" | "state" | "thread">;
|
||||
interface ChatInboundControllerPorts {
|
||||
plugin: Pick<CodexChatHost, "notifyThreadArchived" | "notifyThreadRenamed">;
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
render: {
|
||||
schedule: (options?: ChatViewRenderScheduleOptions) => void;
|
||||
};
|
||||
thread: {
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
publishAppServerMetadataSnapshot: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatInboundController(
|
||||
context: ChatInboundControllerPorts,
|
||||
|
|
@ -92,10 +116,37 @@ export function createChatInboundController(
|
|||
});
|
||||
}
|
||||
|
||||
type ChatConnectionControllerPorts = Pick<
|
||||
ChatControllerCompositionPorts,
|
||||
"client" | "lifecycle" | "liveState" | "plugin" | "render" | "state" | "status" | "thread"
|
||||
>;
|
||||
interface ChatConnectionControllerPorts {
|
||||
plugin: Pick<CodexChatHost, "settings">;
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
client: {
|
||||
setClient: (client: ReturnType<ConnectionManager["currentClient"]>) => void;
|
||||
};
|
||||
lifecycle: {
|
||||
connectionWork: ChatConnectionWorkTracker;
|
||||
invalidateResumeWork: () => void;
|
||||
scheduleDeferredDiagnostics: () => void;
|
||||
clearDeferredDiagnostics: () => void;
|
||||
};
|
||||
thread: {
|
||||
loadSharedThreadList: () => Promise<void>;
|
||||
refreshTabHeader: () => void;
|
||||
resetTurnPresence: (hadTurns: boolean) => void;
|
||||
};
|
||||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
};
|
||||
render: {
|
||||
now: () => void;
|
||||
schedule: (options?: ChatViewRenderScheduleOptions) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatConnectionControllers(
|
||||
context: ChatConnectionControllerPorts,
|
||||
|
|
@ -140,10 +191,30 @@ export function createChatConnectionControllers(
|
|||
};
|
||||
}
|
||||
|
||||
type ChatReconnectControllerGroupPorts = Pick<
|
||||
ChatControllerCompositionPorts,
|
||||
"client" | "lifecycle" | "render" | "state" | "status" | "thread"
|
||||
>;
|
||||
interface ChatReconnectControllerGroupPorts {
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
client: {
|
||||
clear: () => void;
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
lifecycle: {
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredDiagnostics: () => void;
|
||||
};
|
||||
render: {
|
||||
now: () => void;
|
||||
};
|
||||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
thread: {
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatReconnectControllerGroup(
|
||||
context: ChatReconnectControllerGroupPorts,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { ConnectionManager } from "../../../app-server/connection-manager";
|
||||
import type { AppServerClient } from "../../../app-server/client";
|
||||
import { recoverRolloutTokenUsage } from "../../../app-server/rollout-token-usage";
|
||||
import type { ArchiveExportAdapter } from "../../../domain/threads/export";
|
||||
import { createChatThreadActions } from "./thread-actions";
|
||||
import { createChatThreadGoalActions } from "./thread-goal-actions";
|
||||
import { ThreadHistoryController } from "./thread-history-controller";
|
||||
|
|
@ -9,12 +11,66 @@ 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 { ChatControllerCompositionPorts } from "../panel/controller-ports";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
import type { CodexChatHost } from "../chat-host";
|
||||
import type { DisplayItem } from "../display/types";
|
||||
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../panel/lifecycle";
|
||||
|
||||
type ThreadControllerGroupPorts = Pick<
|
||||
ChatControllerCompositionPorts,
|
||||
"client" | "composer" | "lifecycle" | "liveState" | "obsidian" | "plugin" | "render" | "scroll" | "state" | "status" | "thread"
|
||||
>;
|
||||
interface ThreadControllerGroupPorts {
|
||||
obsidian: {
|
||||
archiveAdapter: () => ArchiveExportAdapter;
|
||||
};
|
||||
plugin: Pick<
|
||||
CodexChatHost,
|
||||
| "notifyThreadArchived"
|
||||
| "notifyThreadRenamed"
|
||||
| "openThreadInNewView"
|
||||
| "refreshSharedThreadListFromOpenSurface"
|
||||
| "settings"
|
||||
| "vaultPath"
|
||||
>;
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
systemItem: (text: string) => DisplayItem;
|
||||
};
|
||||
client: {
|
||||
getClient: () => AppServerClient | null;
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
lifecycle: {
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
resumeWork: ChatResumeWorkTracker;
|
||||
getOpened: () => boolean;
|
||||
getClosing: () => boolean;
|
||||
clearDeferredRestoredThreadHydration: () => void;
|
||||
};
|
||||
thread: {
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
notifyIdentityChanged: () => void;
|
||||
resetTurnPresence: (hadTurns: boolean) => void;
|
||||
refreshTabHeader: () => void;
|
||||
};
|
||||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
};
|
||||
scroll: {
|
||||
preservePosition: () => void;
|
||||
forceBottom: () => void;
|
||||
};
|
||||
render: {
|
||||
now: () => void;
|
||||
shellSlots: () => void;
|
||||
};
|
||||
composer: {
|
||||
setText: (text: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createThreadControllerGroup(
|
||||
context: ThreadControllerGroupPorts,
|
||||
|
|
@ -35,6 +91,10 @@ export function createThreadControllerGroup(
|
|||
keepCurrentScrollPosition: scroll.preservePosition,
|
||||
setThreadTurnPresence: thread.resetTurnPresence,
|
||||
});
|
||||
const invalidateResumeWork = () => {
|
||||
resumeWork.invalidate();
|
||||
history.invalidate();
|
||||
};
|
||||
const threadActions = createChatThreadActions({
|
||||
stateStore,
|
||||
vaultPath: plugin.vaultPath,
|
||||
|
|
@ -73,7 +133,7 @@ export function createThreadControllerGroup(
|
|||
deferredTasks,
|
||||
opened: lifecycle.getOpened,
|
||||
resumeThread: thread.resumeThread,
|
||||
invalidateResumeWork: lifecycle.invalidateResumeWork,
|
||||
invalidateResumeWork,
|
||||
stateStore,
|
||||
systemItem: state.systemItem,
|
||||
setStatus: status.set,
|
||||
|
|
@ -106,7 +166,7 @@ export function createThreadControllerGroup(
|
|||
const threadIdentity = createThreadIdentityActions({
|
||||
stateStore,
|
||||
restoredThread,
|
||||
invalidateResumeWork: lifecycle.invalidateResumeWork,
|
||||
invalidateResumeWork,
|
||||
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
|
||||
resetThreadTurnPresence: thread.resetTurnPresence,
|
||||
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
|
||||
|
|
@ -134,10 +194,22 @@ export function createThreadControllerGroup(
|
|||
threadResume,
|
||||
threadIdentity,
|
||||
threadRename,
|
||||
invalidateResumeWork,
|
||||
};
|
||||
}
|
||||
|
||||
type ThreadSelectionControllerGroupPorts = Pick<ChatControllerCompositionPorts, "plugin" | "state" | "status" | "thread">;
|
||||
interface ThreadSelectionControllerGroupPorts {
|
||||
plugin: Pick<CodexChatHost, "focusThreadInOpenView">;
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
status: {
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
thread: {
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
export function createThreadSelectionControllerGroup(
|
||||
context: ThreadSelectionControllerGroupPorts,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export class ThreadResumeController {
|
|||
return;
|
||||
}
|
||||
const resume = this.host.resumeWork.begin(threadId);
|
||||
this.host.history.invalidate();
|
||||
await this.host.ensureConnected();
|
||||
const client = this.host.currentClient();
|
||||
if (!client || this.isStale(resume)) return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import type { App, Component } from "obsidian";
|
||||
|
||||
import type { AppServerClient } from "../../../app-server/client";
|
||||
import type { ChatServerThreadActions } from "../server-actions/thread-actions";
|
||||
import { ChatComposerController } from "../composer/controller";
|
||||
import { activeTurnId } from "../chat-state";
|
||||
import { activeTurnId, type ChatState, type ChatStateStore } from "../chat-state";
|
||||
import type { ChatReconnectActions } from "../session/reconnect-actions";
|
||||
import { PendingRequestController } from "../requests/pending-request-controller";
|
||||
import type { ChatRuntimeSettingsActions } from "../runtime/runtime-settings-actions";
|
||||
|
|
@ -13,14 +16,66 @@ import type { ChatThreadGoalActions } from "../threads/thread-goal-actions";
|
|||
import type { ThreadHistoryController } from "../threads/thread-history-controller";
|
||||
import type { ThreadRenameController } from "../threads/thread-rename-controller";
|
||||
import type { ChatInboundController } from "../inbound/controller";
|
||||
import { currentModel } from "../runtime/effective-settings";
|
||||
import { currentModel, type RuntimeSnapshot } from "../runtime/effective-settings";
|
||||
import { ChatMessageRenderer } from "../ui/message-stream";
|
||||
import type { ChatControllerCompositionPorts } from "../panel/controller-ports";
|
||||
import type { CodexChatHost } from "../chat-host";
|
||||
import type { DisplayDetailSection } from "../display/types";
|
||||
import type { ChatMessageScrollIntentController } from "../panel/message-scroll-intent-controller";
|
||||
import type { ComposerMetaViewModel } from "../panel/model";
|
||||
import type { ChatViewRenderScheduleOptions } from "../panel/lifecycle";
|
||||
|
||||
type ConversationSurfaceControllerGroupPorts = Pick<
|
||||
ChatControllerCompositionPorts,
|
||||
"client" | "lifecycle" | "liveState" | "obsidian" | "plugin" | "render" | "runtime" | "scroll" | "state" | "status" | "thread"
|
||||
>;
|
||||
interface ConversationSurfaceControllerGroupPorts {
|
||||
obsidian: {
|
||||
app: App;
|
||||
owner: Component;
|
||||
viewId: string;
|
||||
};
|
||||
plugin: Pick<CodexChatHost, "openTurnDiff" | "settings" | "vaultPath">;
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
getState: () => ChatState;
|
||||
};
|
||||
client: {
|
||||
getClient: () => AppServerClient | null;
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
lifecycle: {
|
||||
messageScrollIntent: ChatMessageScrollIntentController;
|
||||
};
|
||||
render: {
|
||||
now: () => void;
|
||||
schedule: (options?: ChatViewRenderScheduleOptions) => void;
|
||||
pendingRequestsSignature: () => string;
|
||||
composerPlaceholder: () => string;
|
||||
composerMetaViewModel: () => ComposerMetaViewModel;
|
||||
};
|
||||
runtime: {
|
||||
runtimeSnapshot: () => RuntimeSnapshot;
|
||||
statusSummaryLines: () => string[];
|
||||
connectionDiagnosticDetails: () => DisplayDetailSection[];
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
};
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: () => Promise<boolean>;
|
||||
startNewThread: () => Promise<void>;
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
notifyIdentityChanged: () => void;
|
||||
resetTurnPresence: (hadTurns: boolean) => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
};
|
||||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
|
||||
};
|
||||
scroll: {
|
||||
forceBottom: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createConversationSurfaceControllerGroup(
|
||||
context: ConversationSurfaceControllerGroupPorts,
|
||||
|
|
|
|||
|
|
@ -57,17 +57,27 @@ export class CodexChatView extends ItemView {
|
|||
) {
|
||||
super(leaf);
|
||||
this.deferredTasks = new ChatViewDeferredTasks(() => this.containerEl.win);
|
||||
this.resumeWork = new ChatResumeWorkTracker(() => {
|
||||
this.controllers.thread.history.invalidate();
|
||||
});
|
||||
this.resumeWork = new ChatResumeWorkTracker();
|
||||
this.messageScrollIntent = new ChatMessageScrollIntentController();
|
||||
this.controllers = createChatViewControllers(this.createControllerPorts());
|
||||
this.slotPorts = this.createSlotRendererPorts();
|
||||
this.slotPorts = this.createSlotRendererPorts(this.controllers);
|
||||
this.controllers.render.attachSlotRenderers({
|
||||
renderToolbar: (toolbar) => {
|
||||
renderToolbarSlot(toolbar, this.slotPorts);
|
||||
},
|
||||
renderGoal: (goal) => {
|
||||
renderGoalSlot(goal, this.slotPorts);
|
||||
},
|
||||
renderMessages: (parent) => {
|
||||
renderMessagesSlot(parent, this.slotPorts);
|
||||
},
|
||||
renderComposer: (parent) => {
|
||||
renderComposerSlot(parent, this.slotPorts);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
obsidian: {
|
||||
app: this.app,
|
||||
|
|
@ -101,7 +111,6 @@ export class CodexChatView extends ItemView {
|
|||
clear: () => {
|
||||
this.client = null;
|
||||
},
|
||||
ensureConnected: () => this.controllers.connection.controller.ensureConnected(),
|
||||
},
|
||||
lifecycle: {
|
||||
deferredTasks: this.deferredTasks,
|
||||
|
|
@ -117,10 +126,7 @@ export class CodexChatView extends ItemView {
|
|||
this.closing = closing;
|
||||
},
|
||||
invalidateConnectionWork: () => {
|
||||
this.controllers.connection.controller.invalidate();
|
||||
},
|
||||
invalidateResumeWork: () => {
|
||||
this.invalidateResumeWork();
|
||||
this.connectionWork.invalidate();
|
||||
},
|
||||
scheduleDeferredDiagnostics: () => {
|
||||
this.scheduleDeferredDiagnostics();
|
||||
|
|
@ -140,18 +146,6 @@ export class CodexChatView extends ItemView {
|
|||
},
|
||||
render: {
|
||||
panelRoot: () => this.panelRoot(),
|
||||
renderToolbar: (toolbar) => {
|
||||
renderToolbarSlot(toolbar, this.slotPorts);
|
||||
},
|
||||
renderGoal: (goal) => {
|
||||
renderGoalSlot(goal, this.slotPorts);
|
||||
},
|
||||
renderMessages: (parent) => {
|
||||
renderMessagesSlot(parent, this.slotPorts);
|
||||
},
|
||||
renderComposer: (parent) => {
|
||||
renderComposerSlot(parent, this.slotPorts);
|
||||
},
|
||||
pendingRequestsSignature: () => pendingRequestsSignature(this.slotPorts),
|
||||
activeComposerThreadName: () => activeComposerThreadName(this.slotPorts),
|
||||
composerPlaceholder: () => composerPlaceholder(this.slotPorts),
|
||||
|
|
@ -159,12 +153,6 @@ export class CodexChatView extends ItemView {
|
|||
closeToolbarPanelOnOutsidePointer: (event) => {
|
||||
this.closeToolbarPanelOnOutsidePointer(event);
|
||||
},
|
||||
now: () => {
|
||||
this.controllers.render.controller.render();
|
||||
},
|
||||
shellSlots: () => {
|
||||
this.controllers.render.controller.renderShellSlots();
|
||||
},
|
||||
schedule: (options) => {
|
||||
this.scheduleRender(options);
|
||||
},
|
||||
|
|
@ -173,7 +161,6 @@ export class CodexChatView extends ItemView {
|
|||
runtimeSnapshot: () => this.runtimeSnapshot(),
|
||||
collaborationModeLabel: () => this.collaborationModeLabel(),
|
||||
connectionDiagnosticDetails: () => this.connectionDiagnosticDetails(),
|
||||
mcpStatusLines: () => this.controllers.serverActions.diagnostics.mcpStatusLines(),
|
||||
modelStatusLines: () => this.modelStatusLines(),
|
||||
effortStatusLines: () => this.effortStatusLines(),
|
||||
statusSummaryLines: () => this.statusSummaryLines(),
|
||||
|
|
@ -181,26 +168,10 @@ export class CodexChatView extends ItemView {
|
|||
thread: {
|
||||
ensureRestoredThreadLoaded: () => this.ensureRestoredThreadLoaded(),
|
||||
startNewThread: () => this.startNewThread(),
|
||||
selectThread: (threadId) => this.controllers.thread.selection.selectThread(threadId),
|
||||
resumeThread: (threadId) => this.controllers.thread.resume.resumeThread(threadId),
|
||||
refreshThreads: () => this.controllers.connection.controller.refreshThreads(),
|
||||
refreshSkills: (forceReload) => this.controllers.connection.controller.refreshSkills(forceReload),
|
||||
publishAppServerMetadataSnapshot: () => {
|
||||
this.controllers.serverActions.metadata.publishAppServerMetadataSnapshot();
|
||||
},
|
||||
loadSharedThreadList: () => this.loadSharedThreadList(),
|
||||
notifyIdentityChanged: () => {
|
||||
this.notifyActiveThreadIdentityChanged();
|
||||
},
|
||||
resetTurnPresence: (hadTurns) => {
|
||||
this.controllers.thread.rename.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
restorePlaceholder: (restoredThread) => {
|
||||
this.controllers.thread.restored.restore(restoredThread);
|
||||
},
|
||||
clearRestoredLifecycle: () => {
|
||||
this.controllers.thread.restored.clear();
|
||||
},
|
||||
refreshTabHeader: () => {
|
||||
this.refreshTabHeader();
|
||||
},
|
||||
|
|
@ -218,7 +189,6 @@ export class CodexChatView extends ItemView {
|
|||
scroll: {
|
||||
forceBottom: () => {
|
||||
this.messageScrollIntent.forceBottom();
|
||||
this.controllers.render.messages.forceMessagesToBottom();
|
||||
},
|
||||
preservePosition: () => {
|
||||
this.messageScrollIntent.preservePosition();
|
||||
|
|
@ -228,28 +198,15 @@ export class CodexChatView extends ItemView {
|
|||
set: (status) => {
|
||||
this.dispatch({ type: "connection/status-set", status });
|
||||
},
|
||||
addSystemMessage: (text) => {
|
||||
this.controllers.inbound.controller.addSystemMessage(text);
|
||||
this.controllers.render.controller.render();
|
||||
},
|
||||
addStructuredSystemMessage: (text, details) => {
|
||||
this.controllers.inbound.controller.addStructuredSystemMessage(text, details);
|
||||
this.controllers.render.controller.render();
|
||||
},
|
||||
},
|
||||
composer: {
|
||||
setText: (text) => {
|
||||
this.controllers.composer.controller.setDraft(text, { focus: true, renderIfDetached: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createSlotRendererPorts(): ChatViewSlotRendererPorts {
|
||||
private createSlotRendererPorts(controllers: ChatViewControllers): ChatViewSlotRendererPorts {
|
||||
return {
|
||||
state: {
|
||||
chat: () => this.state,
|
||||
connected: () => this.controllers.connection.manager.isConnected(),
|
||||
connected: () => controllers.connection.manager.isConnected(),
|
||||
turnBusy: () => this.turnBusy,
|
||||
},
|
||||
settings: {
|
||||
|
|
@ -268,45 +225,45 @@ export class CodexChatView extends ItemView {
|
|||
},
|
||||
actions: {
|
||||
toolbar: {
|
||||
archiveConfirmId: () => this.controllers.toolbar.panels.archiveConfirmId(),
|
||||
renameState: (threadId) => this.controllers.thread.rename.editState(threadId),
|
||||
archiveConfirmId: () => controllers.toolbar.panels.archiveConfirmId(),
|
||||
renameState: (threadId) => controllers.thread.rename.editState(threadId),
|
||||
startNewThread: () => this.startNewThread(),
|
||||
toggleChatActions: () => {
|
||||
this.controllers.toolbar.panels.toggleChatActions();
|
||||
controllers.toolbar.panels.toggleChatActions();
|
||||
},
|
||||
compactConversation: () => this.compactConversation(),
|
||||
showGoalEditor: () => {
|
||||
this.setGoalEditingOpen(true, { closeToolbarPanel: true });
|
||||
},
|
||||
toggleHistory: () => {
|
||||
this.controllers.toolbar.panels.toggleHistory();
|
||||
controllers.toolbar.panels.toggleHistory();
|
||||
},
|
||||
toggleStatusPanel: () => {
|
||||
this.controllers.toolbar.panels.toggleStatus();
|
||||
controllers.toolbar.panels.toggleStatus();
|
||||
},
|
||||
reconnectPanel: () => this.controllers.connection.reconnect.reconnectPanel(),
|
||||
refreshStatusPanel: () => this.controllers.connection.controller.refreshStatusPanel(),
|
||||
selectThreadFromToolbar: (threadId) => this.controllers.thread.selection.selectThreadFromToolbar(threadId),
|
||||
reconnectPanel: () => controllers.connection.reconnect.reconnectPanel(),
|
||||
refreshStatusPanel: () => controllers.connection.controller.refreshStatusPanel(),
|
||||
selectThreadFromToolbar: (threadId) => controllers.thread.selection.selectThreadFromToolbar(threadId),
|
||||
startArchive: (threadId) => {
|
||||
this.controllers.toolbar.panels.startArchive(threadId);
|
||||
controllers.toolbar.panels.startArchive(threadId);
|
||||
},
|
||||
archiveThread: (threadId, saveMarkdown) => this.controllers.toolbar.panels.archiveThread(threadId, saveMarkdown),
|
||||
archiveThread: (threadId, saveMarkdown) => controllers.toolbar.panels.archiveThread(threadId, saveMarkdown),
|
||||
startRename: (threadId) => {
|
||||
this.controllers.thread.rename.start(threadId);
|
||||
controllers.thread.rename.start(threadId);
|
||||
},
|
||||
updateRenameDraft: (threadId, value) => {
|
||||
this.controllers.thread.rename.updateDraft(threadId, value);
|
||||
controllers.thread.rename.updateDraft(threadId, value);
|
||||
},
|
||||
saveRename: (threadId, value) => this.controllers.thread.rename.save(threadId, value),
|
||||
saveRename: (threadId, value) => controllers.thread.rename.save(threadId, value),
|
||||
cancelRename: (threadId) => {
|
||||
this.controllers.thread.rename.cancel(threadId);
|
||||
controllers.thread.rename.cancel(threadId);
|
||||
},
|
||||
autoNameDraft: (threadId) => this.controllers.thread.rename.autoNameDraft(threadId),
|
||||
autoNameDraft: (threadId) => controllers.thread.rename.autoNameDraft(threadId),
|
||||
},
|
||||
goal: {
|
||||
saveObjective: (objective, tokenBudget) => this.saveGoalObjective(objective, tokenBudget),
|
||||
setStatus: (threadId, status) => this.controllers.runtime.goals.setStatus(threadId, status),
|
||||
clear: (threadId) => this.controllers.runtime.goals.clear(threadId),
|
||||
setStatus: (threadId, status) => controllers.runtime.goals.setStatus(threadId, status),
|
||||
clear: (threadId) => controllers.runtime.goals.clear(threadId),
|
||||
setEditingOpen: (open) => {
|
||||
this.setGoalEditingOpen(open);
|
||||
},
|
||||
|
|
@ -314,10 +271,10 @@ export class CodexChatView extends ItemView {
|
|||
},
|
||||
slots: {
|
||||
renderMessages: (parent) => {
|
||||
this.controllers.render.messages.render(parent);
|
||||
controllers.render.messages.render(parent);
|
||||
},
|
||||
renderComposer: (parent) => {
|
||||
this.controllers.composer.controller.render(parent);
|
||||
controllers.composer.controller.render(parent);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -523,10 +480,6 @@ export class CodexChatView extends ItemView {
|
|||
await this.controllers.runtime.settings.setRequestedReasoningEffortFromUi(effort);
|
||||
}
|
||||
|
||||
private invalidateResumeWork(): void {
|
||||
this.resumeWork.invalidate();
|
||||
}
|
||||
|
||||
private async ensureRestoredThreadLoaded(): Promise<boolean> {
|
||||
return this.controllers.thread.restored.ensureLoaded();
|
||||
}
|
||||
|
|
|
|||
163
tests/features/chat/panel/composition.test.ts
Normal file
163
tests/features/chat/panel/composition.test.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { App, Component, EventRef, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
import type { RuntimeSnapshot } from "../../../../src/features/chat/runtime/effective-settings";
|
||||
import { createChatStateStore } from "../../../../src/features/chat/chat-state";
|
||||
import { createChatViewControllers } from "../../../../src/features/chat/panel/composition";
|
||||
import type { ChatControllerCompositionPorts } from "../../../../src/features/chat/panel/controller-ports";
|
||||
import { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "../../../../src/features/chat/panel/lifecycle";
|
||||
import { ChatMessageScrollIntentController } from "../../../../src/features/chat/panel/message-scroll-intent-controller";
|
||||
import type { ComposerMetaViewModel } from "../../../../src/features/chat/panel/model";
|
||||
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
|
||||
|
||||
describe("createChatViewControllers", () => {
|
||||
it("constructs the chat controller graph and exposes slot renderer attachment", () => {
|
||||
const controllers = createChatViewControllers(createPorts());
|
||||
|
||||
expect(controllers.connection.controller).toBeTruthy();
|
||||
expect(controllers.inbound.controller).toBeTruthy();
|
||||
expect(controllers.thread.resume).toBeTruthy();
|
||||
expect(controllers.render.messages).toBeTruthy();
|
||||
expect(controllers.composer.controller).toBeTruthy();
|
||||
expect(() => {
|
||||
controllers.render.attachSlotRenderers({
|
||||
renderToolbar: vi.fn(),
|
||||
renderGoal: vi.fn(),
|
||||
renderMessages: vi.fn(),
|
||||
renderComposer: vi.fn(),
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
function createPorts(): ChatControllerCompositionPorts {
|
||||
const stateStore = createChatStateStore();
|
||||
const deferredTasks = new ChatViewDeferredTasks(() => window);
|
||||
const root = document.createElement("div");
|
||||
|
||||
return {
|
||||
obsidian: {
|
||||
app: createApp(),
|
||||
owner: createOwner(),
|
||||
viewId: "test-view",
|
||||
registerEvent: vi.fn(),
|
||||
registerPointerDown: vi.fn(),
|
||||
registerActiveLeafChange: vi.fn(),
|
||||
handleActiveLeafChange: vi.fn((_leaf: WorkspaceLeaf | null) => undefined),
|
||||
archiveAdapter: () => ({
|
||||
exists: vi.fn().mockResolvedValue(false),
|
||||
mkdir: vi.fn().mockResolvedValue(undefined),
|
||||
write: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
},
|
||||
plugin: {
|
||||
settings: DEFAULT_SETTINGS,
|
||||
vaultPath: "/vault",
|
||||
openThreadInNewView: vi.fn().mockResolvedValue(undefined),
|
||||
focusThreadInOpenView: vi.fn().mockResolvedValue(false),
|
||||
openTurnDiff: vi.fn().mockResolvedValue(undefined),
|
||||
notifyThreadArchived: vi.fn(),
|
||||
notifyThreadRenamed: vi.fn(),
|
||||
refreshThreadsViewLiveState: vi.fn(),
|
||||
refreshSharedThreadListFromOpenSurface: vi.fn(),
|
||||
applyThreadListSnapshot: vi.fn(),
|
||||
refreshThreadList: vi.fn(async (fetchThreads: () => Promise<readonly []>) => fetchThreads()),
|
||||
cachedThreadList: () => null,
|
||||
publishAppServerMetadata: vi.fn(),
|
||||
cachedAppServerMetadata: () => null,
|
||||
},
|
||||
state: {
|
||||
stateStore,
|
||||
getState: () => stateStore.getState(),
|
||||
systemItem: (text) => ({ id: "system", kind: "system", role: "system", text }),
|
||||
},
|
||||
client: {
|
||||
getClient: () => null,
|
||||
setClient: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
},
|
||||
lifecycle: {
|
||||
deferredTasks,
|
||||
resumeWork: new ChatResumeWorkTracker(),
|
||||
connectionWork: new ChatConnectionWorkTracker(),
|
||||
messageScrollIntent: new ChatMessageScrollIntentController(),
|
||||
getOpened: () => false,
|
||||
setOpened: vi.fn(),
|
||||
getClosing: () => false,
|
||||
setClosing: vi.fn(),
|
||||
invalidateConnectionWork: vi.fn(),
|
||||
scheduleDeferredDiagnostics: vi.fn(),
|
||||
clearDeferredDiagnostics: vi.fn(),
|
||||
scheduleDeferredRestoredThreadHydration: vi.fn(),
|
||||
clearDeferredRestoredThreadHydration: vi.fn(),
|
||||
scheduleDeferredAppServerWarmup: vi.fn(),
|
||||
},
|
||||
render: {
|
||||
panelRoot: () => root,
|
||||
pendingRequestsSignature: () => "",
|
||||
activeComposerThreadName: () => null,
|
||||
composerPlaceholder: () => "",
|
||||
composerMetaViewModel: () => composerMeta(),
|
||||
closeToolbarPanelOnOutsidePointer: vi.fn(),
|
||||
schedule: vi.fn(),
|
||||
},
|
||||
runtime: {
|
||||
runtimeSnapshot: () => ({}) as RuntimeSnapshot,
|
||||
collaborationModeLabel: () => "Plan",
|
||||
connectionDiagnosticDetails: () => [],
|
||||
modelStatusLines: () => [],
|
||||
effortStatusLines: () => [],
|
||||
statusSummaryLines: () => [],
|
||||
},
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: vi.fn().mockResolvedValue(false),
|
||||
startNewThread: vi.fn().mockResolvedValue(undefined),
|
||||
loadSharedThreadList: vi.fn().mockResolvedValue(undefined),
|
||||
notifyIdentityChanged: vi.fn(),
|
||||
refreshTabHeader: vi.fn(),
|
||||
},
|
||||
liveState: {
|
||||
refresh: vi.fn(),
|
||||
deferRefresh: vi.fn(),
|
||||
},
|
||||
scroll: {
|
||||
forceBottom: vi.fn(),
|
||||
preservePosition: vi.fn(),
|
||||
},
|
||||
status: {
|
||||
set: vi.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createApp(): App {
|
||||
return {
|
||||
vault: {
|
||||
adapter: {},
|
||||
on: vi.fn(() => ({}) as EventRef),
|
||||
},
|
||||
workspace: {
|
||||
getActiveFile: () => null,
|
||||
openLinkText: vi.fn(),
|
||||
},
|
||||
} as unknown as App;
|
||||
}
|
||||
|
||||
function createOwner(): Component {
|
||||
return {} as Component;
|
||||
}
|
||||
|
||||
function composerMeta(): ComposerMetaViewModel {
|
||||
return {
|
||||
fatal: null,
|
||||
context: { cells: [], percent: "--%" },
|
||||
statusSummary: "",
|
||||
model: "",
|
||||
effort: null,
|
||||
planActive: false,
|
||||
autoReviewActive: false,
|
||||
fastActive: false,
|
||||
};
|
||||
}
|
||||
|
|
@ -12,14 +12,21 @@ describe("ChatViewRenderController", () => {
|
|||
const messages = document.createElement("div");
|
||||
const composer = document.createElement("div");
|
||||
const host = renderHost(root, { toolbar, goal, messages, composer });
|
||||
const slotRenderers = {
|
||||
renderToolbar: vi.fn(),
|
||||
renderGoal: vi.fn(),
|
||||
renderMessages: vi.fn(),
|
||||
renderComposer: vi.fn(),
|
||||
};
|
||||
const controller = new ChatViewRenderController(host);
|
||||
controller.setSlotRenderers(slotRenderers);
|
||||
|
||||
controller.render();
|
||||
|
||||
expect(host.renderToolbar).toHaveBeenCalledWith(toolbar);
|
||||
expect(host.renderGoal).toHaveBeenCalledWith(goal);
|
||||
expect(host.renderMessages).toHaveBeenCalledWith(messages);
|
||||
expect(host.renderComposer).toHaveBeenCalledWith(composer);
|
||||
expect(slotRenderers.renderToolbar).toHaveBeenCalledWith(toolbar);
|
||||
expect(slotRenderers.renderGoal).toHaveBeenCalledWith(goal);
|
||||
expect(slotRenderers.renderMessages).toHaveBeenCalledWith(messages);
|
||||
expect(slotRenderers.renderComposer).toHaveBeenCalledWith(composer);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -42,10 +49,6 @@ function renderHost(
|
|||
},
|
||||
},
|
||||
panelRoot: () => root,
|
||||
renderToolbar: vi.fn(),
|
||||
renderGoal: vi.fn(),
|
||||
renderMessages: vi.fn(),
|
||||
renderComposer: vi.fn(),
|
||||
clearScheduledRender: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,12 +65,13 @@ function createController(
|
|||
const client = { resumeThread } as unknown as AppServerClient;
|
||||
const loadLatest = vi.fn().mockResolvedValue(undefined);
|
||||
const applyLatestPage = vi.fn();
|
||||
const invalidateHistory = vi.fn();
|
||||
const restoredClear = vi.fn();
|
||||
const host = {
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
resumeWork: new ChatResumeWorkTracker(() => undefined),
|
||||
history: { loadLatest, applyLatestPage } as unknown as ThreadHistoryController,
|
||||
resumeWork: new ChatResumeWorkTracker(),
|
||||
history: { loadLatest, applyLatestPage, invalidate: invalidateHistory } as unknown as ThreadHistoryController,
|
||||
restoredThread: { clear: restoredClear } as unknown as RestoredThreadController,
|
||||
currentClient: () => client,
|
||||
ensureConnected: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -86,7 +87,16 @@ function createController(
|
|||
syncThreadGoal: vi.fn().mockResolvedValue(undefined),
|
||||
...overrides,
|
||||
};
|
||||
return { controller: new ThreadResumeController(host), host, applyLatestPage, loadLatest, restoredClear, resumeThread, stateStore };
|
||||
return {
|
||||
controller: new ThreadResumeController(host),
|
||||
host,
|
||||
applyLatestPage,
|
||||
invalidateHistory,
|
||||
loadLatest,
|
||||
restoredClear,
|
||||
resumeThread,
|
||||
stateStore,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ThreadResumeController", () => {
|
||||
|
|
|
|||
|
|
@ -70,15 +70,12 @@ describe("chat view lifecycle transitions", () => {
|
|||
expect(tracker.active()).toBeNull();
|
||||
});
|
||||
|
||||
it("tracks resume work and calls the invalidation hook", () => {
|
||||
const invalidate = vi.fn();
|
||||
const tracker = new ChatResumeWorkTracker(invalidate);
|
||||
it("tracks resume work by identity", () => {
|
||||
const tracker = new ChatResumeWorkTracker();
|
||||
const resume = tracker.begin("thread");
|
||||
|
||||
expect(invalidate).toHaveBeenCalledOnce();
|
||||
expect(tracker.isStale(resume)).toBe(false);
|
||||
tracker.invalidate();
|
||||
expect(invalidate).toHaveBeenCalledTimes(2);
|
||||
expect(tracker.isStale(resume)).toBe(true);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue