mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Simplify chat panel composition wiring
This commit is contained in:
parent
2e41795220
commit
aa5b1b6ed0
54 changed files with 1365 additions and 2103 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import type { ServerNotification } from "../../generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
|
||||
import type { ServerInitialization } from "../../domain/server/initialization";
|
||||
import { AppServerClient, type AppServerClientHandlers } from "./client";
|
||||
import { appServerInitializationFromResponse, type AppServerInitialization } from "../protocol/initialization";
|
||||
import { appServerInitializationFromResponse } from "../protocol/initialization";
|
||||
|
||||
export interface ConnectionManagerHandlers {
|
||||
onNotification: (notification: ServerNotification) => void;
|
||||
|
|
@ -14,7 +15,7 @@ export type AppServerClientFactory = (codexPath: string, cwd: string, handlers:
|
|||
|
||||
type ConnectionLifecycleState =
|
||||
| { kind: "idle"; generation: number }
|
||||
| { kind: "connecting"; generation: number; client: AppServerClient; promise: Promise<AppServerInitialization> }
|
||||
| { kind: "connecting"; generation: number; client: AppServerClient; promise: Promise<ServerInitialization> }
|
||||
| { kind: "connected"; generation: number; client: AppServerClient }
|
||||
| { kind: "disconnected"; generation: number };
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ export class ConnectionManager {
|
|||
return Boolean(this.currentClient());
|
||||
}
|
||||
|
||||
async connect(): Promise<AppServerInitialization> {
|
||||
async connect(): Promise<ServerInitialization> {
|
||||
const currentClient = this.currentClient();
|
||||
if (currentClient) {
|
||||
return appServerInitializationFromResponse(currentClient.initializeResponse);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import type { InitializeResponse as AppServerInitializeResponse } from "../../generated/app-server/InitializeResponse";
|
||||
import type { ServerInitialization } from "../../domain/server/initialization";
|
||||
|
||||
export type AppServerInitialization = ServerInitialization;
|
||||
|
||||
export function appServerInitializationFromResponse(response: AppServerInitializeResponse): AppServerInitialization {
|
||||
export function appServerInitializationFromResponse(response: AppServerInitializeResponse): ServerInitialization {
|
||||
return {
|
||||
userAgent: response.userAgent,
|
||||
codexHome: response.codexHome,
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
import type { AppServerClient } from "../../app-server/connection/client";
|
||||
import type { DisplayDetailSection, DisplayItem } from "./display/types";
|
||||
import type { ChatStateStore } from "./state/reducer";
|
||||
import type { ChatViewRenderController } from "./panel/view-render-controller";
|
||||
|
||||
interface ChatControllerCompositionActionPorts {
|
||||
client: {
|
||||
getClient: () => AppServerClient | null;
|
||||
setClient: (client: AppServerClient | null) => void;
|
||||
clear: () => void;
|
||||
};
|
||||
render: {
|
||||
panelRoot: () => HTMLElement | null;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
schedule: () => void;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
systemItem: (text: string) => DisplayItem;
|
||||
structuredSystemItem: (text: string, details: DisplayDetailSection[]) => DisplayItem;
|
||||
};
|
||||
status: {
|
||||
set: (status: string) => void;
|
||||
};
|
||||
scroll: {
|
||||
forceBottom: () => void;
|
||||
followBottom: () => void;
|
||||
preservePosition: () => void;
|
||||
};
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: () => Promise<boolean>;
|
||||
startNewThread: () => Promise<void>;
|
||||
loadSharedThreadList: () => Promise<void>;
|
||||
notifyIdentityChanged: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatControllerCompositionActions {
|
||||
client: ChatControllerCompositionActionPorts["client"] & {
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
render: ChatControllerCompositionActionPorts["render"] & {
|
||||
now: () => void;
|
||||
};
|
||||
status: ChatControllerCompositionActionPorts["status"] & {
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
|
||||
};
|
||||
scroll: ChatControllerCompositionActionPorts["scroll"];
|
||||
thread: ChatControllerCompositionActionPorts["thread"] & {
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
};
|
||||
composer: {
|
||||
setText: (text: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatControllerCompositionActions(
|
||||
ports: ChatControllerCompositionActionPorts,
|
||||
deps: {
|
||||
renderController: ChatViewRenderController;
|
||||
ensureConnected: () => Promise<void>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
setComposerText: (text: string) => void;
|
||||
},
|
||||
): ChatControllerCompositionActions {
|
||||
const { renderController } = deps;
|
||||
const render = {
|
||||
panelRoot: ports.render.panelRoot,
|
||||
closeToolbarPanelOnOutsidePointer: ports.render.closeToolbarPanelOnOutsidePointer,
|
||||
schedule: ports.render.schedule,
|
||||
now: () => {
|
||||
renderController.render();
|
||||
},
|
||||
};
|
||||
const status = {
|
||||
set: ports.status.set,
|
||||
addSystemMessage: (text: string) => {
|
||||
ports.state.stateStore.dispatch({ type: "message-stream/system-item-added", item: ports.state.systemItem(text) });
|
||||
render.now();
|
||||
},
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => {
|
||||
ports.state.stateStore.dispatch({
|
||||
type: "message-stream/system-item-added",
|
||||
item: ports.state.structuredSystemItem(text, details),
|
||||
});
|
||||
render.now();
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
client: {
|
||||
getClient: ports.client.getClient,
|
||||
setClient: ports.client.setClient,
|
||||
clear: ports.client.clear,
|
||||
ensureConnected: deps.ensureConnected,
|
||||
},
|
||||
render,
|
||||
status,
|
||||
scroll: ports.scroll,
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: ports.thread.ensureRestoredThreadLoaded,
|
||||
startNewThread: ports.thread.startNewThread,
|
||||
loadSharedThreadList: ports.thread.loadSharedThreadList,
|
||||
notifyIdentityChanged: ports.thread.notifyIdentityChanged,
|
||||
refreshTabHeader: ports.thread.refreshTabHeader,
|
||||
selectThread: deps.selectThread,
|
||||
refreshThreads: deps.refreshThreads,
|
||||
refreshSkills: deps.refreshSkills,
|
||||
},
|
||||
composer: {
|
||||
setText: (text) => {
|
||||
deps.setComposerText(text);
|
||||
render.now();
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -2,27 +2,22 @@ import type { App, Component, EventRef } from "obsidian";
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { AppServerClient } from "../../app-server/connection/client";
|
||||
import type { SharedServerMetadata } from "../../domain/server/metadata";
|
||||
import type { ArchiveExportAdapter } from "../thread-export/archive-markdown";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import type { CodexPanelSettings } from "../../settings/model";
|
||||
import type { RuntimeSnapshot } from "./runtime/snapshot";
|
||||
import type { ChatState, ChatStateStore } from "./state/reducer";
|
||||
import type { ChatTurnDiffViewState } from "./turn-diff/model";
|
||||
import type { ChatMessageScrollIntentController } from "./ui/message-stream/scroll-intent-controller";
|
||||
import type { ChatStateStore } from "./state/reducer";
|
||||
import type { ChatMessageScrollIntentState } from "./ui/message-stream/scroll-intent-state";
|
||||
import type { DisplayDetailSection, DisplayItem } from "./display/types";
|
||||
import type { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "./lifecycle";
|
||||
import type { ComposerMetaViewModel } from "./ui/composer";
|
||||
import type { CodexChatHost } from "./chat-host";
|
||||
|
||||
export interface ChatControllerCompositionPorts {
|
||||
obsidian: ChatPanelObsidianContext;
|
||||
plugin: ChatControllerHostContext;
|
||||
plugin: CodexChatHost;
|
||||
state: ChatPanelStateContext;
|
||||
client: ChatPanelClientContext;
|
||||
lifecycle: ChatPanelLifecycleContext;
|
||||
render: ChatPanelRenderContext;
|
||||
messageStream: ChatPanelMessageStreamContext;
|
||||
composerView: ChatPanelComposerContext;
|
||||
surface: ChatPanelSurfaceContext;
|
||||
runtime: ChatPanelRuntimeContext;
|
||||
thread: ChatThreadContext;
|
||||
liveState: ChatPanelLiveStateContext;
|
||||
|
|
@ -30,23 +25,6 @@ export interface ChatControllerCompositionPorts {
|
|||
status: ChatPanelStatusContext;
|
||||
}
|
||||
|
||||
interface ChatControllerHostContext {
|
||||
settings: CodexPanelSettings;
|
||||
vaultPath: string;
|
||||
openThreadInNewView: (threadId: string) => Promise<unknown>;
|
||||
focusThreadInOpenView: (threadId: string) => Promise<boolean>;
|
||||
openTurnDiff: (state: ChatTurnDiffViewState) => Promise<void>;
|
||||
notifyThreadArchived: (threadId: string) => void;
|
||||
notifyThreadRenamed: (threadId: string, name: string | null) => void;
|
||||
refreshThreadsViewLiveState: () => void;
|
||||
refreshSharedThreadListFromOpenSurface: () => void;
|
||||
applyThreadListSnapshot: (threads: readonly Thread[]) => void;
|
||||
publishAppServerMetadata: (metadata: SharedServerMetadata) => void;
|
||||
publishAppServerIdentity: (userAgent: string | null) => void;
|
||||
cachedThreadList: () => readonly Thread[] | null;
|
||||
cachedAppServerMetadata: () => SharedServerMetadata | null;
|
||||
}
|
||||
|
||||
interface ChatPanelObsidianContext {
|
||||
app: App;
|
||||
owner: Component;
|
||||
|
|
@ -58,7 +36,6 @@ interface ChatPanelObsidianContext {
|
|||
|
||||
interface ChatPanelStateContext {
|
||||
stateStore: ChatStateStore;
|
||||
getState: () => ChatState;
|
||||
systemItem: (text: string) => DisplayItem;
|
||||
structuredSystemItem: (text: string, details: DisplayDetailSection[]) => DisplayItem;
|
||||
}
|
||||
|
|
@ -73,17 +50,12 @@ interface ChatPanelLifecycleContext {
|
|||
deferredTasks: ChatViewDeferredTasks;
|
||||
resumeWork: ChatResumeWorkTracker;
|
||||
connectionWork: ChatConnectionWorkTracker;
|
||||
messageScrollIntent: ChatMessageScrollIntentController;
|
||||
messageScrollIntent: ChatMessageScrollIntentState;
|
||||
getOpened: () => boolean;
|
||||
setOpened: (opened: boolean) => void;
|
||||
getClosing: () => boolean;
|
||||
setClosing: (closing: boolean) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
scheduleDeferredDiagnostics: () => void;
|
||||
clearDeferredDiagnostics: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
clearDeferredRestoredThreadHydration: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
refreshDeferredDiagnostics: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface ChatPanelRenderContext {
|
||||
|
|
@ -96,17 +68,13 @@ interface ChatPanelRenderContext {
|
|||
schedule: () => void;
|
||||
}
|
||||
|
||||
interface ChatPanelMessageStreamContext {
|
||||
interface ChatPanelSurfaceContext {
|
||||
pendingRequestsSignature: () => string;
|
||||
}
|
||||
|
||||
interface ChatPanelComposerContext {
|
||||
composerPlaceholder: () => string;
|
||||
composerMetaViewModel: () => ComposerMetaViewModel;
|
||||
}
|
||||
|
||||
interface ChatPanelRuntimeContext {
|
||||
runtimeSnapshotForState: (state: ChatState) => RuntimeSnapshot;
|
||||
collaborationModeLabel: () => string;
|
||||
connectionDiagnosticDetails: () => DisplayDetailSection[];
|
||||
modelStatusLines: () => string[];
|
||||
|
|
|
|||
|
|
@ -6,24 +6,25 @@ import type { ChatComposerController } from "./conversation/composer/controller"
|
|||
import type { ChatInboundController } from "./protocol/inbound/controller";
|
||||
import type { GoalActions } from "./threads/goal-actions";
|
||||
import { createChatRuntimeSettingsActions, type ChatRuntimeSettingsActions } from "./runtime/settings-actions";
|
||||
import type { ChatThreadActions } from "./threads/actions";
|
||||
import type { ChatThreadActions } from "./threads/action-context";
|
||||
import type { AutoTitleController } from "./threads/auto-title-controller";
|
||||
import type { HistoryController } from "./threads/history-controller";
|
||||
import type { RenameController } from "./threads/rename-controller";
|
||||
import type { ToolbarPanelController } from "./panel/regions/toolbar";
|
||||
import type { ToolbarPanelActions } from "./panel/regions/toolbar";
|
||||
import type { ChatConnectionController } from "./connection/connection-controller";
|
||||
import type { ChatReconnectActions } from "./connection/reconnect-actions";
|
||||
import type { PendingRequestController } from "./conversation/pending-requests/controller";
|
||||
import type { DisplayDetailSection } from "./display/types";
|
||||
import { rejectServerRequest, respondToServerRequest } from "./protocol/server-requests/responder";
|
||||
import type { ComposerSubmitActions } from "./conversation/turns/composer-submit-actions";
|
||||
import type { RestorationController } from "./threads/restoration-controller";
|
||||
import type { IdentitySync } from "./threads/identity-sync";
|
||||
import type { ResumeController } from "./threads/resume-controller";
|
||||
import type { SelectionActions } from "./threads/selection-actions";
|
||||
import type { ChatViewRenderController } from "./panel/view-render-controller";
|
||||
import type { MessageStreamRenderer } from "./ui/message-stream/renderer";
|
||||
import type { ChatControllerCompositionPorts } from "./composition-ports";
|
||||
import { createChatControllerCompositionActions } from "./composition-actions";
|
||||
import { scheduleAppServerWarmup } from "./connection/app-server-warmup";
|
||||
import { runtimeSnapshotForChatState } from "./runtime/snapshot";
|
||||
import {
|
||||
createChatServerActionControllers,
|
||||
createChatConnectionControllers,
|
||||
|
|
@ -32,11 +33,7 @@ import {
|
|||
} from "./connection/composition";
|
||||
import { createThreadControllerGroup, createThreadSelectionActionGroup } from "./threads/composition";
|
||||
import { createConversationSurfaceControllerGroup } from "./conversation/composition";
|
||||
import {
|
||||
createConnectionLifecycleControllerGroup,
|
||||
createPanelUiControllerGroup,
|
||||
createViewRenderControllerGroup,
|
||||
} from "./panel/composition";
|
||||
import { createChatViewRenderer, createConnectionLifecycleControllerGroup, createPanelUiControllerGroup } from "./panel/composition";
|
||||
|
||||
export interface ChatViewControllers {
|
||||
connection: {
|
||||
|
|
@ -71,14 +68,14 @@ export interface ChatViewControllers {
|
|||
pending: PendingRequestController;
|
||||
};
|
||||
toolbar: {
|
||||
panels: ToolbarPanelController;
|
||||
panels: ToolbarPanelActions;
|
||||
};
|
||||
composer: {
|
||||
controller: ChatComposerController;
|
||||
submission: ComposerSubmitActions;
|
||||
};
|
||||
render: {
|
||||
controller: ChatViewRenderController;
|
||||
now: () => void;
|
||||
messageStream: MessageStreamRenderer;
|
||||
openView: () => void;
|
||||
closeView: () => void;
|
||||
|
|
@ -86,99 +83,68 @@ export interface ChatViewControllers {
|
|||
};
|
||||
}
|
||||
|
||||
interface ChatCompositionSideEffects {
|
||||
render: Pick<ChatControllerCompositionPorts["render"], "panelRoot" | "closeToolbarPanelOnOutsidePointer" | "schedule"> & {
|
||||
now: () => void;
|
||||
};
|
||||
status: ChatControllerCompositionPorts["status"] & {
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
|
||||
};
|
||||
composer: {
|
||||
setText: (text: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatViewControllers(ports: ChatControllerCompositionPorts): ChatViewControllers {
|
||||
const connection = new ConnectionManager(() => ports.plugin.settings.codexPath, ports.plugin.vaultPath);
|
||||
const { renderController } = createViewRenderControllerGroup({
|
||||
plugin: {
|
||||
settings: ports.plugin.settings,
|
||||
},
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
},
|
||||
lifecycle: {
|
||||
deferredTasks: ports.lifecycle.deferredTasks,
|
||||
},
|
||||
render: {
|
||||
panelRoot: ports.render.panelRoot,
|
||||
toolbarNode: ports.render.toolbarNode,
|
||||
goalNode: ports.render.goalNode,
|
||||
messageStreamNode: ports.render.messageStreamNode,
|
||||
composerNode: ports.render.composerNode,
|
||||
},
|
||||
});
|
||||
const renderNow = createChatViewRenderer(ports);
|
||||
let connectionController: ChatConnectionController | null = null;
|
||||
let selection: SelectionActions | null = null;
|
||||
let composerController: ChatComposerController | null = null;
|
||||
const actions = createChatControllerCompositionActions(
|
||||
{
|
||||
state: ports.state,
|
||||
client: ports.client,
|
||||
render: ports.render,
|
||||
status: ports.status,
|
||||
scroll: ports.scroll,
|
||||
thread: ports.thread,
|
||||
const ensureConnected = () => requireComposedController(connectionController, "connection controller").ensureConnected();
|
||||
const refreshThreads = () => requireComposedController(connectionController, "connection controller").refreshThreads();
|
||||
const refreshSkills = (forceReload?: boolean) =>
|
||||
requireComposedController(connectionController, "connection controller").refreshSkills(forceReload);
|
||||
const selectThread = (threadId: string) => requireComposedController(selection, "selection actions").selectThread(threadId);
|
||||
const sideEffects = createChatCompositionSideEffects(ports, {
|
||||
renderNow,
|
||||
setComposerText: (text) => {
|
||||
requireComposedController(composerController, "composer controller").setDraft(text, { focus: true });
|
||||
},
|
||||
{
|
||||
renderController,
|
||||
ensureConnected: () => requireComposedController(connectionController, "connection controller").ensureConnected(),
|
||||
refreshThreads: () => requireComposedController(connectionController, "connection controller").refreshThreads(),
|
||||
refreshSkills: (forceReload) => requireComposedController(connectionController, "connection controller").refreshSkills(forceReload),
|
||||
selectThread: (threadId) => requireComposedController(selection, "selection actions").selectThread(threadId),
|
||||
setComposerText: (text) => {
|
||||
requireComposedController(composerController, "composer controller").setDraft(text, { focus: true });
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
const runtimeSettings = createChatRuntimeSettingsActions({
|
||||
stateStore: ports.state.stateStore,
|
||||
currentClient: ports.client.getClient,
|
||||
runtimeSnapshotForState: ports.runtime.runtimeSnapshotForState,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
collaborationModeLabel: ports.runtime.collaborationModeLabel,
|
||||
addSystemMessage: actions.status.addSystemMessage,
|
||||
addSystemMessage: sideEffects.status.addSystemMessage,
|
||||
});
|
||||
const scheduleWarmup = () => {
|
||||
scheduleAppServerWarmup({
|
||||
deferredTasks: ports.lifecycle.deferredTasks,
|
||||
opened: ports.lifecycle.getOpened,
|
||||
closing: ports.lifecycle.getClosing,
|
||||
connected: () => connection.isConnected(),
|
||||
ensureConnected,
|
||||
});
|
||||
};
|
||||
const threadControllers = createThreadControllerGroup(
|
||||
{
|
||||
obsidian: {
|
||||
archiveAdapter: ports.obsidian.archiveAdapter,
|
||||
...ports,
|
||||
client: {
|
||||
getClient: ports.client.getClient,
|
||||
ensureConnected,
|
||||
},
|
||||
plugin: {
|
||||
notifyThreadArchived: (threadId) => {
|
||||
ports.plugin.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
ports.plugin.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
openThreadInNewView: (threadId) => ports.plugin.openThreadInNewView(threadId),
|
||||
refreshSharedThreadListFromOpenSurface: () => {
|
||||
ports.plugin.refreshSharedThreadListFromOpenSurface();
|
||||
},
|
||||
settings: ports.plugin.settings,
|
||||
vaultPath: ports.plugin.vaultPath,
|
||||
},
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
},
|
||||
client: actions.client,
|
||||
lifecycle: {
|
||||
deferredTasks: ports.lifecycle.deferredTasks,
|
||||
resumeWork: ports.lifecycle.resumeWork,
|
||||
getOpened: ports.lifecycle.getOpened,
|
||||
getClosing: ports.lifecycle.getClosing,
|
||||
clearDeferredRestoredThreadHydration: ports.lifecycle.clearDeferredRestoredThreadHydration,
|
||||
},
|
||||
render: actions.render,
|
||||
status: actions.status,
|
||||
render: sideEffects.render,
|
||||
status: sideEffects.status,
|
||||
thread: {
|
||||
selectThread: actions.thread.selectThread,
|
||||
refreshThreads: actions.thread.refreshThreads,
|
||||
notifyIdentityChanged: ports.thread.notifyIdentityChanged,
|
||||
refreshTabHeader: ports.thread.refreshTabHeader,
|
||||
...ports.thread,
|
||||
selectThread,
|
||||
refreshThreads,
|
||||
},
|
||||
liveState: {
|
||||
refresh: ports.liveState.refresh,
|
||||
},
|
||||
scroll: actions.scroll,
|
||||
composer: actions.composer,
|
||||
scroll: ports.scroll,
|
||||
composer: sideEffects.composer,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
|
|
@ -194,21 +160,33 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
setOpened: ports.lifecycle.setOpened,
|
||||
getClosing: ports.lifecycle.getClosing,
|
||||
setClosing: ports.lifecycle.setClosing,
|
||||
invalidateConnectionWork: ports.lifecycle.invalidateConnectionWork,
|
||||
invalidateConnectionWork: () => {
|
||||
ports.lifecycle.connectionWork.invalidate();
|
||||
},
|
||||
invalidateResumeWork: threadControllers.invalidateResumeWork,
|
||||
scheduleDeferredDiagnostics: ports.lifecycle.scheduleDeferredDiagnostics,
|
||||
clearDeferredDiagnostics: ports.lifecycle.clearDeferredDiagnostics,
|
||||
scheduleDeferredRestoredThreadHydration: ports.lifecycle.scheduleDeferredRestoredThreadHydration,
|
||||
clearDeferredRestoredThreadHydration: ports.lifecycle.clearDeferredRestoredThreadHydration,
|
||||
scheduleDeferredAppServerWarmup: ports.lifecycle.scheduleDeferredAppServerWarmup,
|
||||
scheduleDeferredDiagnostics: () => {
|
||||
ports.lifecycle.deferredTasks.scheduleDiagnostics(() => {
|
||||
void ports.lifecycle.refreshDeferredDiagnostics();
|
||||
});
|
||||
},
|
||||
clearDeferredDiagnostics: () => {
|
||||
ports.lifecycle.deferredTasks.clearDiagnostics();
|
||||
},
|
||||
scheduleDeferredRestoredThreadHydration: () => {
|
||||
restoration.scheduleHydration();
|
||||
},
|
||||
clearDeferredRestoredThreadHydration: () => {
|
||||
restoration.clearHydration();
|
||||
},
|
||||
scheduleDeferredAppServerWarmup: () => {
|
||||
scheduleWarmup();
|
||||
},
|
||||
};
|
||||
const { toolbarPanels, applyViewState } = createPanelUiControllerGroup(
|
||||
{
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
},
|
||||
state: ports.state,
|
||||
lifecycle: lifecycleActions,
|
||||
render: actions.render,
|
||||
render: sideEffects.render,
|
||||
thread: {
|
||||
restorePlaceholder: (restoredThreadState) => {
|
||||
restoration.restore(restoredThreadState);
|
||||
|
|
@ -224,16 +202,12 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
);
|
||||
selection = createThreadSelectionActionGroup(
|
||||
{
|
||||
plugin: {
|
||||
focusThreadInOpenView: (threadId) => ports.plugin.focusThreadInOpenView(threadId),
|
||||
},
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
},
|
||||
plugin: ports.plugin,
|
||||
state: ports.state,
|
||||
thread: {
|
||||
resumeThread: (threadId) => resume.resumeThread(threadId),
|
||||
},
|
||||
status: actions.status,
|
||||
status: sideEffects.status,
|
||||
},
|
||||
{
|
||||
closeForThreadSelection: () => {
|
||||
|
|
@ -243,13 +217,14 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
).selection;
|
||||
const { reconnectActions } = createChatReconnectControllerGroup(
|
||||
{
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
state: ports.state,
|
||||
client: {
|
||||
clear: ports.client.clear,
|
||||
ensureConnected,
|
||||
},
|
||||
client: actions.client,
|
||||
lifecycle: lifecycleActions,
|
||||
render: actions.render,
|
||||
status: actions.status,
|
||||
render: sideEffects.render,
|
||||
status: sideEffects.status,
|
||||
thread: {
|
||||
resumeThread: (threadId) => resume.resumeThread(threadId),
|
||||
},
|
||||
|
|
@ -258,50 +233,22 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
connection,
|
||||
},
|
||||
);
|
||||
const serverActionControllers = createChatServerActionControllers(
|
||||
{
|
||||
plugin: {
|
||||
applyThreadListSnapshot: (threads) => {
|
||||
ports.plugin.applyThreadListSnapshot(threads);
|
||||
},
|
||||
publishAppServerMetadata: (metadata) => {
|
||||
ports.plugin.publishAppServerMetadata(metadata);
|
||||
},
|
||||
vaultPath: ports.plugin.vaultPath,
|
||||
},
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
},
|
||||
runtime: {
|
||||
runtimeSnapshotForState: ports.runtime.runtimeSnapshotForState,
|
||||
},
|
||||
},
|
||||
{
|
||||
connection,
|
||||
goals,
|
||||
},
|
||||
);
|
||||
const serverActionControllers = createChatServerActionControllers(ports, {
|
||||
connection,
|
||||
goals,
|
||||
});
|
||||
const { serverThreads, serverMetadata, serverDiagnostics } = serverActionControllers;
|
||||
const serverRequestHost = {
|
||||
currentClient: ports.client.getClient,
|
||||
};
|
||||
const inboundController = createChatInboundController(
|
||||
{
|
||||
plugin: {
|
||||
notifyThreadArchived: (threadId) => {
|
||||
ports.plugin.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
ports.plugin.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
},
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
},
|
||||
render: actions.render,
|
||||
plugin: ports.plugin,
|
||||
state: ports.state,
|
||||
render: sideEffects.render,
|
||||
thread: {
|
||||
refreshThreads: actions.thread.refreshThreads,
|
||||
refreshSkills: actions.thread.refreshSkills,
|
||||
refreshThreads,
|
||||
refreshSkills,
|
||||
publishAppServerMetadataSnapshot: () => {
|
||||
serverMetadata.publishAppServerMetadataSnapshot();
|
||||
},
|
||||
|
|
@ -317,16 +264,11 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
);
|
||||
connectionController = createChatConnectionControllers(
|
||||
{
|
||||
plugin: {
|
||||
publishAppServerIdentity: (userAgent) => {
|
||||
ports.plugin.publishAppServerIdentity(userAgent);
|
||||
},
|
||||
settings: ports.plugin.settings,
|
||||
plugin: ports.plugin,
|
||||
state: ports.state,
|
||||
client: {
|
||||
setClient: ports.client.setClient,
|
||||
},
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
},
|
||||
client: actions.client,
|
||||
lifecycle: lifecycleActions,
|
||||
thread: {
|
||||
loadSharedThreadList: ports.thread.loadSharedThreadList,
|
||||
|
|
@ -335,11 +277,9 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
autoTitle.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
},
|
||||
status: actions.status,
|
||||
liveState: {
|
||||
refresh: ports.liveState.refresh,
|
||||
},
|
||||
render: actions.render,
|
||||
status: sideEffects.status,
|
||||
liveState: ports.liveState,
|
||||
render: sideEffects.render,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
|
|
@ -352,16 +292,16 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
onNotification: (notification) => {
|
||||
inboundController.handleNotification(notification);
|
||||
ports.liveState.refresh();
|
||||
actions.render.schedule();
|
||||
sideEffects.render.schedule();
|
||||
},
|
||||
onServerRequest: (request) => {
|
||||
inboundController.handleServerRequest(request);
|
||||
ports.liveState.refresh();
|
||||
actions.render.now();
|
||||
sideEffects.render.now();
|
||||
},
|
||||
onLog: (message) => {
|
||||
inboundController.handleAppServerLog(message);
|
||||
actions.render.now();
|
||||
sideEffects.render.now();
|
||||
},
|
||||
onExit: () => {
|
||||
connectionController.handleExit();
|
||||
|
|
@ -370,54 +310,25 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
|
||||
const conversationControllers = createConversationSurfaceControllerGroup(
|
||||
{
|
||||
obsidian: {
|
||||
app: ports.obsidian.app,
|
||||
owner: ports.obsidian.owner,
|
||||
viewId: ports.obsidian.viewId,
|
||||
...ports,
|
||||
client: {
|
||||
getClient: ports.client.getClient,
|
||||
ensureConnected,
|
||||
},
|
||||
plugin: {
|
||||
openTurnDiff: (state) => ports.plugin.openTurnDiff(state),
|
||||
settings: ports.plugin.settings,
|
||||
vaultPath: ports.plugin.vaultPath,
|
||||
},
|
||||
state: {
|
||||
stateStore: ports.state.stateStore,
|
||||
getState: ports.state.getState,
|
||||
},
|
||||
client: actions.client,
|
||||
render: actions.render,
|
||||
render: sideEffects.render,
|
||||
runtime: {
|
||||
runtimeSnapshotForState: ports.runtime.runtimeSnapshotForState,
|
||||
statusSummaryLines: ports.runtime.statusSummaryLines,
|
||||
connectionDiagnosticDetails: ports.runtime.connectionDiagnosticDetails,
|
||||
modelStatusLines: ports.runtime.modelStatusLines,
|
||||
effortStatusLines: ports.runtime.effortStatusLines,
|
||||
...ports.runtime,
|
||||
mcpStatusLines: () => serverDiagnostics.mcpStatusLines(),
|
||||
},
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: ports.thread.ensureRestoredThreadLoaded,
|
||||
startNewThread: ports.thread.startNewThread,
|
||||
selectThread: actions.thread.selectThread,
|
||||
notifyIdentityChanged: ports.thread.notifyIdentityChanged,
|
||||
...ports.thread,
|
||||
selectThread,
|
||||
resetTurnPresence: (hadTurns) => {
|
||||
autoTitle.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
},
|
||||
status: actions.status,
|
||||
scroll: actions.scroll,
|
||||
lifecycle: {
|
||||
messageScrollIntent: ports.lifecycle.messageScrollIntent,
|
||||
},
|
||||
messageStream: {
|
||||
pendingRequestsSignature: ports.messageStream.pendingRequestsSignature,
|
||||
},
|
||||
composerView: {
|
||||
composerPlaceholder: ports.composerView.composerPlaceholder,
|
||||
composerMetaViewModel: ports.composerView.composerMetaViewModel,
|
||||
},
|
||||
liveState: {
|
||||
refresh: ports.liveState.refresh,
|
||||
},
|
||||
status: sideEffects.status,
|
||||
scroll: ports.scroll,
|
||||
},
|
||||
{
|
||||
controller: inboundController,
|
||||
|
|
@ -432,40 +343,14 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
const { pendingRequests, composerSubmit } = conversationControllers;
|
||||
const { messageStreamRenderer } = conversationControllers;
|
||||
composerController = conversationControllers.composerController;
|
||||
const { scheduleAppServerWarmup, openView, closeView } = createConnectionLifecycleControllerGroup(
|
||||
const lifecycleControllers = createConnectionLifecycleControllerGroup(
|
||||
{
|
||||
obsidian: {
|
||||
registerEvent: ports.obsidian.registerEvent,
|
||||
registerPointerDown: ports.obsidian.registerPointerDown,
|
||||
},
|
||||
plugin: {
|
||||
cachedThreadList: () => ports.plugin.cachedThreadList(),
|
||||
cachedAppServerMetadata: () => ports.plugin.cachedAppServerMetadata(),
|
||||
},
|
||||
...ports,
|
||||
client: {
|
||||
clear: actions.client.clear,
|
||||
ensureConnected: actions.client.ensureConnected,
|
||||
},
|
||||
lifecycle: {
|
||||
deferredTasks: lifecycleActions.deferredTasks,
|
||||
getOpened: lifecycleActions.getOpened,
|
||||
setOpened: lifecycleActions.setOpened,
|
||||
getClosing: lifecycleActions.getClosing,
|
||||
setClosing: lifecycleActions.setClosing,
|
||||
invalidateConnectionWork: lifecycleActions.invalidateConnectionWork,
|
||||
invalidateResumeWork: lifecycleActions.invalidateResumeWork,
|
||||
scheduleDeferredRestoredThreadHydration: lifecycleActions.scheduleDeferredRestoredThreadHydration,
|
||||
scheduleDeferredAppServerWarmup: lifecycleActions.scheduleDeferredAppServerWarmup,
|
||||
},
|
||||
render: {
|
||||
panelRoot: actions.render.panelRoot,
|
||||
closeToolbarPanelOnOutsidePointer: actions.render.closeToolbarPanelOnOutsidePointer,
|
||||
now: actions.render.now,
|
||||
},
|
||||
liveState: {
|
||||
refresh: ports.liveState.refresh,
|
||||
deferRefresh: ports.liveState.deferRefresh,
|
||||
clear: ports.client.clear,
|
||||
},
|
||||
lifecycle: lifecycleActions,
|
||||
render: sideEffects.render,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
|
|
@ -481,7 +366,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
manager: connection,
|
||||
controller: connectionController,
|
||||
reconnect: reconnectActions,
|
||||
scheduleWarmup: scheduleAppServerWarmup,
|
||||
scheduleWarmup,
|
||||
},
|
||||
inbound: {
|
||||
controller: inboundController,
|
||||
|
|
@ -516,10 +401,10 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
|
|||
submission: composerSubmit,
|
||||
},
|
||||
render: {
|
||||
controller: renderController,
|
||||
now: sideEffects.render.now,
|
||||
messageStream: messageStreamRenderer,
|
||||
openView,
|
||||
closeView,
|
||||
openView: lifecycleControllers.openView,
|
||||
closeView: lifecycleControllers.closeView,
|
||||
applyViewState,
|
||||
},
|
||||
};
|
||||
|
|
@ -529,3 +414,45 @@ function requireComposedController<T>(controller: T | null, name: string): T {
|
|||
if (!controller) throw new Error(`Chat view controller composition did not initialize ${name}.`);
|
||||
return controller;
|
||||
}
|
||||
|
||||
function createChatCompositionSideEffects(
|
||||
ports: Pick<ChatControllerCompositionPorts, "render" | "state" | "status">,
|
||||
deps: {
|
||||
renderNow: () => void;
|
||||
setComposerText: (text: string) => void;
|
||||
},
|
||||
): ChatCompositionSideEffects {
|
||||
const render = {
|
||||
panelRoot: ports.render.panelRoot,
|
||||
closeToolbarPanelOnOutsidePointer: ports.render.closeToolbarPanelOnOutsidePointer,
|
||||
schedule: ports.render.schedule,
|
||||
now: () => {
|
||||
deps.renderNow();
|
||||
},
|
||||
};
|
||||
const status = {
|
||||
set: ports.status.set,
|
||||
addSystemMessage: (text: string) => {
|
||||
ports.state.stateStore.dispatch({ type: "message-stream/system-item-added", item: ports.state.systemItem(text) });
|
||||
render.now();
|
||||
},
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => {
|
||||
ports.state.stateStore.dispatch({
|
||||
type: "message-stream/system-item-added",
|
||||
item: ports.state.structuredSystemItem(text, details),
|
||||
});
|
||||
render.now();
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
render,
|
||||
status,
|
||||
composer: {
|
||||
setText: (text) => {
|
||||
deps.setComposerText(text);
|
||||
render.now();
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { Notice } from "obsidian";
|
||||
|
||||
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
|
||||
import type { RuntimeSnapshot } from "../runtime/snapshot";
|
||||
import type { ChatState, ChatStateStore } from "../state/reducer";
|
||||
import type { SharedServerMetadata } from "../../../domain/server/metadata";
|
||||
import type { Thread } from "../../../domain/threads/model";
|
||||
import type { CodexPanelSettings } from "../../../settings/model";
|
||||
import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "./server-actions/diagnostics";
|
||||
import { createChatServerMetadataActions, type ChatServerMetadataActions } from "./server-actions/metadata";
|
||||
import { createChatServerThreadActions } from "./server-actions/threads";
|
||||
|
|
@ -16,20 +11,10 @@ import type { GoalActions } from "../threads/goal-actions";
|
|||
import type { AutoTitleController } from "../threads/auto-title-controller";
|
||||
import { ChatInboundController } from "../protocol/inbound/controller";
|
||||
import type { ChatConnectionWorkTracker } from "../lifecycle";
|
||||
import type { ChatControllerCompositionPorts } from "../composition-ports";
|
||||
import { runtimeSnapshotForChatState } from "../runtime/snapshot";
|
||||
|
||||
interface ChatServerActionControllerPorts {
|
||||
plugin: {
|
||||
applyThreadListSnapshot: (threads: readonly Thread[]) => void;
|
||||
publishAppServerMetadata: (metadata: SharedServerMetadata) => void;
|
||||
vaultPath: string;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
runtime: {
|
||||
runtimeSnapshotForState: (state: ChatState) => RuntimeSnapshot;
|
||||
};
|
||||
}
|
||||
type ChatServerActionControllerPorts = Pick<ChatControllerCompositionPorts, "plugin" | "state">;
|
||||
|
||||
export function createChatServerActionControllers(
|
||||
context: ChatServerActionControllerPorts,
|
||||
|
|
@ -38,7 +23,7 @@ export function createChatServerActionControllers(
|
|||
goals: GoalActions;
|
||||
},
|
||||
) {
|
||||
const { plugin, runtime } = context;
|
||||
const { plugin } = context;
|
||||
const { stateStore } = context.state;
|
||||
const currentClient = () => refs.connection.currentClient();
|
||||
const serverMetadata = createChatServerMetadataActions({
|
||||
|
|
@ -62,7 +47,7 @@ export function createChatServerActionControllers(
|
|||
stateStore,
|
||||
vaultPath: plugin.vaultPath,
|
||||
currentClient,
|
||||
runtimeSnapshotForState: runtime.runtimeSnapshotForState,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
publishThreadList: (threads) => {
|
||||
plugin.applyThreadListSnapshot(threads);
|
||||
},
|
||||
|
|
@ -74,14 +59,7 @@ export function createChatServerActionControllers(
|
|||
return { serverThreads, serverMetadata, serverDiagnostics };
|
||||
}
|
||||
|
||||
interface ChatInboundControllerPorts {
|
||||
plugin: {
|
||||
notifyThreadArchived: (threadId: string) => void;
|
||||
notifyThreadRenamed: (threadId: string, name: string | null) => void;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
type ChatInboundControllerPorts = Pick<ChatControllerCompositionPorts, "plugin" | "state"> & {
|
||||
render: {
|
||||
schedule: () => void;
|
||||
};
|
||||
|
|
@ -90,7 +68,7 @@ interface ChatInboundControllerPorts {
|
|||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
publishAppServerMetadataSnapshot: () => void;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createChatInboundController(
|
||||
context: ChatInboundControllerPorts,
|
||||
|
|
@ -127,14 +105,7 @@ export function createChatInboundController(
|
|||
});
|
||||
}
|
||||
|
||||
interface ChatConnectionControllerPorts {
|
||||
plugin: {
|
||||
publishAppServerIdentity: (userAgent: string | null) => void;
|
||||
settings: CodexPanelSettings;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
type ChatConnectionControllerPorts = Pick<ChatControllerCompositionPorts, "plugin" | "state" | "liveState"> & {
|
||||
client: {
|
||||
setClient: (client: ReturnType<ConnectionManager["currentClient"]>) => void;
|
||||
};
|
||||
|
|
@ -153,14 +124,11 @@ interface ChatConnectionControllerPorts {
|
|||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
};
|
||||
render: {
|
||||
now: () => void;
|
||||
schedule: () => void;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createChatConnectionControllers(
|
||||
context: ChatConnectionControllerPorts,
|
||||
|
|
@ -194,7 +162,9 @@ export function createChatConnectionControllers(
|
|||
resetThreadTurnPresence: thread.resetTurnPresence,
|
||||
setStatus: status.set,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
publishAppServerIdentity: plugin.publishAppServerIdentity,
|
||||
publishAppServerIdentity: (userAgent) => {
|
||||
plugin.publishAppServerIdentity(userAgent);
|
||||
},
|
||||
configuredCommand: () => plugin.settings.codexPath,
|
||||
refreshLiveState: liveState.refresh,
|
||||
render: render.now,
|
||||
|
|
@ -206,10 +176,7 @@ export function createChatConnectionControllers(
|
|||
};
|
||||
}
|
||||
|
||||
interface ChatReconnectControllerGroupPorts {
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
type ChatReconnectControllerGroupPorts = Pick<ChatControllerCompositionPorts, "state"> & {
|
||||
client: {
|
||||
clear: () => void;
|
||||
ensureConnected: () => Promise<void>;
|
||||
|
|
@ -229,7 +196,7 @@ interface ChatReconnectControllerGroupPorts {
|
|||
thread: {
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createChatReconnectControllerGroup(
|
||||
context: ChatReconnectControllerGroupPorts,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import type { App, Component } from "obsidian";
|
||||
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ChatServerThreadActions } from "../connection/server-actions/threads";
|
||||
import { ChatComposerController } from "./composer/controller";
|
||||
import { activeTurnId, type ChatState, type ChatStateStore } from "../state/reducer";
|
||||
import { activeTurnId } from "../state/reducer";
|
||||
import type { ChatReconnectActions } from "../connection/reconnect-actions";
|
||||
import { PendingRequestController } from "./pending-requests/controller";
|
||||
import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions";
|
||||
|
|
@ -11,80 +8,45 @@ import { createComposerSubmitActions } from "./turns/composer-submit-actions";
|
|||
import { createPlanImplementation } from "./turns/plan-implementation";
|
||||
import { createSlashCommandHandler } from "./turns/slash-command-handler";
|
||||
import { TurnSubmissionController } from "./turns/turn-submission-controller";
|
||||
import type { ChatThreadActions } from "../threads/actions";
|
||||
import type { ChatThreadActions } from "../threads/action-context";
|
||||
import type { GoalActions } from "../threads/goal-actions";
|
||||
import type { HistoryController } from "../threads/history-controller";
|
||||
import type { ChatInboundController } from "../protocol/inbound/controller";
|
||||
import { currentModel, runtimeConfigOrDefault } from "../runtime/effective";
|
||||
import type { RuntimeSnapshot } from "../runtime/snapshot";
|
||||
import { runtimeSnapshotForChatState } from "../runtime/snapshot";
|
||||
import { MessageStreamRenderer } from "../ui/message-stream/renderer";
|
||||
import type { ChatControllerCompositionPorts } from "../composition-ports";
|
||||
import type { DisplayDetailSection } from "../display/types";
|
||||
import type { ChatMessageScrollIntentController } from "../ui/message-stream/scroll-intent-controller";
|
||||
import type { ChatTurnDiffViewState } from "../turn-diff/model";
|
||||
import type { ComposerMetaViewModel } from "../ui/composer";
|
||||
import type { CodexPanelSettings } from "../../../settings/model";
|
||||
|
||||
interface ConversationSurfaceControllerGroupPorts {
|
||||
obsidian: {
|
||||
app: App;
|
||||
owner: Component;
|
||||
viewId: string;
|
||||
};
|
||||
plugin: {
|
||||
openTurnDiff: (state: ChatTurnDiffViewState) => Promise<void>;
|
||||
settings: CodexPanelSettings;
|
||||
vaultPath: string;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
getState: () => ChatState;
|
||||
};
|
||||
type ConversationSurfaceControllerGroupPorts = Pick<
|
||||
ChatControllerCompositionPorts,
|
||||
"obsidian" | "plugin" | "state" | "lifecycle" | "surface" | "runtime" | "liveState"
|
||||
> & {
|
||||
client: {
|
||||
getClient: () => AppServerClient | null;
|
||||
getClient: ChatControllerCompositionPorts["client"]["getClient"];
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
lifecycle: {
|
||||
messageScrollIntent: ChatMessageScrollIntentController;
|
||||
};
|
||||
render: {
|
||||
now: () => void;
|
||||
schedule: () => void;
|
||||
};
|
||||
messageStream: {
|
||||
pendingRequestsSignature: () => string;
|
||||
};
|
||||
composerView: {
|
||||
composerPlaceholder: () => string;
|
||||
composerMetaViewModel: () => ComposerMetaViewModel;
|
||||
};
|
||||
runtime: {
|
||||
runtimeSnapshotForState: (state: ChatState) => 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;
|
||||
followBottom: () => void;
|
||||
scroll: Pick<ChatControllerCompositionPorts["scroll"], "forceBottom" | "followBottom">;
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: ChatControllerCompositionPorts["thread"]["ensureRestoredThreadLoaded"];
|
||||
startNewThread: ChatControllerCompositionPorts["thread"]["startNewThread"];
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
notifyIdentityChanged: () => void;
|
||||
resetTurnPresence: (hadTurns: boolean) => void;
|
||||
};
|
||||
}
|
||||
runtime: ChatControllerCompositionPorts["runtime"] & {
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
};
|
||||
};
|
||||
|
||||
export function createConversationSurfaceControllerGroup(
|
||||
context: ConversationSurfaceControllerGroupPorts,
|
||||
|
|
@ -98,7 +60,7 @@ export function createConversationSurfaceControllerGroup(
|
|||
history: HistoryController;
|
||||
},
|
||||
) {
|
||||
const { plugin, state, render, messageStream, composerView, runtime, thread, liveState, status, lifecycle, client, scroll } = context;
|
||||
const { plugin, state, render, surface, runtime, thread, liveState, status, lifecycle, client, scroll } = context;
|
||||
const { app, owner, viewId } = context.obsidian;
|
||||
const stateStore = state.stateStore;
|
||||
const currentClient = client.getClient;
|
||||
|
|
@ -111,14 +73,14 @@ export function createConversationSurfaceControllerGroup(
|
|||
sendShortcut: () => plugin.settings.sendShortcut,
|
||||
scrollThreadFromComposerEdges: () => plugin.settings.scrollThreadFromComposerEdges,
|
||||
canInterrupt: () => {
|
||||
const current = state.getState();
|
||||
const current = stateStore.getState();
|
||||
return current.turn.lifecycle.kind !== "idle" && Boolean(current.activeThread.id && activeTurnId(current));
|
||||
},
|
||||
composerPlaceholder: composerView.composerPlaceholder,
|
||||
composerMeta: composerView.composerMetaViewModel,
|
||||
composerPlaceholder: surface.composerPlaceholder,
|
||||
composerMeta: surface.composerMetaViewModel,
|
||||
currentModelForSuggestions: () => {
|
||||
const current = state.getState();
|
||||
return currentModel(runtime.runtimeSnapshotForState(current), runtimeConfigOrDefault(current.connection.runtimeConfig));
|
||||
const current = stateStore.getState();
|
||||
return currentModel(runtimeSnapshotForChatState(current), runtimeConfigOrDefault(current.connection.runtimeConfig));
|
||||
},
|
||||
togglePlan: () => void refs.runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
|
||||
|
|
@ -128,6 +90,16 @@ export function createConversationSurfaceControllerGroup(
|
|||
messageStreamRenderer.repinMessageStreamToBottomIfPinned();
|
||||
},
|
||||
});
|
||||
const codexInput = (text: string) => composerController.codexInput(text);
|
||||
const setComposerDraft = (text: string, options?: { focus?: boolean; clearSuggestions?: boolean }) => {
|
||||
composerController.setDraft(text, options);
|
||||
};
|
||||
const startThread = (preview?: string) => refs.serverThreads.startThread(preview);
|
||||
const startThreadForGoal = async (objective: string) => {
|
||||
const response = await refs.serverThreads.startThread(objective, { syncGoal: false });
|
||||
return response?.threadId ?? null;
|
||||
};
|
||||
|
||||
const pendingRequests = new PendingRequestController({
|
||||
stateStore,
|
||||
controller: refs.controller,
|
||||
|
|
@ -138,93 +110,60 @@ export function createConversationSurfaceControllerGroup(
|
|||
|
||||
const turnSubmission = new TurnSubmissionController({
|
||||
stateStore,
|
||||
connection: {
|
||||
vaultPath: plugin.vaultPath,
|
||||
currentClient,
|
||||
},
|
||||
restoredThread: {
|
||||
ensureRestoredThreadLoaded: thread.ensureRestoredThreadLoaded,
|
||||
},
|
||||
thread: {
|
||||
startThread: (preview) => refs.serverThreads.startThread(preview),
|
||||
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
|
||||
resetThreadTurnPresence: thread.resetTurnPresence,
|
||||
},
|
||||
runtime: {
|
||||
applyPendingThreadSettings: () => refs.runtimeSettings.applyPendingThreadSettings(),
|
||||
},
|
||||
composer: {
|
||||
codexInput: (text) => composerController.codexInput(text),
|
||||
setDraft: (text, options) => {
|
||||
composerController.setDraft(text, options);
|
||||
},
|
||||
},
|
||||
view: {
|
||||
render: render.now,
|
||||
scheduleRender: render.schedule,
|
||||
},
|
||||
status: {
|
||||
setStatus: status.set,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
},
|
||||
vaultPath: plugin.vaultPath,
|
||||
currentClient,
|
||||
ensureRestoredThreadLoaded: thread.ensureRestoredThreadLoaded,
|
||||
startThread,
|
||||
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
|
||||
resetThreadTurnPresence: thread.resetTurnPresence,
|
||||
applyPendingThreadSettings: () => refs.runtimeSettings.applyPendingThreadSettings(),
|
||||
codexInput,
|
||||
setDraft: setComposerDraft,
|
||||
render: render.now,
|
||||
scheduleRender: render.schedule,
|
||||
setStatus: status.set,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
});
|
||||
const slashCommands = createSlashCommandHandler({
|
||||
stateStore,
|
||||
currentClient,
|
||||
codexInput: (text) => composerController.codexInput(text),
|
||||
threads: {
|
||||
startNewThread: thread.startNewThread,
|
||||
startThreadForGoal: async (objective) => {
|
||||
const response = await refs.serverThreads.startThread(objective, { syncGoal: false });
|
||||
return response?.threadId ?? null;
|
||||
},
|
||||
resumeThread: thread.selectThread,
|
||||
forkThread: (threadId) => refs.threadActions.forkThread(threadId),
|
||||
rollbackThread: (threadId) => refs.threadActions.rollbackThread(threadId),
|
||||
compactThread: (threadId) => refs.threadActions.compactThread(threadId),
|
||||
archiveThread: (threadId) => refs.threadActions.archiveThread(threadId),
|
||||
renameThread: (threadId, name) => refs.threadActions.renameThread(threadId, name).then(() => undefined),
|
||||
reconnect: () => refs.reconnectActions.reconnectPanel(),
|
||||
},
|
||||
runtime: {
|
||||
toggleFastMode: () => refs.runtimeSettings.toggleFastMode(),
|
||||
toggleCollaborationMode: () => refs.runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
|
||||
requestModel: (model) => refs.runtimeSettings.requestModel(model),
|
||||
resetModelToConfig: () => refs.runtimeSettings.resetModelToConfig(),
|
||||
requestReasoningEffort: (effort) => refs.runtimeSettings.requestReasoningEffort(effort),
|
||||
resetReasoningEffortToConfig: () => refs.runtimeSettings.resetReasoningEffortToConfig(),
|
||||
},
|
||||
goals: {
|
||||
activeGoal: () => refs.goals.activeGoal(),
|
||||
setObjective: (threadId, objective, tokenBudget) => refs.goals.setObjective(threadId, objective, tokenBudget),
|
||||
setStatus: (threadId, goalStatus) => refs.goals.setStatus(threadId, goalStatus),
|
||||
clear: (threadId) => refs.goals.clear(threadId),
|
||||
},
|
||||
status: {
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
addStructuredSystemMessage: status.addStructuredSystemMessage,
|
||||
setStatus: status.set,
|
||||
statusSummaryLines: runtime.statusSummaryLines,
|
||||
connectionDiagnosticDetails: runtime.connectionDiagnosticDetails,
|
||||
mcpStatusLines: runtime.mcpStatusLines,
|
||||
modelStatusLines: runtime.modelStatusLines,
|
||||
effortStatusLines: runtime.effortStatusLines,
|
||||
},
|
||||
codexInput,
|
||||
startNewThread: thread.startNewThread,
|
||||
startThreadForGoal,
|
||||
resumeThread: thread.selectThread,
|
||||
forkThread: (threadId) => refs.threadActions.forkThread(threadId),
|
||||
rollbackThread: (threadId) => refs.threadActions.rollbackThread(threadId),
|
||||
compactThread: (threadId) => refs.threadActions.compactThread(threadId),
|
||||
archiveThread: (threadId) => refs.threadActions.archiveThread(threadId),
|
||||
renameThread: (threadId, name) => refs.threadActions.renameThread(threadId, name).then(() => undefined),
|
||||
reconnect: () => refs.reconnectActions.reconnectPanel(),
|
||||
toggleFastMode: () => refs.runtimeSettings.toggleFastMode(),
|
||||
toggleCollaborationMode: () => refs.runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
|
||||
requestModel: (model) => refs.runtimeSettings.requestModel(model),
|
||||
resetModelToConfig: () => refs.runtimeSettings.resetModelToConfig(),
|
||||
requestReasoningEffort: (effort) => refs.runtimeSettings.requestReasoningEffort(effort),
|
||||
resetReasoningEffortToConfig: () => refs.runtimeSettings.resetReasoningEffortToConfig(),
|
||||
activeGoal: () => refs.goals.activeGoal(),
|
||||
setGoalObjective: (threadId, objective, tokenBudget) => refs.goals.setObjective(threadId, objective, tokenBudget),
|
||||
setGoalStatus: (threadId, goalStatus) => refs.goals.setStatus(threadId, goalStatus),
|
||||
clearGoal: (threadId) => refs.goals.clear(threadId),
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
addStructuredSystemMessage: status.addStructuredSystemMessage,
|
||||
setStatus: status.set,
|
||||
statusSummaryLines: runtime.statusSummaryLines,
|
||||
connectionDiagnosticDetails: runtime.connectionDiagnosticDetails,
|
||||
mcpStatusLines: runtime.mcpStatusLines,
|
||||
modelStatusLines: runtime.modelStatusLines,
|
||||
effortStatusLines: runtime.effortStatusLines,
|
||||
});
|
||||
const planImplementation = createPlanImplementation({
|
||||
stateStore,
|
||||
connection: {
|
||||
currentClient,
|
||||
ensureConnected: client.ensureConnected,
|
||||
},
|
||||
submission: {
|
||||
sendTurnText: (text) => turnSubmission.sendTurnText(text),
|
||||
},
|
||||
runtime: {
|
||||
requestDefaultCollaborationModeForNextTurn: () => {
|
||||
refs.runtimeSettings.requestDefaultCollaborationModeForNextTurn();
|
||||
},
|
||||
currentClient,
|
||||
ensureConnected: client.ensureConnected,
|
||||
sendTurnText: (text) => turnSubmission.sendTurnText(text),
|
||||
requestDefaultCollaborationModeForNextTurn: () => {
|
||||
refs.runtimeSettings.requestDefaultCollaborationModeForNextTurn();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -252,7 +191,7 @@ export function createConversationSurfaceControllerGroup(
|
|||
openTurnDiff: (state) => void plugin.openTurnDiff(state),
|
||||
},
|
||||
requests: {
|
||||
pendingSignature: messageStream.pendingRequestsSignature,
|
||||
pendingSignature: surface.pendingRequestsSignature,
|
||||
pendingSnapshot: () => pendingRequests.snapshot(),
|
||||
pendingActions: () => pendingRequests.actions(),
|
||||
consumePendingAutoFocus: () => pendingRequests.consumeAutoFocus(),
|
||||
|
|
|
|||
|
|
@ -7,41 +7,29 @@ import type { SlashCommandExecutionResult } from "./slash-command-execution";
|
|||
import type { SlashCommandName } from "../composer/slash-commands";
|
||||
import type { ReferencedThreadDisplay } from "../../../../domain/threads/reference";
|
||||
|
||||
interface ComposerDraftPort {
|
||||
readonly trimmedDraft: string;
|
||||
setDraft(text: string, options?: { clearSuggestions?: boolean; focus?: boolean }): void;
|
||||
}
|
||||
|
||||
interface ComposerSlashCommandPort {
|
||||
execute(command: SlashCommandName, args: string): Promise<SlashCommandExecutionResult | undefined>;
|
||||
}
|
||||
|
||||
interface ComposerTurnSubmissionPort {
|
||||
sendTurnText(text: string, codexInputOverride?: CodexInput, referencedThread?: ReferencedThreadDisplay): Promise<void>;
|
||||
}
|
||||
|
||||
interface ComposerConnectionPort {
|
||||
ensureConnected: () => Promise<void>;
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
interface ComposerStatusPort {
|
||||
setStatus: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
interface ComposerScrollPort {
|
||||
followBottom: () => void;
|
||||
}
|
||||
|
||||
export interface ComposerSubmitActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
composer: ComposerDraftPort;
|
||||
slashCommands: ComposerSlashCommandPort;
|
||||
turnSubmission: ComposerTurnSubmissionPort;
|
||||
connection: ComposerConnectionPort;
|
||||
status: ComposerStatusPort;
|
||||
scroll: ComposerScrollPort;
|
||||
composer: {
|
||||
readonly trimmedDraft: string;
|
||||
setDraft(text: string, options?: { clearSuggestions?: boolean; focus?: boolean }): void;
|
||||
};
|
||||
slashCommands: {
|
||||
execute(command: SlashCommandName, args: string): Promise<SlashCommandExecutionResult | undefined>;
|
||||
};
|
||||
turnSubmission: {
|
||||
sendTurnText(text: string, codexInputOverride?: CodexInput, referencedThread?: ReferencedThreadDisplay): Promise<void>;
|
||||
};
|
||||
connection: {
|
||||
ensureConnected: () => Promise<void>;
|
||||
currentClient: () => AppServerClient | null;
|
||||
};
|
||||
status: {
|
||||
setStatus: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
scroll: {
|
||||
followBottom: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComposerSubmitActions {
|
||||
|
|
|
|||
|
|
@ -5,24 +5,12 @@ import type { DisplayItem } from "../../display/types";
|
|||
|
||||
const IMPLEMENT_PLAN_PROMPT = "Please implement this plan.";
|
||||
|
||||
interface PlanImplementationConnectionPort {
|
||||
currentClient(): AppServerClient | null;
|
||||
ensureConnected(): Promise<void>;
|
||||
}
|
||||
|
||||
interface PlanImplementationSubmissionPort {
|
||||
sendTurnText(text: string): Promise<void>;
|
||||
}
|
||||
|
||||
interface PlanImplementationRuntimePort {
|
||||
requestDefaultCollaborationModeForNextTurn(): void;
|
||||
}
|
||||
|
||||
export interface PlanImplementationHost {
|
||||
stateStore: ChatStateStore;
|
||||
connection: PlanImplementationConnectionPort;
|
||||
submission: PlanImplementationSubmissionPort;
|
||||
runtime: PlanImplementationRuntimePort;
|
||||
currentClient(): AppServerClient | null;
|
||||
ensureConnected(): Promise<void>;
|
||||
sendTurnText(text: string): Promise<void>;
|
||||
requestDefaultCollaborationModeForNextTurn(): void;
|
||||
}
|
||||
|
||||
export interface PlanImplementation {
|
||||
|
|
@ -39,10 +27,10 @@ export function createPlanImplementation(host: PlanImplementationHost): PlanImpl
|
|||
|
||||
async function implementPlan(host: PlanImplementationHost, item: DisplayItem): Promise<void> {
|
||||
if (!canImplementPlan(host.stateStore.getState(), item)) return;
|
||||
await host.connection.ensureConnected();
|
||||
if (!host.connection.currentClient() || !activeThreadId(host.stateStore.getState())) return;
|
||||
await host.ensureConnected();
|
||||
if (!host.currentClient() || !activeThreadId(host.stateStore.getState())) return;
|
||||
|
||||
host.runtime.requestDefaultCollaborationModeForNextTurn();
|
||||
host.requestDefaultCollaborationModeForNextTurn();
|
||||
host.stateStore.dispatch({ type: "ui/panel-set", panel: null });
|
||||
await host.submission.sendTurnText(IMPLEMENT_PLAN_PROMPT);
|
||||
await host.sendTurnText(IMPLEMENT_PLAN_PROMPT);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ import { codexTextInputWithAttachments, type CodexInput } from "../../../../doma
|
|||
import { readReferencedThreadConversationSummaries } from "../../../../app-server/services/threads";
|
||||
import { referencedThreadPromptBundle, REFERENCED_THREAD_TURN_LIMIT } from "../../../../domain/threads/reference";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { ThreadGoal, ThreadGoalStatus } from "../../../../domain/threads/goal";
|
||||
import {
|
||||
executeSlashCommand as runSlashCommand,
|
||||
type SlashCommandExecutionContext,
|
||||
type SlashCommandExecutionResult,
|
||||
type ThreadReferenceInput,
|
||||
} from "./slash-command-execution";
|
||||
import type { SlashCommandName } from "../composer/slash-commands";
|
||||
import type { DisplayDetailSection } from "../../display/types";
|
||||
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import { submissionStateSnapshot } from "../../state/selectors";
|
||||
|
|
@ -18,54 +17,13 @@ import type { ChatStateStore } from "../../state/reducer";
|
|||
import { currentModel, runtimeConfigOrDefault } from "../../runtime/effective";
|
||||
import { runtimeSnapshotForChatState } from "../../runtime/snapshot";
|
||||
|
||||
export interface SlashCommandThreadPort {
|
||||
startNewThread: () => Promise<void>;
|
||||
startThreadForGoal: (objective: string) => Promise<string | null>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
forkThread: (threadId: string) => Promise<void>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
compactThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string) => Promise<void>;
|
||||
renameThread: (threadId: string, name: string) => Promise<void>;
|
||||
reconnect: () => Promise<void>;
|
||||
}
|
||||
type DynamicSlashCommandExecutionContext = "activeThreadId" | "busy" | "listedThreads" | "referThread" | "supportedReasoningEfforts";
|
||||
|
||||
export interface SlashCommandRuntimePort {
|
||||
toggleFastMode: () => void | Promise<void>;
|
||||
toggleCollaborationMode: () => void | Promise<void>;
|
||||
toggleAutoReview: () => void | Promise<void>;
|
||||
requestModel: (model: string) => boolean | undefined | Promise<boolean | undefined>;
|
||||
resetModelToConfig: () => boolean | undefined | Promise<boolean | undefined>;
|
||||
requestReasoningEffort: (effort: ReasoningEffort) => boolean | undefined | Promise<boolean | undefined>;
|
||||
resetReasoningEffortToConfig: () => boolean | undefined | Promise<boolean | undefined>;
|
||||
}
|
||||
|
||||
export interface SlashCommandStatusPort {
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
|
||||
setStatus: (status: string) => void;
|
||||
statusSummaryLines: () => string[];
|
||||
connectionDiagnosticDetails: () => DisplayDetailSection[];
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
}
|
||||
|
||||
export interface SlashCommandGoalPort {
|
||||
activeGoal: () => ThreadGoal | null;
|
||||
setObjective: (threadId: string, objective: string, tokenBudget: number | null) => Promise<boolean>;
|
||||
setStatus: (threadId: string, status: ThreadGoalStatus) => Promise<boolean>;
|
||||
clear: (threadId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface SlashCommandHandlerHost {
|
||||
export interface SlashCommandHandlerHost extends Omit<SlashCommandExecutionContext, DynamicSlashCommandExecutionContext> {
|
||||
stateStore: ChatStateStore;
|
||||
currentClient: () => AppServerClient | null;
|
||||
codexInput: (text: string) => CodexInput;
|
||||
threads: SlashCommandThreadPort;
|
||||
runtime: SlashCommandRuntimePort;
|
||||
goals: SlashCommandGoalPort;
|
||||
status: SlashCommandStatusPort;
|
||||
setStatus: (status: string) => void;
|
||||
}
|
||||
|
||||
export interface SlashCommandHandler {
|
||||
|
|
@ -87,45 +45,15 @@ async function executeSlashCommand(
|
|||
const client = host.currentClient();
|
||||
if (!client && command !== "reconnect" && command !== "compact") return;
|
||||
return runSlashCommand(command, args, {
|
||||
...host,
|
||||
activeThreadId: state.activeThreadId,
|
||||
listedThreads: state.listedThreads,
|
||||
startNewThread: () => host.threads.startNewThread(),
|
||||
startThreadForGoal: (objective) => host.threads.startThreadForGoal(objective),
|
||||
resumeThread: (threadId) => host.threads.resumeThread(threadId),
|
||||
reconnect: () => host.threads.reconnect(),
|
||||
busy: state.busy,
|
||||
referThread: (thread, message) => {
|
||||
if (!client) return Promise.resolve(null);
|
||||
return referencedThreadInput(host, client, thread, message);
|
||||
},
|
||||
forkThread: (threadId) => host.threads.forkThread(threadId),
|
||||
rollbackThread: (threadId) => host.threads.rollbackThread(threadId),
|
||||
compactThread: (threadId) => host.threads.compactThread(threadId),
|
||||
archiveThread: (threadId) => host.threads.archiveThread(threadId),
|
||||
renameThread: (threadId, name) => host.threads.renameThread(threadId, name),
|
||||
busy: state.busy,
|
||||
toggleFastMode: () => host.runtime.toggleFastMode(),
|
||||
toggleCollaborationMode: () => host.runtime.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => host.runtime.toggleAutoReview(),
|
||||
addSystemMessage: (text) => {
|
||||
host.status.addSystemMessage(text);
|
||||
},
|
||||
addStructuredSystemMessage: (text, details) => {
|
||||
host.status.addStructuredSystemMessage(text, details);
|
||||
},
|
||||
requestModel: (model) => host.runtime.requestModel(model),
|
||||
resetModelToConfig: () => host.runtime.resetModelToConfig(),
|
||||
requestReasoningEffort: (effort) => host.runtime.requestReasoningEffort(effort),
|
||||
resetReasoningEffortToConfig: () => host.runtime.resetReasoningEffortToConfig(),
|
||||
supportedReasoningEfforts: () => supportedReasoningEfforts(host.stateStore.getState()),
|
||||
activeGoal: () => host.goals.activeGoal(),
|
||||
setGoalObjective: (threadId, objective, tokenBudget) => host.goals.setObjective(threadId, objective, tokenBudget),
|
||||
setGoalStatus: (threadId, status) => host.goals.setStatus(threadId, status),
|
||||
clearGoal: (threadId) => host.goals.clear(threadId),
|
||||
statusSummaryLines: () => host.status.statusSummaryLines(),
|
||||
connectionDiagnosticDetails: () => host.status.connectionDiagnosticDetails(),
|
||||
mcpStatusLines: () => host.status.mcpStatusLines(),
|
||||
modelStatusLines: () => host.status.modelStatusLines(),
|
||||
effortStatusLines: () => host.status.effortStatusLines(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -144,18 +72,18 @@ async function referencedThreadInput(
|
|||
try {
|
||||
const turns = await readReferencedThreadConversationSummaries(client, thread.id, REFERENCED_THREAD_TURN_LIMIT);
|
||||
if (turns.length === 0) {
|
||||
host.status.addSystemMessage("Referenced thread has no readable conversation turns.");
|
||||
host.addSystemMessage("Referenced thread has no readable conversation turns.");
|
||||
return null;
|
||||
}
|
||||
const reference = referencedThreadPromptBundle(thread, turns, message);
|
||||
const messageInput = host.codexInput(message);
|
||||
host.status.setStatus(reference.status);
|
||||
host.setStatus(reference.status);
|
||||
return {
|
||||
input: codexTextInputWithAttachments(reference.prompt, messageInput),
|
||||
referencedThread: reference.referencedThread,
|
||||
};
|
||||
} catch (error) {
|
||||
host.status.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,59 +11,31 @@ import {
|
|||
shouldAcknowledgeTurnStart,
|
||||
} from "./optimistic-turn-start";
|
||||
|
||||
export interface TurnSubmissionConnectionPort {
|
||||
export interface TurnSubmissionControllerHost {
|
||||
stateStore: ChatStateStore;
|
||||
vaultPath: string;
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionRestoredThreadPort {
|
||||
ensureRestoredThreadLoaded: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionThreadPort {
|
||||
startThread: (preview?: string) => Promise<unknown>;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
resetThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionRuntimePort {
|
||||
applyPendingThreadSettings: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionComposerPort {
|
||||
codexInput: (text: string) => CodexInput;
|
||||
setDraft: (text: string, options?: { focus?: boolean; clearSuggestions?: boolean }) => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionViewPort {
|
||||
render: () => void;
|
||||
scheduleRender: () => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionStatusPort {
|
||||
setStatus: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
export interface TurnSubmissionControllerHost {
|
||||
stateStore: ChatStateStore;
|
||||
connection: TurnSubmissionConnectionPort;
|
||||
restoredThread: TurnSubmissionRestoredThreadPort;
|
||||
thread: TurnSubmissionThreadPort;
|
||||
runtime: TurnSubmissionRuntimePort;
|
||||
composer: TurnSubmissionComposerPort;
|
||||
view: TurnSubmissionViewPort;
|
||||
status: TurnSubmissionStatusPort;
|
||||
}
|
||||
|
||||
export class TurnSubmissionController {
|
||||
private static localItemSequence = 0;
|
||||
|
||||
constructor(private readonly host: TurnSubmissionControllerHost) {}
|
||||
|
||||
async sendTurnText(text: string, codexInputOverride?: CodexInput, referencedThread?: ReferencedThreadDisplay): Promise<void> {
|
||||
if (!(await this.host.restoredThread.ensureRestoredThreadLoaded())) return;
|
||||
const client = this.host.connection.currentClient();
|
||||
if (!(await this.host.ensureRestoredThreadLoaded())) return;
|
||||
const client = this.host.currentClient();
|
||||
if (!client) return;
|
||||
|
||||
const initialState = submissionStateSnapshot(this.host.stateStore.getState());
|
||||
|
|
@ -75,16 +47,16 @@ export class TurnSubmissionController {
|
|||
let optimisticUserId: string | null = null;
|
||||
try {
|
||||
if (!initialState.activeThreadId) {
|
||||
const threadResponse = await this.host.thread.startThread(text);
|
||||
const threadResponse = await this.host.startThread(text);
|
||||
if (!threadResponse) return;
|
||||
this.host.thread.notifyActiveThreadIdentityChanged();
|
||||
this.host.thread.resetThreadTurnPresence(false);
|
||||
this.host.notifyActiveThreadIdentityChanged();
|
||||
this.host.resetThreadTurnPresence(false);
|
||||
}
|
||||
const activeThreadId = submissionStateSnapshot(this.host.stateStore.getState()).activeThreadId;
|
||||
if (!activeThreadId) return;
|
||||
if (!(await this.host.runtime.applyPendingThreadSettings())) return;
|
||||
if (!(await this.host.applyPendingThreadSettings())) return;
|
||||
|
||||
const codexInput = codexInputOverride ?? this.host.composer.codexInput(text);
|
||||
const codexInput = codexInputOverride ?? this.host.codexInput(text);
|
||||
optimisticUserId = TurnSubmissionController.nextLocalItemId("local-user");
|
||||
const optimistic = optimisticTurnStart({
|
||||
id: optimisticUserId,
|
||||
|
|
@ -97,12 +69,12 @@ export class TurnSubmissionController {
|
|||
item: optimistic.item,
|
||||
pendingTurnStart: optimistic.pendingTurnStart,
|
||||
});
|
||||
this.host.composer.setDraft("");
|
||||
this.host.view.render();
|
||||
this.host.setDraft("");
|
||||
this.host.render();
|
||||
|
||||
const response = await client.startTurn({
|
||||
threadId: activeThreadId,
|
||||
cwd: this.host.connection.vaultPath,
|
||||
cwd: this.host.vaultPath,
|
||||
input: codexInput,
|
||||
clientUserMessageId: optimisticUserId,
|
||||
});
|
||||
|
|
@ -125,7 +97,7 @@ export class TurnSubmissionController {
|
|||
pendingTurnStart: pendingStart,
|
||||
});
|
||||
this.host.stateStore.dispatch({ type: "turn/start-acknowledged", turnId: response.turn.id, displayItems });
|
||||
this.host.status.setStatus("Turn running...");
|
||||
this.host.setStatus("Turn running...");
|
||||
}
|
||||
} catch (error) {
|
||||
const failedState = submissionStateSnapshot(this.host.stateStore.getState());
|
||||
|
|
@ -136,11 +108,11 @@ export class TurnSubmissionController {
|
|||
pendingTurnStart: failedState.pendingTurnStart,
|
||||
});
|
||||
this.host.stateStore.dispatch({ type: "turn/start-failed", displayItems });
|
||||
this.host.composer.setDraft(text);
|
||||
this.host.status.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.host.setDraft(text);
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
this.host.view.scheduleRender();
|
||||
this.host.scheduleRender();
|
||||
}
|
||||
|
||||
private async steerCurrentTurn(
|
||||
|
|
@ -153,13 +125,13 @@ export class TurnSubmissionController {
|
|||
const threadId = state.activeThreadId;
|
||||
const expectedTurnId = state.activeTurnId;
|
||||
if (!threadId || !expectedTurnId) {
|
||||
this.host.status.addSystemMessage("Current turn is not steerable yet.");
|
||||
this.host.addSystemMessage("Current turn is not steerable yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
const codexInput = codexInputOverride ?? this.host.composer.codexInput(text);
|
||||
const codexInput = codexInputOverride ?? this.host.codexInput(text);
|
||||
const localSteerId = TurnSubmissionController.nextLocalItemId("local-steer");
|
||||
this.host.composer.setDraft("", { clearSuggestions: true });
|
||||
this.host.setDraft("", { clearSuggestions: true });
|
||||
|
||||
try {
|
||||
await client.steerTurn(threadId, expectedTurnId, codexInput, localSteerId);
|
||||
|
|
@ -174,14 +146,14 @@ export class TurnSubmissionController {
|
|||
codexInput,
|
||||
}),
|
||||
});
|
||||
this.host.status.setStatus("Steered current turn.");
|
||||
this.host.setStatus("Steered current turn.");
|
||||
} catch (error) {
|
||||
if (!this.isCurrentTurn(threadId, expectedTurnId)) return;
|
||||
this.host.composer.setDraft(text, { focus: true });
|
||||
this.host.status.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.host.setDraft(text, { focus: true });
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
this.host.view.scheduleRender();
|
||||
this.host.scheduleRender();
|
||||
}
|
||||
|
||||
private isCurrentTurn(threadId: string, turnId: string): boolean {
|
||||
|
|
|
|||
|
|
@ -31,59 +31,64 @@ export type RestoredThreadLifecycleEvent =
|
|||
| { type: "loading-finished"; loading: Promise<void> }
|
||||
| { type: "cleared" };
|
||||
|
||||
export class ChatViewDeferredTasks {
|
||||
private readonly restoredThreadHydrationTask: DeferredTask;
|
||||
private readonly renderTask: DeferredTask;
|
||||
private readonly diagnosticsTask: DeferredTask;
|
||||
private readonly appServerWarmupTask: DeferredTask;
|
||||
export interface ChatViewDeferredTasks {
|
||||
scheduleRender(callback: () => void): void;
|
||||
clearRender(): void;
|
||||
scheduleDiagnostics(callback: () => void): void;
|
||||
clearDiagnostics(): void;
|
||||
scheduleRestoredThreadHydration(callback: () => void): void;
|
||||
clearRestoredThreadHydration(): void;
|
||||
scheduleAppServerWarmup(callback: () => void): void;
|
||||
clearAppServerWarmup(): void;
|
||||
clearAll(): void;
|
||||
}
|
||||
|
||||
constructor(getWindow: () => DeferredTaskWindow) {
|
||||
this.renderTask = new DeferredTask(getWindow, 50);
|
||||
this.diagnosticsTask = new DeferredTask(getWindow, 1_000);
|
||||
this.restoredThreadHydrationTask = new DeferredTask(getWindow, 1_500);
|
||||
this.appServerWarmupTask = new DeferredTask(getWindow, 0);
|
||||
}
|
||||
export function createChatViewDeferredTasks(getWindow: () => DeferredTaskWindow): ChatViewDeferredTasks {
|
||||
const renderTask = new DeferredTask(getWindow, 50);
|
||||
const diagnosticsTask = new DeferredTask(getWindow, 1_000);
|
||||
const restoredThreadHydrationTask = new DeferredTask(getWindow, 1_500);
|
||||
const appServerWarmupTask = new DeferredTask(getWindow, 0);
|
||||
|
||||
scheduleRender(callback: () => void): void {
|
||||
this.renderTask.schedule(() => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
return {
|
||||
scheduleRender(callback): void {
|
||||
renderTask.schedule(callback);
|
||||
},
|
||||
|
||||
clearRender(): void {
|
||||
this.renderTask.clear();
|
||||
}
|
||||
clearRender(): void {
|
||||
renderTask.clear();
|
||||
},
|
||||
|
||||
scheduleDiagnostics(callback: () => void): void {
|
||||
this.diagnosticsTask.schedule(callback);
|
||||
}
|
||||
scheduleDiagnostics(callback): void {
|
||||
diagnosticsTask.schedule(callback);
|
||||
},
|
||||
|
||||
clearDiagnostics(): void {
|
||||
this.diagnosticsTask.clear();
|
||||
}
|
||||
clearDiagnostics(): void {
|
||||
diagnosticsTask.clear();
|
||||
},
|
||||
|
||||
scheduleRestoredThreadHydration(callback: () => void): void {
|
||||
this.restoredThreadHydrationTask.schedule(callback);
|
||||
}
|
||||
scheduleRestoredThreadHydration(callback): void {
|
||||
restoredThreadHydrationTask.schedule(callback);
|
||||
},
|
||||
|
||||
clearRestoredThreadHydration(): void {
|
||||
this.restoredThreadHydrationTask.clear();
|
||||
}
|
||||
clearRestoredThreadHydration(): void {
|
||||
restoredThreadHydrationTask.clear();
|
||||
},
|
||||
|
||||
scheduleAppServerWarmup(callback: () => void): void {
|
||||
this.appServerWarmupTask.schedule(callback);
|
||||
}
|
||||
scheduleAppServerWarmup(callback): void {
|
||||
appServerWarmupTask.schedule(callback);
|
||||
},
|
||||
|
||||
clearAppServerWarmup(): void {
|
||||
this.appServerWarmupTask.clear();
|
||||
}
|
||||
clearAppServerWarmup(): void {
|
||||
appServerWarmupTask.clear();
|
||||
},
|
||||
|
||||
clearAll(): void {
|
||||
this.clearRestoredThreadHydration();
|
||||
this.clearAppServerWarmup();
|
||||
this.clearDiagnostics();
|
||||
this.clearRender();
|
||||
}
|
||||
clearAll(): void {
|
||||
restoredThreadHydrationTask.clear();
|
||||
appServerWarmupTask.clear();
|
||||
diagnosticsTask.clear();
|
||||
renderTask.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class ChatConnectionWorkTracker {
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import type { SharedServerMetadata } from "../../../domain/server/metadata";
|
||||
import type { Thread } from "../../../domain/threads/model";
|
||||
import type { ChatServerMetadataActions } from "../connection/server-actions/metadata";
|
||||
import type { ChatServerThreadActions } from "../connection/server-actions/threads";
|
||||
|
||||
export interface CachedSharedAppServerStateSource {
|
||||
cachedThreadList: () => readonly Thread[] | null;
|
||||
cachedAppServerMetadata: () => SharedServerMetadata | null;
|
||||
}
|
||||
|
||||
export function applyCachedSharedAppServerState(
|
||||
source: CachedSharedAppServerStateSource,
|
||||
serverThreads: ChatServerThreadActions,
|
||||
serverMetadata: ChatServerMetadataActions,
|
||||
): void {
|
||||
const threads = source.cachedThreadList();
|
||||
if (threads) serverThreads.applyThreadList(threads);
|
||||
const metadata = source.cachedAppServerMetadata();
|
||||
if (metadata) serverMetadata.applyAppServerMetadata(metadata);
|
||||
}
|
||||
|
|
@ -1,67 +1,49 @@
|
|||
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
|
||||
import type { ComponentChild as UiNode } from "preact";
|
||||
import type { ChatStateStore } from "../state/reducer";
|
||||
import type { CodexPanelSettings } from "../../../settings/model";
|
||||
import type { SharedServerMetadata } from "../../../domain/server/metadata";
|
||||
import type { Thread } from "../../../domain/threads/model";
|
||||
import type { ChatServerMetadataActions } from "../connection/server-actions/metadata";
|
||||
import type { ChatServerThreadActions } from "../connection/server-actions/threads";
|
||||
import type { ChatComposerController } from "../conversation/composer/controller";
|
||||
import type { ChatThreadActions } from "../threads/actions";
|
||||
import { scheduleAppServerWarmup } from "../connection/app-server-warmup";
|
||||
import type { ChatThreadActions } from "../threads/action-context";
|
||||
import { closeChatView, openChatView, type ChatViewLifecycleHost } from "./view-lifecycle";
|
||||
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "./regions/toolbar";
|
||||
import { ChatViewRenderController } from "./view-render-controller";
|
||||
import { createToolbarArchiveConfirmState, createToolbarPanelActions } from "./regions/toolbar";
|
||||
import { applyChatViewState } from "./view-state";
|
||||
import type { MessageStreamRenderer } from "../ui/message-stream/renderer";
|
||||
import { applyCachedSharedAppServerState, type CachedSharedAppServerStateSource } from "./cached-app-server-state";
|
||||
import type { ChatViewDeferredTasks, RestoredThreadState } from "../lifecycle";
|
||||
import { createChatShellRenderPort } from "./shell-render";
|
||||
import type { ChatControllerCompositionPorts } from "../composition-ports";
|
||||
import { renderChatPanelShell } from "../ui/shell";
|
||||
|
||||
interface ViewRenderControllerGroupPorts {
|
||||
plugin: {
|
||||
settings: CodexPanelSettings;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
lifecycle: {
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
};
|
||||
render: {
|
||||
panelRoot: () => HTMLElement | null;
|
||||
toolbarNode: () => UiNode;
|
||||
goalNode: () => UiNode;
|
||||
messageStreamNode: () => UiNode;
|
||||
composerNode: () => UiNode;
|
||||
};
|
||||
export interface CachedSharedAppServerStateSource {
|
||||
cachedThreadList: () => readonly Thread[] | null;
|
||||
cachedAppServerMetadata: () => SharedServerMetadata | null;
|
||||
}
|
||||
|
||||
export function createViewRenderControllerGroup(context: ViewRenderControllerGroupPorts) {
|
||||
type ChatViewRendererPorts = Pick<ChatControllerCompositionPorts, "plugin" | "state" | "lifecycle" | "render">;
|
||||
|
||||
export function createChatViewRenderer(context: ChatViewRendererPorts): () => void {
|
||||
const { plugin, render, lifecycle } = context;
|
||||
const { deferredTasks } = lifecycle;
|
||||
|
||||
return {
|
||||
renderController: new ChatViewRenderController({
|
||||
shell: createChatShellRenderPort(context.state.stateStore, {
|
||||
showToolbar: () => plugin.settings.showToolbar,
|
||||
toolbarNode: context.render.toolbarNode,
|
||||
goalNode: context.render.goalNode,
|
||||
messageStreamNode: context.render.messageStreamNode,
|
||||
composerNode: context.render.composerNode,
|
||||
}),
|
||||
panelRoot: render.panelRoot,
|
||||
clearScheduledRender: () => {
|
||||
deferredTasks.clearRender();
|
||||
},
|
||||
}),
|
||||
return () => {
|
||||
deferredTasks.clearRender();
|
||||
const root = render.panelRoot();
|
||||
if (!root) return;
|
||||
renderChatPanelShell(root, {
|
||||
stateStore: context.state.stateStore,
|
||||
showToolbar: plugin.settings.showToolbar,
|
||||
toolbarNode: context.render.toolbarNode,
|
||||
goalNode: context.render.goalNode,
|
||||
messageStreamNode: context.render.messageStreamNode,
|
||||
composerNode: context.render.composerNode,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
interface ConnectionLifecycleControllerGroupPorts {
|
||||
obsidian: Pick<ChatViewLifecycleHost, "registerEvent" | "registerPointerDown">;
|
||||
type ConnectionLifecycleControllerGroupPorts = Pick<ChatControllerCompositionPorts, "plugin" | "liveState"> & {
|
||||
obsidian: Pick<ChatViewLifecycleHost["events"], "registerEvent" | "registerPointerDown">;
|
||||
plugin: CachedSharedAppServerStateSource;
|
||||
client: {
|
||||
clear: () => void;
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
lifecycle: {
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
|
|
@ -83,7 +65,7 @@ interface ConnectionLifecycleControllerGroupPorts {
|
|||
refresh: () => void;
|
||||
deferRefresh: () => void;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createConnectionLifecycleControllerGroup(
|
||||
context: ConnectionLifecycleControllerGroupPorts,
|
||||
|
|
@ -98,53 +80,54 @@ export function createConnectionLifecycleControllerGroup(
|
|||
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);
|
||||
lifecycle: {
|
||||
setOpened: lifecycle.setOpened,
|
||||
setClosing: lifecycle.setClosing,
|
||||
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
|
||||
invalidateResumeWork: lifecycle.invalidateResumeWork,
|
||||
clearDeferredTasks: () => {
|
||||
deferredTasks.clearAll();
|
||||
},
|
||||
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
|
||||
scheduleDeferredRestoredThreadHydration: lifecycle.scheduleDeferredRestoredThreadHydration,
|
||||
},
|
||||
registerPointerDown: obsidian.registerPointerDown,
|
||||
applyCachedSharedAppServerState: () => {
|
||||
applyCachedSharedAppServerState(plugin, refs.serverThreads, refs.serverMetadata);
|
||||
events: {
|
||||
registerEvent: obsidian.registerEvent,
|
||||
registerComposerNoteIndexInvalidation: (register) => {
|
||||
refs.composerController.registerNoteIndexInvalidation(register);
|
||||
},
|
||||
registerPointerDown: obsidian.registerPointerDown,
|
||||
closeToolbarPanelOnOutsidePointer: render.closeToolbarPanelOnOutsidePointer,
|
||||
},
|
||||
render: render.now,
|
||||
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
|
||||
scheduleDeferredRestoredThreadHydration: lifecycle.scheduleDeferredRestoredThreadHydration,
|
||||
closeToolbarPanelOnOutsidePointer: render.closeToolbarPanelOnOutsidePointer,
|
||||
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
|
||||
invalidateResumeWork: lifecycle.invalidateResumeWork,
|
||||
clearDeferredTasks: () => {
|
||||
deferredTasks.clearAll();
|
||||
render: {
|
||||
panelRoot: render.panelRoot,
|
||||
now: render.now,
|
||||
},
|
||||
panelRoot: render.panelRoot,
|
||||
disposeMessages: () => {
|
||||
refs.messageStreamRenderer.dispose();
|
||||
sharedState: {
|
||||
applyCachedAppServerState: () => {
|
||||
applyCachedSharedAppServerState(plugin, refs.serverThreads, refs.serverMetadata);
|
||||
},
|
||||
},
|
||||
disposeComposer: () => {
|
||||
refs.composerController.dispose();
|
||||
resources: {
|
||||
disposeMessages: () => {
|
||||
refs.messageStreamRenderer.dispose();
|
||||
},
|
||||
disposeComposer: () => {
|
||||
refs.composerController.dispose();
|
||||
},
|
||||
disconnect: () => {
|
||||
refs.connection.disconnect();
|
||||
},
|
||||
clearClient: client.clear,
|
||||
},
|
||||
disconnect: () => {
|
||||
refs.connection.disconnect();
|
||||
liveState: {
|
||||
refresh: liveState.refresh,
|
||||
deferRefresh: liveState.deferRefresh,
|
||||
},
|
||||
clearClient: client.clear,
|
||||
refreshLiveState: liveState.refresh,
|
||||
deferRefreshLiveState: liveState.deferRefresh,
|
||||
};
|
||||
|
||||
return {
|
||||
scheduleAppServerWarmup: () => {
|
||||
scheduleAppServerWarmup(warmupHost);
|
||||
},
|
||||
openView: () => {
|
||||
openChatView(viewLifecycleHost);
|
||||
},
|
||||
|
|
@ -154,10 +137,7 @@ export function createConnectionLifecycleControllerGroup(
|
|||
};
|
||||
}
|
||||
|
||||
interface PanelUiControllerGroupPorts {
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
type PanelUiControllerGroupPorts = Pick<ChatControllerCompositionPorts, "state"> & {
|
||||
lifecycle: {
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredRestoredThreadHydration: () => void;
|
||||
|
|
@ -170,7 +150,7 @@ interface PanelUiControllerGroupPorts {
|
|||
clearRestoredLifecycle: () => void;
|
||||
restorePlaceholder: (restoredThread: RestoredThreadState) => void;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createPanelUiControllerGroup(
|
||||
context: PanelUiControllerGroupPorts,
|
||||
|
|
@ -188,7 +168,7 @@ export function createPanelUiControllerGroup(
|
|||
restoreThreadPlaceholder: thread.restorePlaceholder,
|
||||
};
|
||||
|
||||
const toolbarPanels = new ToolbarPanelController({
|
||||
const toolbarPanels = createToolbarPanelActions({
|
||||
stateStore: context.state.stateStore,
|
||||
threadActions: refs.threadActions,
|
||||
archiveConfirm: createToolbarArchiveConfirmState(),
|
||||
|
|
@ -200,3 +180,14 @@ export function createPanelUiControllerGroup(
|
|||
|
||||
return { toolbarPanels, applyViewState };
|
||||
}
|
||||
|
||||
function applyCachedSharedAppServerState(
|
||||
source: CachedSharedAppServerStateSource,
|
||||
serverThreads: ChatServerThreadActions,
|
||||
serverMetadata: ChatServerMetadataActions,
|
||||
): void {
|
||||
const threads = source.cachedThreadList();
|
||||
if (threads) serverThreads.applyThreadList(threads);
|
||||
const metadata = source.cachedAppServerMetadata();
|
||||
if (metadata) serverMetadata.applyAppServerMetadata(metadata);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { h } from "preact";
|
||||
|
||||
import {
|
||||
autoReviewActive,
|
||||
currentModel,
|
||||
|
|
@ -18,6 +21,7 @@ import type {
|
|||
ComposerMetaViewModel,
|
||||
RuntimeChoice,
|
||||
} from "../../ui/composer";
|
||||
import { useChatPanelShellState } from "../../ui/shell";
|
||||
import { explicitThreadName } from "../../../../domain/threads/model";
|
||||
import type { ChatPanelComposerPorts, RestoredThreadTitleSnapshot } from "./ports";
|
||||
|
||||
|
|
@ -33,6 +37,23 @@ export function composerPlaceholder(threadName: string | null): string {
|
|||
return threadName ? `Ask Codex to work on “${threadName}”...` : "Ask Codex to work on this task...";
|
||||
}
|
||||
|
||||
export function chatPanelComposerRegionNode(node: () => UiNode): UiNode {
|
||||
return h(ComposerRegion, { node });
|
||||
}
|
||||
|
||||
function ComposerRegion({ node }: { node: () => UiNode }): UiNode {
|
||||
const { connection, threadList, activeThread, runtime, turn, messageStream, composer, renderVersion } = useChatPanelShellState();
|
||||
void connection.value;
|
||||
void threadList.value;
|
||||
void activeThread.value;
|
||||
void runtime.value;
|
||||
void turn.value;
|
||||
void messageStream.value;
|
||||
void composer.value;
|
||||
void renderVersion.value;
|
||||
return node();
|
||||
}
|
||||
|
||||
export function chatPanelComposerPlaceholder(ports: ChatPanelComposerPorts): string {
|
||||
return composerPlaceholder(activeComposerThreadName(ports.state.chat(), ports.thread.restoredPlaceholder()));
|
||||
}
|
||||
|
|
|
|||
178
src/features/chat/panel/regions/composition.ts
Normal file
178
src/features/chat/panel/regions/composition.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { CodexPanelSettings } from "../../../../settings/model";
|
||||
import type { RuntimeSnapshot } from "../../runtime/snapshot";
|
||||
import type { ChatViewControllers } from "../../composition";
|
||||
import { type ChatAction, type ChatState, type ChatStateStore, chatTurnBusy } from "../../state/reducer";
|
||||
import type { RestoredThreadTitleSnapshot, ChatPanelRegionPorts } from "./ports";
|
||||
|
||||
export interface ChatPanelRegionHost {
|
||||
settings: CodexPanelSettings;
|
||||
vaultPath: string;
|
||||
stateStore: ChatStateStore;
|
||||
runtimeSnapshot: () => RuntimeSnapshot;
|
||||
restoredThreadPlaceholder: () => RestoredThreadTitleSnapshot | null;
|
||||
messageStreamNode: () => UiNode;
|
||||
startNewThread: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function createChatPanelRegionPorts(host: ChatPanelRegionHost, controllers: ChatViewControllers): ChatPanelRegionPorts {
|
||||
const state = {
|
||||
chat: () => host.stateStore.getState(),
|
||||
};
|
||||
const dispatch = (action: ChatAction): void => {
|
||||
host.stateStore.dispatch(action);
|
||||
};
|
||||
const render = (): void => {
|
||||
controllers.render.now();
|
||||
};
|
||||
const setGoalEditingOpen = (open: boolean, { closeToolbarPanel = false }: { closeToolbarPanel?: boolean } = {}): void => {
|
||||
if (closeToolbarPanel) dispatch({ type: "ui/panel-set", panel: null });
|
||||
dispatch({ type: "ui/detail-open-set", key: "goal:editor", open });
|
||||
render();
|
||||
};
|
||||
|
||||
return {
|
||||
toolbar: {
|
||||
state: {
|
||||
...state,
|
||||
connected: () => controllers.connection.manager.isConnected(),
|
||||
turnBusy: () => chatTurnBusy(host.stateStore.getState()),
|
||||
},
|
||||
settings: {
|
||||
vaultPath: () => host.vaultPath,
|
||||
configuredCommand: () => host.settings.codexPath,
|
||||
archiveExportEnabled: () => host.settings.archiveExportEnabled,
|
||||
},
|
||||
runtime: {
|
||||
snapshot: host.runtimeSnapshot,
|
||||
},
|
||||
view: {
|
||||
toolbar: {
|
||||
archiveConfirmId: () => controllers.toolbar.panels.archiveConfirmId(),
|
||||
archiveConfirmSubscribe: (listener) => controllers.toolbar.panels.onArchiveConfirmChange(listener),
|
||||
renameState: (threadId) => controllers.thread.rename.editState(threadId),
|
||||
renameSubscribe: (listener) => controllers.thread.rename.subscribe(listener),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toolbar: {
|
||||
startNewThread: () => {
|
||||
void host.startNewThread();
|
||||
},
|
||||
toggleChatActions: () => {
|
||||
controllers.toolbar.panels.toggleChatActions();
|
||||
},
|
||||
compactConversation: () => {
|
||||
void compactConversation(host.stateStore.getState(), controllers);
|
||||
},
|
||||
setGoal: () => {
|
||||
setGoalEditingOpen(true, { closeToolbarPanel: true });
|
||||
},
|
||||
toggleHistory: () => {
|
||||
controllers.toolbar.panels.toggleHistory();
|
||||
},
|
||||
toggleStatusPanel: () => {
|
||||
controllers.toolbar.panels.toggleStatus();
|
||||
},
|
||||
connect: () => {
|
||||
void controllers.connection.reconnect.reconnectPanel();
|
||||
},
|
||||
refreshStatus: () => {
|
||||
void controllers.connection.controller.refreshStatusPanel();
|
||||
},
|
||||
resumeThread: (threadId) => {
|
||||
void controllers.thread.selection.selectThreadFromToolbar(threadId);
|
||||
},
|
||||
startArchiveThread: (threadId) => {
|
||||
controllers.toolbar.panels.startArchive(threadId);
|
||||
},
|
||||
archiveThread: (threadId, saveMarkdown) => {
|
||||
void controllers.toolbar.panels.archiveThread(threadId, saveMarkdown);
|
||||
},
|
||||
startRenameThread: (threadId) => {
|
||||
controllers.thread.rename.start(threadId);
|
||||
},
|
||||
updateRenameDraft: (threadId, value) => {
|
||||
controllers.thread.rename.updateDraft(threadId, value);
|
||||
},
|
||||
saveRenameThread: (threadId, value) => {
|
||||
void controllers.thread.rename.save(threadId, value);
|
||||
},
|
||||
cancelRenameThread: (threadId) => {
|
||||
controllers.thread.rename.cancel(threadId);
|
||||
},
|
||||
autoNameThread: (threadId) => {
|
||||
void controllers.thread.rename.autoNameDraft(threadId);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
goal: {
|
||||
state,
|
||||
settings: {
|
||||
sendShortcut: () => host.settings.sendShortcut,
|
||||
},
|
||||
actions: {
|
||||
goal: {
|
||||
saveObjective: (objective, tokenBudget) => saveGoalObjective(host.stateStore.getState(), controllers, objective, tokenBudget),
|
||||
setStatus: (threadId, status) => controllers.runtime.goals.setStatus(threadId, status),
|
||||
clear: (threadId) => controllers.runtime.goals.clear(threadId),
|
||||
setEditingOpen: (open) => {
|
||||
setGoalEditingOpen(open);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
messageStream: {
|
||||
state,
|
||||
render: {
|
||||
node: host.messageStreamNode,
|
||||
},
|
||||
},
|
||||
composer: {
|
||||
state,
|
||||
thread: {
|
||||
restoredPlaceholder: host.restoredThreadPlaceholder,
|
||||
},
|
||||
runtime: {
|
||||
snapshot: host.runtimeSnapshot,
|
||||
requestModel: (model) => controllers.runtime.settings.requestModelFromUi(model),
|
||||
requestReasoningEffort: (effort) => controllers.runtime.settings.requestReasoningEffortFromUi(effort),
|
||||
resetReasoningEffortToConfig: () => controllers.runtime.settings.resetReasoningEffortToConfigFromUi(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function compactConversation(state: ChatState, controllers: ChatViewControllers): Promise<void> {
|
||||
const threadId = state.activeThread.id;
|
||||
if (!threadId) {
|
||||
controllers.inbound.controller.addSystemMessage("No active thread to compact.");
|
||||
controllers.render.now();
|
||||
return;
|
||||
}
|
||||
await controllers.thread.actions.compactThread(threadId);
|
||||
}
|
||||
|
||||
async function saveGoalObjective(
|
||||
state: ChatState,
|
||||
controllers: ChatViewControllers,
|
||||
objective: string,
|
||||
tokenBudget: number | null,
|
||||
): Promise<void> {
|
||||
let threadId = state.activeThread.id;
|
||||
if (!threadId) {
|
||||
try {
|
||||
await controllers.connection.controller.ensureConnected();
|
||||
const response = await controllers.serverActions.threads.startThread(objective, { syncGoal: false });
|
||||
threadId = response?.threadId ?? null;
|
||||
} catch (error) {
|
||||
controllers.inbound.controller.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
controllers.render.now();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!threadId) return;
|
||||
void controllers.runtime.goals.setObjective(threadId, objective, tokenBudget);
|
||||
}
|
||||
|
|
@ -1,6 +1,29 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { h } from "preact";
|
||||
import { useComputed } from "@preact/signals";
|
||||
|
||||
import type { GoalRegionActions, GoalRegionOptions } from "../../ui/goal";
|
||||
import { goalRegionNode } from "../../ui/goal";
|
||||
import { useChatPanelShellState } from "../../ui/shell";
|
||||
import type { ChatPanelGoalPorts } from "./ports";
|
||||
|
||||
export function chatPanelGoalRegionNode(ports: ChatPanelGoalPorts): UiNode {
|
||||
return h(GoalRegion, { ports });
|
||||
}
|
||||
|
||||
function GoalRegion({ ports }: { ports: ChatPanelGoalPorts }): UiNode {
|
||||
const { activeThread, ui, renderVersion, latestState } = useChatPanelShellState();
|
||||
const props = useComputed(() => {
|
||||
void renderVersion.value;
|
||||
return chatPanelGoalProps(ports, {
|
||||
...latestState(),
|
||||
activeThread: activeThread.value,
|
||||
ui: ui.value,
|
||||
});
|
||||
});
|
||||
return goalRegionNode(props.value.goal, props.value.actions, props.value.options);
|
||||
}
|
||||
|
||||
export function chatPanelGoalProps(
|
||||
ports: ChatPanelGoalPorts,
|
||||
state: ReturnType<ChatPanelGoalPorts["state"]["chat"]>,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { h } from "preact";
|
||||
|
||||
import { pendingRequestsSignature as requestStateSignature } from "../../conversation/pending-requests/signatures";
|
||||
import { useChatPanelShellState } from "../../ui/shell";
|
||||
import type { ChatPanelMessageStreamPorts } from "./ports";
|
||||
|
||||
export function chatPanelMessageStreamNode(ports: ChatPanelMessageStreamPorts) {
|
||||
export function chatPanelMessageStreamRegionNode(ports: ChatPanelMessageStreamPorts): UiNode {
|
||||
return h(MessageStreamRegion, { ports });
|
||||
}
|
||||
|
||||
function MessageStreamRegion({ ports }: { ports: ChatPanelMessageStreamPorts }): UiNode {
|
||||
const { activeThread, runtime, messageStream, requests, turn, ui, renderVersion } = useChatPanelShellState();
|
||||
void activeThread.value;
|
||||
void runtime.value;
|
||||
void messageStream.value;
|
||||
void requests.value;
|
||||
void turn.value;
|
||||
void ui.value;
|
||||
void renderVersion.value;
|
||||
return chatPanelMessageStreamNode(ports);
|
||||
}
|
||||
|
||||
function chatPanelMessageStreamNode(ports: ChatPanelMessageStreamPorts) {
|
||||
return ports.render.node();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,3 +81,10 @@ export interface ChatPanelComposerPorts extends ChatPanelStatePort {
|
|||
resetReasoningEffortToConfig: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatPanelRegionPorts {
|
||||
toolbar: ChatPanelToolbarPorts;
|
||||
goal: ChatPanelGoalPorts;
|
||||
messageStream: ChatPanelMessageStreamPorts;
|
||||
composer: ChatPanelComposerPorts;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { useComputed } from "@preact/signals";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { goalRegionNode } from "../../ui/goal";
|
||||
import { useChatPanelShellState } from "../../ui/shell";
|
||||
import { toolbarNode } from "../../ui/toolbar";
|
||||
import { chatPanelGoalProps } from "./goal";
|
||||
import { chatPanelMessageStreamNode } from "./message-stream";
|
||||
import type { ChatPanelGoalPorts, ChatPanelMessageStreamPorts, ChatPanelToolbarPorts } from "./ports";
|
||||
import { chatPanelToolbarViewModel } from "./toolbar";
|
||||
|
||||
export function chatPanelToolbarRegionNode(ports: ChatPanelToolbarPorts): UiNode {
|
||||
return <ToolbarRegion ports={ports} />;
|
||||
}
|
||||
|
||||
export function chatPanelGoalRegionNode(ports: ChatPanelGoalPorts): UiNode {
|
||||
return <GoalRegion ports={ports} />;
|
||||
}
|
||||
|
||||
export function chatPanelMessageStreamRegionNode(ports: ChatPanelMessageStreamPorts): UiNode {
|
||||
return <MessageStreamRegion ports={ports} />;
|
||||
}
|
||||
|
||||
export function chatPanelComposerRegionNode(node: () => UiNode): UiNode {
|
||||
return <ComposerRegion node={node} />;
|
||||
}
|
||||
|
||||
function ToolbarRegion({ ports }: { ports: ChatPanelToolbarPorts }): UiNode {
|
||||
const shellState = useChatPanelShellState();
|
||||
useToolbarArchiveConfirmSubscription(ports);
|
||||
useToolbarRenameSubscription(ports);
|
||||
void shellState.renderVersion.value;
|
||||
return toolbarNode(chatPanelToolbarViewModel(ports, shellState), ports.actions.toolbar);
|
||||
}
|
||||
|
||||
function GoalRegion({ ports }: { ports: ChatPanelGoalPorts }): UiNode {
|
||||
const { activeThread, ui, renderVersion, latestState } = useChatPanelShellState();
|
||||
const props = useComputed(() => {
|
||||
void renderVersion.value;
|
||||
return chatPanelGoalProps(ports, {
|
||||
...latestState(),
|
||||
activeThread: activeThread.value,
|
||||
ui: ui.value,
|
||||
});
|
||||
});
|
||||
return goalRegionNode(props.value.goal, props.value.actions, props.value.options);
|
||||
}
|
||||
|
||||
function MessageStreamRegion({ ports }: { ports: ChatPanelMessageStreamPorts }): UiNode {
|
||||
const { activeThread, runtime, messageStream, requests, turn, ui, renderVersion } = useChatPanelShellState();
|
||||
void activeThread.value;
|
||||
void runtime.value;
|
||||
void messageStream.value;
|
||||
void requests.value;
|
||||
void turn.value;
|
||||
void ui.value;
|
||||
void renderVersion.value;
|
||||
return chatPanelMessageStreamNode(ports);
|
||||
}
|
||||
|
||||
function ComposerRegion({ node }: { node: () => UiNode }): UiNode {
|
||||
const { connection, threadList, activeThread, runtime, turn, messageStream, composer, renderVersion } = useChatPanelShellState();
|
||||
void connection.value;
|
||||
void threadList.value;
|
||||
void activeThread.value;
|
||||
void runtime.value;
|
||||
void turn.value;
|
||||
void messageStream.value;
|
||||
void composer.value;
|
||||
void renderVersion.value;
|
||||
return node();
|
||||
}
|
||||
|
||||
function useToolbarArchiveConfirmSubscription(ports: ChatPanelToolbarPorts): void {
|
||||
const [, setVersion] = useState(0);
|
||||
useEffect(
|
||||
() =>
|
||||
ports.view.toolbar.archiveConfirmSubscribe(() => {
|
||||
setVersion((version) => version + 1);
|
||||
}),
|
||||
[ports],
|
||||
);
|
||||
}
|
||||
|
||||
function useToolbarRenameSubscription(ports: ChatPanelToolbarPorts): void {
|
||||
const [, setVersion] = useState(0);
|
||||
useEffect(
|
||||
() =>
|
||||
ports.view.toolbar.renameSubscribe(() => {
|
||||
setVersion((version) => version + 1);
|
||||
}),
|
||||
[ports],
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { h } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import { getThreadTitle } from "../../../../domain/threads/model";
|
||||
import type { ChatThreadActions } from "../../threads/actions";
|
||||
import type { ChatThreadActions } from "../../threads/action-context";
|
||||
import { runtimeConfigSections, rateLimitSummary } from "../../display/status/runtime";
|
||||
import { connectionDiagnosticSections } from "../../display/status/diagnostics";
|
||||
import type { RuntimeSnapshot } from "../../runtime/snapshot";
|
||||
import type { ChatAction, ChatState, ChatStateStore } from "../../state/reducer";
|
||||
import type { ChatPanelShellState } from "../../ui/shell";
|
||||
import type { ToolbarThreadRow, ToolbarViewModel } from "../../ui/toolbar";
|
||||
import { useChatPanelShellState, type ChatPanelShellState } from "../../ui/shell";
|
||||
import { toolbarNode, type ToolbarThreadRow, type ToolbarViewModel } from "../../ui/toolbar";
|
||||
import type { ChatPanelToolbarPorts } from "./ports";
|
||||
|
||||
export interface ToolbarViewModelInput {
|
||||
|
|
@ -27,7 +31,7 @@ export interface ConnectionDiagnosticsModelInput {
|
|||
configuredCommand: string;
|
||||
}
|
||||
|
||||
export interface ToolbarPanelControllerHost {
|
||||
export interface ToolbarPanelActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
threadActions: ChatThreadActions;
|
||||
archiveConfirm: ToolbarArchiveConfirmState;
|
||||
|
|
@ -40,7 +44,20 @@ export interface ToolbarArchiveConfirmState {
|
|||
subscribe: (listener: () => void) => () => void;
|
||||
}
|
||||
|
||||
export interface ToolbarOutsidePointerContext {
|
||||
export interface ToolbarPanelActions {
|
||||
archiveConfirmId(): string | null;
|
||||
onArchiveConfirmChange(listener: () => void): () => void;
|
||||
toggleHistory(): void;
|
||||
toggleChatActions(): void;
|
||||
closeToolbarPanels(): void;
|
||||
toggleStatus(): void;
|
||||
closeForThreadSelection(): void;
|
||||
startArchive(threadId: string): void;
|
||||
archiveThread(threadId: string, saveMarkdown: boolean): Promise<void>;
|
||||
closeOnOutsidePointer(context: ToolbarOutsidePointerContext): void;
|
||||
}
|
||||
|
||||
interface ToolbarOutsidePointerContext {
|
||||
target: EventTarget | null;
|
||||
viewWindow: ToolbarDomWindow | null;
|
||||
contains: (element: Element) => boolean;
|
||||
|
|
@ -49,7 +66,7 @@ export interface ToolbarOutsidePointerContext {
|
|||
|
||||
type ToolbarDomWindow = Window & { Element: typeof Element };
|
||||
|
||||
export function chatPanelToolbarViewModel(ports: ChatPanelToolbarPorts, shellState: ChatPanelShellState) {
|
||||
function chatPanelToolbarViewModel(ports: ChatPanelToolbarPorts, shellState: ChatPanelShellState) {
|
||||
const latestState = shellState.latestState();
|
||||
return toolbarViewModel({
|
||||
state: {
|
||||
|
|
@ -72,6 +89,40 @@ export function chatPanelToolbarViewModel(ports: ChatPanelToolbarPorts, shellSta
|
|||
});
|
||||
}
|
||||
|
||||
export function chatPanelToolbarRegionNode(ports: ChatPanelToolbarPorts): UiNode {
|
||||
return h(ToolbarRegion, { ports });
|
||||
}
|
||||
|
||||
function ToolbarRegion({ ports }: { ports: ChatPanelToolbarPorts }): UiNode {
|
||||
const shellState = useChatPanelShellState();
|
||||
useToolbarArchiveConfirmSubscription(ports);
|
||||
useToolbarRenameSubscription(ports);
|
||||
void shellState.renderVersion.value;
|
||||
return toolbarNode(chatPanelToolbarViewModel(ports, shellState), ports.actions.toolbar);
|
||||
}
|
||||
|
||||
function useToolbarArchiveConfirmSubscription(ports: ChatPanelToolbarPorts): void {
|
||||
const [, setVersion] = useState(0);
|
||||
useEffect(
|
||||
() =>
|
||||
ports.view.toolbar.archiveConfirmSubscribe(() => {
|
||||
setVersion((version) => version + 1);
|
||||
}),
|
||||
[ports],
|
||||
);
|
||||
}
|
||||
|
||||
function useToolbarRenameSubscription(ports: ChatPanelToolbarPorts): void {
|
||||
const [, setVersion] = useState(0);
|
||||
useEffect(
|
||||
() =>
|
||||
ports.view.toolbar.renameSubscribe(() => {
|
||||
setVersion((version) => version + 1);
|
||||
}),
|
||||
[ports],
|
||||
);
|
||||
}
|
||||
|
||||
export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel {
|
||||
const { state, snapshot } = input;
|
||||
const limit = rateLimitSummary(snapshot, Date.now());
|
||||
|
|
@ -137,92 +188,85 @@ export function connectionDiagnosticsModel(input: ConnectionDiagnosticsModelInpu
|
|||
});
|
||||
}
|
||||
|
||||
export class ToolbarPanelController {
|
||||
constructor(private readonly host: ToolbarPanelControllerHost) {}
|
||||
export function createToolbarPanelActions(host: ToolbarPanelActionsHost): ToolbarPanelActions {
|
||||
const state = (): ChatState => host.stateStore.getState();
|
||||
const dispatch = (action: ChatAction): void => {
|
||||
host.stateStore.dispatch(action);
|
||||
};
|
||||
const hasOpenPanel = (): boolean => state().ui.toolbarPanel !== null;
|
||||
const close = (): void => {
|
||||
if (!hasOpenPanel()) return;
|
||||
|
||||
private get state(): ChatState {
|
||||
return this.host.stateStore.getState();
|
||||
}
|
||||
dispatch({ type: "ui/panel-set", panel: null });
|
||||
host.archiveConfirm.set(null);
|
||||
host.scheduleRender();
|
||||
};
|
||||
|
||||
private dispatch(action: ChatAction): void {
|
||||
this.host.stateStore.dispatch(action);
|
||||
}
|
||||
return {
|
||||
archiveConfirmId(): string | null {
|
||||
return host.archiveConfirm.get();
|
||||
},
|
||||
|
||||
archiveConfirmId(): string | null {
|
||||
return this.host.archiveConfirm.get();
|
||||
}
|
||||
onArchiveConfirmChange(listener: () => void): () => void {
|
||||
return host.archiveConfirm.subscribe(listener);
|
||||
},
|
||||
|
||||
onArchiveConfirmChange(listener: () => void): () => void {
|
||||
return this.host.archiveConfirm.subscribe(listener);
|
||||
}
|
||||
toggleHistory(): void {
|
||||
dispatch({ type: "ui/panel-set", panel: "history", toggle: true });
|
||||
host.scheduleRender();
|
||||
},
|
||||
|
||||
toggleHistory(): void {
|
||||
this.dispatch({ type: "ui/panel-set", panel: "history", toggle: true });
|
||||
this.host.scheduleRender();
|
||||
}
|
||||
toggleChatActions(): void {
|
||||
dispatch({ type: "ui/panel-set", panel: "chat-actions", toggle: true });
|
||||
host.scheduleRender();
|
||||
},
|
||||
|
||||
toggleChatActions(): void {
|
||||
this.dispatch({ type: "ui/panel-set", panel: "chat-actions", toggle: true });
|
||||
this.host.scheduleRender();
|
||||
}
|
||||
closeToolbarPanels(): void {
|
||||
close();
|
||||
},
|
||||
|
||||
closeToolbarPanels(): void {
|
||||
this.close();
|
||||
}
|
||||
toggleStatus(): void {
|
||||
dispatch({ type: "ui/panel-set", panel: "status-panel", toggle: true });
|
||||
host.scheduleRender();
|
||||
},
|
||||
|
||||
toggleStatus(): void {
|
||||
this.dispatch({ type: "ui/panel-set", panel: "status-panel", toggle: true });
|
||||
this.host.scheduleRender();
|
||||
}
|
||||
closeForThreadSelection(): void {
|
||||
host.archiveConfirm.set(null);
|
||||
},
|
||||
|
||||
closeForThreadSelection(): void {
|
||||
this.host.archiveConfirm.set(null);
|
||||
}
|
||||
startArchive(threadId: string): void {
|
||||
host.archiveConfirm.set(threadId);
|
||||
},
|
||||
|
||||
startArchive(threadId: string): void {
|
||||
this.host.archiveConfirm.set(threadId);
|
||||
}
|
||||
async archiveThread(threadId: string, saveMarkdown: boolean): Promise<void> {
|
||||
if (host.archiveConfirm.get() === threadId) host.archiveConfirm.set(null);
|
||||
await host.threadActions.archiveThread(threadId, saveMarkdown);
|
||||
host.scheduleRender();
|
||||
},
|
||||
|
||||
async archiveThread(threadId: string, saveMarkdown: boolean): Promise<void> {
|
||||
if (this.host.archiveConfirm.get() === threadId) this.host.archiveConfirm.set(null);
|
||||
await this.host.threadActions.archiveThread(threadId, saveMarkdown);
|
||||
this.host.scheduleRender();
|
||||
}
|
||||
closeOnOutsidePointer(context: ToolbarOutsidePointerContext): void {
|
||||
if (!hasOpenPanel()) return;
|
||||
|
||||
closeOnOutsidePointer(context: ToolbarOutsidePointerContext): void {
|
||||
if (!this.hasOpenPanel()) return;
|
||||
|
||||
const target = context.target;
|
||||
if (isToolbarElement(target, context.viewWindow)) {
|
||||
const insideToolbarPanel = target.closest(".codex-panel__toolbar-primary, .codex-panel__toolbar-panel");
|
||||
if (insideToolbarPanel && context.contains(insideToolbarPanel)) {
|
||||
if (this.host.archiveConfirm.get() && !target.closest(".codex-panel__archive-confirm")) {
|
||||
this.host.archiveConfirm.set(null);
|
||||
const target = context.target;
|
||||
if (isToolbarElement(target, context.viewWindow)) {
|
||||
const insideToolbarPanel = target.closest(".codex-panel__toolbar-primary, .codex-panel__toolbar-panel");
|
||||
if (insideToolbarPanel && context.contains(insideToolbarPanel)) {
|
||||
if (host.archiveConfirm.get() && !target.closest(".codex-panel__archive-confirm")) {
|
||||
host.archiveConfirm.set(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.host.archiveConfirm.get()) {
|
||||
this.host.archiveConfirm.set(null);
|
||||
}
|
||||
if (host.archiveConfirm.get()) {
|
||||
host.archiveConfirm.set(null);
|
||||
}
|
||||
|
||||
if (context.renameEditing) return;
|
||||
if (context.renameEditing) return;
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
private hasOpenPanel(): boolean {
|
||||
return this.state.ui.toolbarPanel !== null;
|
||||
}
|
||||
|
||||
private close(): void {
|
||||
if (!this.hasOpenPanel()) return;
|
||||
|
||||
this.dispatch({ type: "ui/panel-set", panel: null });
|
||||
this.host.archiveConfirm.set(null);
|
||||
this.host.scheduleRender();
|
||||
}
|
||||
close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isToolbarElement(target: EventTarget | null, viewWindow: ToolbarDomWindow | null): target is Element {
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { ChatStateStore } from "../state/reducer";
|
||||
import { renderChatPanelShell } from "../ui/shell";
|
||||
|
||||
export interface ChatShellRenderPort {
|
||||
render(root: HTMLElement): void;
|
||||
}
|
||||
|
||||
export function createChatShellRenderPort(
|
||||
stateStore: ChatStateStore,
|
||||
options: {
|
||||
showToolbar: () => boolean;
|
||||
toolbarNode: () => UiNode;
|
||||
goalNode: () => UiNode;
|
||||
messageStreamNode: () => UiNode;
|
||||
composerNode: () => UiNode;
|
||||
},
|
||||
): ChatShellRenderPort {
|
||||
return {
|
||||
render(root) {
|
||||
renderChatPanelShell(root, {
|
||||
stateStore,
|
||||
showToolbar: options.showToolbar(),
|
||||
toolbarNode: options.toolbarNode,
|
||||
goalNode: options.goalNode,
|
||||
messageStreamNode: options.messageStreamNode,
|
||||
composerNode: options.composerNode,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -3,55 +3,67 @@ import type { EventRef } from "obsidian";
|
|||
import { unmountChatPanelShell } from "../ui/shell";
|
||||
|
||||
export interface ChatViewLifecycleHost {
|
||||
setOpened: (opened: boolean) => void;
|
||||
setClosing: (closing: boolean) => void;
|
||||
registerEvent: (eventRef: EventRef) => void;
|
||||
registerComposerNoteIndexInvalidation: (register: (eventRef: EventRef) => void) => void;
|
||||
registerPointerDown: (handler: (event: PointerEvent) => void) => void;
|
||||
applyCachedSharedAppServerState: () => void;
|
||||
render: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredTasks: () => void;
|
||||
panelRoot: () => HTMLElement | null;
|
||||
disposeMessages: () => void;
|
||||
disposeComposer: () => void;
|
||||
disconnect: () => void;
|
||||
clearClient: () => void;
|
||||
refreshLiveState: () => void;
|
||||
deferRefreshLiveState: () => void;
|
||||
lifecycle: {
|
||||
setOpened: (opened: boolean) => void;
|
||||
setClosing: (closing: boolean) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredTasks: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
};
|
||||
events: {
|
||||
registerEvent: (eventRef: EventRef) => void;
|
||||
registerComposerNoteIndexInvalidation: (register: (eventRef: EventRef) => void) => void;
|
||||
registerPointerDown: (handler: (event: PointerEvent) => void) => void;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
};
|
||||
render: {
|
||||
panelRoot: () => HTMLElement | null;
|
||||
now: () => void;
|
||||
};
|
||||
sharedState: {
|
||||
applyCachedAppServerState: () => void;
|
||||
};
|
||||
resources: {
|
||||
disposeMessages: () => void;
|
||||
disposeComposer: () => void;
|
||||
disconnect: () => void;
|
||||
clearClient: () => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
deferRefresh: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function openChatView(host: ChatViewLifecycleHost): void {
|
||||
host.setOpened(true);
|
||||
host.setClosing(false);
|
||||
host.registerComposerNoteIndexInvalidation((eventRef) => {
|
||||
host.registerEvent(eventRef);
|
||||
host.lifecycle.setOpened(true);
|
||||
host.lifecycle.setClosing(false);
|
||||
host.events.registerComposerNoteIndexInvalidation((eventRef) => {
|
||||
host.events.registerEvent(eventRef);
|
||||
});
|
||||
host.registerPointerDown((event) => {
|
||||
host.closeToolbarPanelOnOutsidePointer(event);
|
||||
host.events.registerPointerDown((event) => {
|
||||
host.events.closeToolbarPanelOnOutsidePointer(event);
|
||||
});
|
||||
host.applyCachedSharedAppServerState();
|
||||
host.render();
|
||||
host.scheduleDeferredAppServerWarmup();
|
||||
host.scheduleDeferredRestoredThreadHydration();
|
||||
host.sharedState.applyCachedAppServerState();
|
||||
host.render.now();
|
||||
host.lifecycle.scheduleDeferredAppServerWarmup();
|
||||
host.lifecycle.scheduleDeferredRestoredThreadHydration();
|
||||
}
|
||||
|
||||
export function closeChatView(host: ChatViewLifecycleHost): void {
|
||||
host.setOpened(false);
|
||||
host.setClosing(true);
|
||||
host.invalidateConnectionWork();
|
||||
host.invalidateResumeWork();
|
||||
host.clearDeferredTasks();
|
||||
const panelRoot = host.panelRoot();
|
||||
host.disposeMessages();
|
||||
host.disposeComposer();
|
||||
host.lifecycle.setOpened(false);
|
||||
host.lifecycle.setClosing(true);
|
||||
host.lifecycle.invalidateConnectionWork();
|
||||
host.lifecycle.invalidateResumeWork();
|
||||
host.lifecycle.clearDeferredTasks();
|
||||
const panelRoot = host.render.panelRoot();
|
||||
host.resources.disposeMessages();
|
||||
host.resources.disposeComposer();
|
||||
unmountChatPanelShell(panelRoot);
|
||||
host.disconnect();
|
||||
host.clearClient();
|
||||
host.refreshLiveState();
|
||||
host.deferRefreshLiveState();
|
||||
host.resources.disconnect();
|
||||
host.resources.clearClient();
|
||||
host.liveState.refresh();
|
||||
host.liveState.deferRefresh();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import type { ChatShellRenderPort } from "./shell-render";
|
||||
|
||||
export interface ChatViewRenderControllerHost {
|
||||
shell: ChatShellRenderPort;
|
||||
panelRoot: () => HTMLElement | null;
|
||||
clearScheduledRender: () => void;
|
||||
}
|
||||
|
||||
export class ChatViewRenderController {
|
||||
constructor(private readonly host: ChatViewRenderControllerHost) {}
|
||||
|
||||
render(): void {
|
||||
this.host.clearScheduledRender();
|
||||
const root = this.host.panelRoot();
|
||||
if (!root) return;
|
||||
this.host.shell.render(root);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,15 @@ export interface ChatThreadActionsHost {
|
|||
refreshSharedThreadListFromOpenSurface: () => void;
|
||||
}
|
||||
|
||||
export interface ChatThreadActions {
|
||||
compactThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string, saveMarkdown?: boolean) => Promise<void>;
|
||||
forkThread: (threadId: string) => Promise<void>;
|
||||
forkThreadFromTurn: (threadId: string, turnId: string | null, archiveSource: boolean) => Promise<void>;
|
||||
renameThread: (threadId: string, name: string) => Promise<boolean>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function threadActionState(host: ChatThreadActionsHost): ChatState {
|
||||
return host.stateStore.getState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { archiveThread } from "./archive-actions";
|
||||
import { compactThread } from "./compact-actions";
|
||||
import type { ChatThreadActionsHost } from "./action-context";
|
||||
import { forkThread, forkThreadFromTurn } from "./fork-actions";
|
||||
import { renameThread } from "./rename-actions";
|
||||
import { rollbackThread } from "./rollback-actions";
|
||||
|
||||
export type { ChatThreadActionsHost } from "./action-context";
|
||||
|
||||
export interface ChatThreadActions {
|
||||
compactThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string, saveMarkdown?: boolean) => Promise<void>;
|
||||
forkThread: (threadId: string) => Promise<void>;
|
||||
forkThreadFromTurn: (threadId: string, turnId: string | null, archiveSource: boolean) => Promise<void>;
|
||||
renameThread: (threadId: string, name: string) => Promise<boolean>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createChatThreadActions(host: ChatThreadActionsHost): ChatThreadActions {
|
||||
return {
|
||||
compactThread: (threadId) => compactThread(host, threadId),
|
||||
archiveThread: (threadId, saveMarkdown) => archiveThread(host, threadId, saveMarkdown),
|
||||
forkThread: (threadId) => forkThread(host, threadId),
|
||||
forkThreadFromTurn: (threadId, turnId, archiveSource) => forkThreadFromTurn(host, threadId, turnId, archiveSource),
|
||||
renameThread: (threadId, name) => renameThread(host, threadId, name),
|
||||
rollbackThread: (threadId) => rollbackThread(host, threadId),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,37 +1,28 @@
|
|||
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import { recoverRolloutTokenUsage } from "../../../app-server/services/rollout-token-usage";
|
||||
import type { ArchiveExportAdapter } from "../../thread-export/archive-markdown";
|
||||
import { createChatThreadActions } from "./actions";
|
||||
import { archiveThread } from "./archive-actions";
|
||||
import { AutoTitleController } from "./auto-title-controller";
|
||||
import { compactThread } from "./compact-actions";
|
||||
import { forkThread, forkThreadFromTurn } from "./fork-actions";
|
||||
import { createGoalActions } from "./goal-actions";
|
||||
import { HistoryController } from "./history-controller";
|
||||
import { createIdentitySync } from "./identity-sync";
|
||||
import { RenameController } from "./rename-controller";
|
||||
import { renameThread } from "./rename-actions";
|
||||
import { ResumeController } from "./resume-controller";
|
||||
import { rollbackThread } from "./rollback-actions";
|
||||
import { createSelectionActions } from "./selection-actions";
|
||||
import { RestorationController } from "./restoration-controller";
|
||||
import type { ChatStateStore } from "../state/reducer";
|
||||
import type { ChatThreadActions, ChatThreadActionsHost } from "./action-context";
|
||||
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle";
|
||||
import type { CodexPanelSettings } from "../../../settings/model";
|
||||
import type { ChatControllerCompositionPorts } from "../composition-ports";
|
||||
|
||||
interface ThreadControllerGroupPorts {
|
||||
obsidian: {
|
||||
archiveAdapter: () => ArchiveExportAdapter;
|
||||
};
|
||||
plugin: {
|
||||
notifyThreadArchived: (threadId: string) => void;
|
||||
notifyThreadRenamed: (threadId: string, name: string | null) => void;
|
||||
openThreadInNewView: (threadId: string) => Promise<unknown>;
|
||||
refreshSharedThreadListFromOpenSurface: () => void;
|
||||
settings: CodexPanelSettings;
|
||||
vaultPath: string;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
type ThreadControllerGroupPorts = Pick<
|
||||
ChatControllerCompositionPorts,
|
||||
"obsidian" | "plugin" | "state" | "lifecycle" | "thread" | "liveState"
|
||||
> & {
|
||||
client: {
|
||||
getClient: () => AppServerClient | null;
|
||||
getClient: ChatControllerCompositionPorts["client"]["getClient"];
|
||||
ensureConnected: () => Promise<void>;
|
||||
};
|
||||
lifecycle: {
|
||||
|
|
@ -39,7 +30,6 @@ interface ThreadControllerGroupPorts {
|
|||
resumeWork: ChatResumeWorkTracker;
|
||||
getOpened: () => boolean;
|
||||
getClosing: () => boolean;
|
||||
clearDeferredRestoredThreadHydration: () => void;
|
||||
};
|
||||
thread: {
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
|
|
@ -51,20 +41,14 @@ interface ThreadControllerGroupPorts {
|
|||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
};
|
||||
scroll: {
|
||||
preservePosition: () => void;
|
||||
forceBottom: () => void;
|
||||
};
|
||||
scroll: Pick<ChatControllerCompositionPorts["scroll"], "preservePosition" | "forceBottom">;
|
||||
render: {
|
||||
now: () => void;
|
||||
};
|
||||
composer: {
|
||||
setText: (text: string) => void;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createThreadControllerGroup(
|
||||
context: ThreadControllerGroupPorts,
|
||||
|
|
@ -111,7 +95,7 @@ export function createThreadControllerGroup(
|
|||
resumeWork.invalidate();
|
||||
history.invalidate();
|
||||
};
|
||||
const actions = createChatThreadActions({
|
||||
const threadActionHost: ChatThreadActionsHost = {
|
||||
stateStore,
|
||||
vaultPath: plugin.vaultPath,
|
||||
settings: () => plugin.settings,
|
||||
|
|
@ -133,7 +117,15 @@ export function createThreadControllerGroup(
|
|||
refreshSharedThreadListFromOpenSurface: () => {
|
||||
plugin.refreshSharedThreadListFromOpenSurface();
|
||||
},
|
||||
});
|
||||
};
|
||||
const actions: ChatThreadActions = {
|
||||
compactThread: (threadId) => compactThread(threadActionHost, threadId),
|
||||
archiveThread: (threadId, saveMarkdown) => archiveThread(threadActionHost, threadId, saveMarkdown),
|
||||
forkThread: (threadId) => forkThread(threadActionHost, threadId),
|
||||
forkThreadFromTurn: (threadId, turnId, archiveSource) => forkThreadFromTurn(threadActionHost, threadId, turnId, archiveSource),
|
||||
renameThread: (threadId, name) => renameThread(threadActionHost, threadId, name),
|
||||
rollbackThread: (threadId) => rollbackThread(threadActionHost, threadId),
|
||||
};
|
||||
const goals = createGoalActions({
|
||||
stateStore,
|
||||
currentClient,
|
||||
|
|
@ -165,7 +157,9 @@ export function createThreadControllerGroup(
|
|||
ensureConnected: client.ensureConnected,
|
||||
closing: lifecycle.getClosing,
|
||||
resetThreadTurnPresence,
|
||||
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
|
||||
clearDeferredRestoredThreadHydration: () => {
|
||||
restoration.clearHydration();
|
||||
},
|
||||
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
render: render.now,
|
||||
|
|
@ -181,7 +175,9 @@ export function createThreadControllerGroup(
|
|||
stateStore,
|
||||
restoration,
|
||||
invalidateResumeWork,
|
||||
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
|
||||
clearDeferredRestoredThreadHydration: () => {
|
||||
restoration.clearHydration();
|
||||
},
|
||||
resetThreadTurnPresence,
|
||||
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
|
||||
refreshTabHeader: thread.refreshTabHeader,
|
||||
|
|
@ -207,20 +203,14 @@ function requireThreadController<T>(controller: T | null, name: string): T {
|
|||
return controller;
|
||||
}
|
||||
|
||||
interface ThreadSelectionActionGroupPorts {
|
||||
plugin: {
|
||||
focusThreadInOpenView: (threadId: string) => Promise<boolean>;
|
||||
};
|
||||
state: {
|
||||
stateStore: ChatStateStore;
|
||||
};
|
||||
type ThreadSelectionActionGroupPorts = Pick<ChatControllerCompositionPorts, "plugin" | "state"> & {
|
||||
status: {
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
thread: {
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function createThreadSelectionActionGroup(
|
||||
context: ThreadSelectionActionGroupPorts,
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
import type { MessageStreamScrollIntent } from "./virtualizer";
|
||||
|
||||
export class ChatMessageScrollIntentController {
|
||||
private nextIntent: MessageStreamScrollIntent = "auto";
|
||||
|
||||
consumeIntent(): MessageStreamScrollIntent {
|
||||
const value = this.nextIntent;
|
||||
this.nextIntent = "auto";
|
||||
return value;
|
||||
}
|
||||
|
||||
forceBottom(): void {
|
||||
this.nextIntent = "force-bottom";
|
||||
}
|
||||
|
||||
followBottom(): void {
|
||||
this.nextIntent = "follow-bottom";
|
||||
}
|
||||
|
||||
preservePosition(): void {
|
||||
this.nextIntent = "preserve";
|
||||
}
|
||||
}
|
||||
32
src/features/chat/ui/message-stream/scroll-intent-state.ts
Normal file
32
src/features/chat/ui/message-stream/scroll-intent-state.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { MessageStreamScrollIntent } from "./virtualizer";
|
||||
|
||||
export interface ChatMessageScrollIntentState {
|
||||
consumeIntent(): MessageStreamScrollIntent;
|
||||
forceBottom(): void;
|
||||
followBottom(): void;
|
||||
preservePosition(): void;
|
||||
}
|
||||
|
||||
export function createChatMessageScrollIntentState(): ChatMessageScrollIntentState {
|
||||
let nextIntent: MessageStreamScrollIntent = "auto";
|
||||
|
||||
return {
|
||||
consumeIntent(): MessageStreamScrollIntent {
|
||||
const value = nextIntent;
|
||||
nextIntent = "auto";
|
||||
return value;
|
||||
},
|
||||
|
||||
forceBottom(): void {
|
||||
nextIntent = "force-bottom";
|
||||
},
|
||||
|
||||
followBottom(): void {
|
||||
nextIntent = "follow-bottom";
|
||||
},
|
||||
|
||||
preservePosition(): void {
|
||||
nextIntent = "preserve";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -3,12 +3,11 @@ import { ItemView, type ViewStateResult, type WorkspaceLeaf } from "obsidian";
|
|||
import type { AppServerClient } from "../../app-server/connection/client";
|
||||
import { VIEW_TYPE_CODEX_PANEL } from "../../constants";
|
||||
import type { DisplayDetailSection, DisplayItem } from "./display/types";
|
||||
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import type { ModelMetadata } from "../../domain/catalog/metadata";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import type { RuntimeSnapshot } from "./runtime/snapshot";
|
||||
import { collaborationModeLabel as formatCollaborationModeLabel } from "./runtime/pending-settings";
|
||||
import { chatTurnBusy, createChatStateStore, type ChatState, type ChatAction } from "./state/reducer";
|
||||
import { chatTurnBusy, createChatStateStore, type ChatAction, type ChatState } from "./state/reducer";
|
||||
import type { OpenCodexPanelSnapshot } from "../../workspace/open-panel-snapshot";
|
||||
import type { SharedServerMetadata } from "../../domain/server/metadata";
|
||||
import type { CodexChatHost } from "./chat-host";
|
||||
|
|
@ -22,23 +21,16 @@ import { runtimeSnapshotForChatState } from "./runtime/snapshot";
|
|||
import { codexPanelDisplayTitle, getThreadTitle } from "../../domain/threads/model";
|
||||
import { connectionDiagnosticsModel } from "./panel/regions/toolbar";
|
||||
import { openPanelTurnLifecycle } from "./panel/snapshot";
|
||||
import { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "./lifecycle";
|
||||
import { ChatMessageScrollIntentController } from "./ui/message-stream/scroll-intent-controller";
|
||||
import { ChatConnectionWorkTracker, ChatResumeWorkTracker, createChatViewDeferredTasks, type ChatViewDeferredTasks } from "./lifecycle";
|
||||
import { createChatMessageScrollIntentState, type ChatMessageScrollIntentState } from "./ui/message-stream/scroll-intent-state";
|
||||
import type { ChatControllerCompositionPorts } from "./composition-ports";
|
||||
import { createChatViewControllers, type ChatViewControllers } from "./composition";
|
||||
import type { ChatPanelComposerPorts, ChatPanelGoalPorts, ChatPanelMessageStreamPorts, ChatPanelToolbarPorts } from "./panel/regions/ports";
|
||||
import { chatPanelComposerMetaViewModel, chatPanelComposerPlaceholder } from "./panel/regions/composer";
|
||||
import { chatPanelMessageStreamPendingRequestsSignature } from "./panel/regions/message-stream";
|
||||
import {
|
||||
chatPanelComposerRegionNode,
|
||||
chatPanelGoalRegionNode,
|
||||
chatPanelMessageStreamRegionNode,
|
||||
chatPanelToolbarRegionNode,
|
||||
} from "./panel/regions/render";
|
||||
|
||||
type ChatPanelToolbarActions = ChatPanelToolbarPorts["actions"]["toolbar"];
|
||||
type ChatPanelToolbarState = ChatPanelToolbarPorts["view"]["toolbar"];
|
||||
type ChatPanelGoalActions = ChatPanelGoalPorts["actions"]["goal"];
|
||||
import type { ChatPanelRegionPorts } from "./panel/regions/ports";
|
||||
import { createChatPanelRegionPorts } from "./panel/regions/composition";
|
||||
import { chatPanelComposerMetaViewModel, chatPanelComposerPlaceholder, chatPanelComposerRegionNode } from "./panel/regions/composer";
|
||||
import { chatPanelGoalRegionNode } from "./panel/regions/goal";
|
||||
import { chatPanelMessageStreamPendingRequestsSignature, chatPanelMessageStreamRegionNode } from "./panel/regions/message-stream";
|
||||
import { chatPanelToolbarRegionNode } from "./panel/regions/toolbar";
|
||||
|
||||
export class CodexChatView extends ItemView {
|
||||
private client: AppServerClient | null = null;
|
||||
|
|
@ -46,11 +38,8 @@ export class CodexChatView extends ItemView {
|
|||
private readonly chatState = createChatStateStore();
|
||||
private readonly viewId = `codex-panel-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
private readonly deferredTasks: ChatViewDeferredTasks;
|
||||
private readonly messageScrollIntent: ChatMessageScrollIntentController;
|
||||
private readonly toolbarPorts: ChatPanelToolbarPorts;
|
||||
private readonly goalPorts: ChatPanelGoalPorts;
|
||||
private readonly messageStreamPorts: ChatPanelMessageStreamPorts;
|
||||
private readonly composerPorts: ChatPanelComposerPorts;
|
||||
private readonly messageScrollIntent: ChatMessageScrollIntentState;
|
||||
private readonly panelPorts: ChatPanelRegionPorts;
|
||||
private readonly connectionWork = new ChatConnectionWorkTracker();
|
||||
private readonly resumeWork: ChatResumeWorkTracker;
|
||||
private opened = false;
|
||||
|
|
@ -61,18 +50,29 @@ export class CodexChatView extends ItemView {
|
|||
private readonly plugin: CodexChatHost,
|
||||
) {
|
||||
super(leaf);
|
||||
this.deferredTasks = new ChatViewDeferredTasks(() => this.containerEl.win);
|
||||
this.deferredTasks = createChatViewDeferredTasks(() => this.containerEl.win);
|
||||
this.resumeWork = new ChatResumeWorkTracker();
|
||||
this.messageScrollIntent = new ChatMessageScrollIntentController();
|
||||
this.messageScrollIntent = createChatMessageScrollIntentState();
|
||||
this.controllers = createChatViewControllers(this.createControllerPorts());
|
||||
const panelPorts = this.createPanelRegionPorts(this.controllers);
|
||||
this.toolbarPorts = panelPorts.toolbar;
|
||||
this.goalPorts = panelPorts.goal;
|
||||
this.messageStreamPorts = panelPorts.messageStream;
|
||||
this.composerPorts = panelPorts.composer;
|
||||
this.panelPorts = createChatPanelRegionPorts(
|
||||
{
|
||||
settings: this.plugin.settings,
|
||||
vaultPath: this.plugin.vaultPath,
|
||||
stateStore: this.chatState,
|
||||
runtimeSnapshot: () => this.runtimeSnapshot(),
|
||||
restoredThreadPlaceholder: () => this.restoredThreadPlaceholder(),
|
||||
messageStreamNode: () => this.controllers.render.messageStream.renderNode(),
|
||||
startNewThread: () => this.startNewThread(),
|
||||
},
|
||||
this.controllers,
|
||||
);
|
||||
}
|
||||
|
||||
private createControllerPorts(): ChatControllerCompositionPorts {
|
||||
const refreshThreadsViewLiveState = () => {
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
};
|
||||
|
||||
return {
|
||||
obsidian: {
|
||||
app: this.app,
|
||||
|
|
@ -86,39 +86,9 @@ export class CodexChatView extends ItemView {
|
|||
},
|
||||
archiveAdapter: () => this.app.vault.adapter,
|
||||
},
|
||||
plugin: {
|
||||
settings: this.plugin.settings,
|
||||
vaultPath: this.plugin.vaultPath,
|
||||
openThreadInNewView: (threadId) => this.plugin.openThreadInNewView(threadId),
|
||||
focusThreadInOpenView: (threadId) => this.plugin.focusThreadInOpenView(threadId),
|
||||
openTurnDiff: (state) => this.plugin.openTurnDiff(state),
|
||||
notifyThreadArchived: (threadId) => {
|
||||
this.plugin.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
this.plugin.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
refreshThreadsViewLiveState: () => {
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
},
|
||||
refreshSharedThreadListFromOpenSurface: () => {
|
||||
this.plugin.refreshSharedThreadListFromOpenSurface();
|
||||
},
|
||||
applyThreadListSnapshot: (threads) => {
|
||||
this.plugin.applyThreadListSnapshot(threads);
|
||||
},
|
||||
publishAppServerMetadata: (metadata) => {
|
||||
this.plugin.publishAppServerMetadata(metadata);
|
||||
},
|
||||
publishAppServerIdentity: (userAgent) => {
|
||||
this.plugin.publishAppServerIdentity(userAgent);
|
||||
},
|
||||
cachedThreadList: () => this.plugin.cachedThreadList(),
|
||||
cachedAppServerMetadata: () => this.plugin.cachedAppServerMetadata(),
|
||||
},
|
||||
plugin: this.plugin,
|
||||
state: {
|
||||
stateStore: this.chatState,
|
||||
getState: () => this.state,
|
||||
systemItem: (text) => this.systemItem(text),
|
||||
structuredSystemItem: (text, details) => this.structuredSystemItem(text, details),
|
||||
},
|
||||
|
|
@ -144,30 +114,13 @@ export class CodexChatView extends ItemView {
|
|||
setClosing: (closing) => {
|
||||
this.closing = closing;
|
||||
},
|
||||
invalidateConnectionWork: () => {
|
||||
this.connectionWork.invalidate();
|
||||
},
|
||||
scheduleDeferredDiagnostics: () => {
|
||||
this.scheduleDeferredDiagnostics();
|
||||
},
|
||||
clearDeferredDiagnostics: () => {
|
||||
this.clearDeferredDiagnostics();
|
||||
},
|
||||
scheduleDeferredRestoredThreadHydration: () => {
|
||||
this.scheduleDeferredRestoredThreadHydration();
|
||||
},
|
||||
clearDeferredRestoredThreadHydration: () => {
|
||||
this.clearDeferredRestoredThreadHydration();
|
||||
},
|
||||
scheduleDeferredAppServerWarmup: () => {
|
||||
this.scheduleDeferredAppServerWarmup();
|
||||
},
|
||||
refreshDeferredDiagnostics: () => this.refreshDeferredDiagnostics(),
|
||||
},
|
||||
render: {
|
||||
panelRoot: () => this.panelRoot(),
|
||||
toolbarNode: () => chatPanelToolbarRegionNode(this.toolbarPorts),
|
||||
goalNode: () => chatPanelGoalRegionNode(this.goalPorts),
|
||||
messageStreamNode: () => chatPanelMessageStreamRegionNode(this.messageStreamPorts),
|
||||
toolbarNode: () => chatPanelToolbarRegionNode(this.panelPorts.toolbar),
|
||||
goalNode: () => chatPanelGoalRegionNode(this.panelPorts.goal),
|
||||
messageStreamNode: () => chatPanelMessageStreamRegionNode(this.panelPorts.messageStream),
|
||||
composerNode: () => chatPanelComposerRegionNode(() => this.controllers.composer.controller.renderNode()),
|
||||
closeToolbarPanelOnOutsidePointer: (event) => {
|
||||
this.closeToolbarPanelOnOutsidePointer(event);
|
||||
|
|
@ -176,15 +129,12 @@ export class CodexChatView extends ItemView {
|
|||
this.scheduleRender();
|
||||
},
|
||||
},
|
||||
messageStream: {
|
||||
pendingRequestsSignature: () => chatPanelMessageStreamPendingRequestsSignature(this.messageStreamPorts),
|
||||
},
|
||||
composerView: {
|
||||
composerPlaceholder: () => chatPanelComposerPlaceholder(this.composerPorts),
|
||||
composerMetaViewModel: () => chatPanelComposerMetaViewModel(this.composerPorts),
|
||||
surface: {
|
||||
pendingRequestsSignature: () => chatPanelMessageStreamPendingRequestsSignature(this.panelPorts.messageStream),
|
||||
composerPlaceholder: () => chatPanelComposerPlaceholder(this.panelPorts.composer),
|
||||
composerMetaViewModel: () => chatPanelComposerMetaViewModel(this.panelPorts.composer),
|
||||
},
|
||||
runtime: {
|
||||
runtimeSnapshotForState: (state) => this.runtimeSnapshotForState(state),
|
||||
collaborationModeLabel: () => this.collaborationModeLabel(),
|
||||
connectionDiagnosticDetails: () => this.connectionDiagnosticDetails(),
|
||||
modelStatusLines: () => this.modelStatusLines(),
|
||||
|
|
@ -203,12 +153,10 @@ export class CodexChatView extends ItemView {
|
|||
},
|
||||
},
|
||||
liveState: {
|
||||
refresh: () => {
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
},
|
||||
refresh: refreshThreadsViewLiveState,
|
||||
deferRefresh: () => {
|
||||
this.containerEl.win.setTimeout(() => {
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
refreshThreadsViewLiveState();
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
|
|
@ -231,181 +179,10 @@ export class CodexChatView extends ItemView {
|
|||
};
|
||||
}
|
||||
|
||||
private createPanelRegionPorts(controllers: ChatViewControllers): {
|
||||
toolbar: ChatPanelToolbarPorts;
|
||||
goal: ChatPanelGoalPorts;
|
||||
messageStream: ChatPanelMessageStreamPorts;
|
||||
composer: ChatPanelComposerPorts;
|
||||
} {
|
||||
const state = {
|
||||
chat: () => this.state,
|
||||
};
|
||||
return {
|
||||
toolbar: {
|
||||
state: {
|
||||
...state,
|
||||
connected: () => controllers.connection.manager.isConnected(),
|
||||
turnBusy: () => this.turnBusy,
|
||||
},
|
||||
settings: {
|
||||
vaultPath: () => this.plugin.vaultPath,
|
||||
configuredCommand: () => this.plugin.settings.codexPath,
|
||||
archiveExportEnabled: () => this.plugin.settings.archiveExportEnabled,
|
||||
},
|
||||
runtime: {
|
||||
snapshot: () => this.runtimeSnapshot(),
|
||||
},
|
||||
view: {
|
||||
toolbar: this.createToolbarPanelState(controllers),
|
||||
},
|
||||
actions: {
|
||||
toolbar: this.createToolbarPanelActions(controllers),
|
||||
},
|
||||
},
|
||||
goal: {
|
||||
state,
|
||||
settings: {
|
||||
sendShortcut: () => this.plugin.settings.sendShortcut,
|
||||
},
|
||||
actions: {
|
||||
goal: this.createGoalPanelActions(controllers),
|
||||
},
|
||||
},
|
||||
messageStream: {
|
||||
state,
|
||||
render: {
|
||||
node: () => this.controllers.render.messageStream.renderNode(),
|
||||
},
|
||||
},
|
||||
composer: {
|
||||
state,
|
||||
thread: {
|
||||
restoredPlaceholder: () => this.restoredThreadPlaceholder(),
|
||||
},
|
||||
runtime: {
|
||||
snapshot: () => this.runtimeSnapshot(),
|
||||
requestModel: (model) => this.requestModelFromUi(model),
|
||||
requestReasoningEffort: (effort) => this.requestReasoningEffortFromUi(effort),
|
||||
resetReasoningEffortToConfig: () => this.resetReasoningEffortToConfigFromUi(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createToolbarPanelState(controllers: ChatViewControllers): ChatPanelToolbarState {
|
||||
return {
|
||||
archiveConfirmId: () => controllers.toolbar.panels.archiveConfirmId(),
|
||||
archiveConfirmSubscribe: (listener) => controllers.toolbar.panels.onArchiveConfirmChange(listener),
|
||||
renameState: (threadId) => controllers.thread.rename.editState(threadId),
|
||||
renameSubscribe: (listener) => controllers.thread.rename.subscribe(listener),
|
||||
};
|
||||
}
|
||||
|
||||
private createToolbarPanelActions(controllers: ChatViewControllers): ChatPanelToolbarActions {
|
||||
return {
|
||||
startNewThread: () => {
|
||||
void this.startNewThread();
|
||||
},
|
||||
toggleChatActions: () => {
|
||||
controllers.toolbar.panels.toggleChatActions();
|
||||
},
|
||||
compactConversation: () => {
|
||||
void this.compactConversation();
|
||||
},
|
||||
setGoal: () => {
|
||||
this.setGoalEditingOpen(true, { closeToolbarPanel: true });
|
||||
},
|
||||
toggleHistory: () => {
|
||||
controllers.toolbar.panels.toggleHistory();
|
||||
},
|
||||
toggleStatusPanel: () => {
|
||||
controllers.toolbar.panels.toggleStatus();
|
||||
},
|
||||
connect: () => {
|
||||
void controllers.connection.reconnect.reconnectPanel();
|
||||
},
|
||||
refreshStatus: () => {
|
||||
void controllers.connection.controller.refreshStatusPanel();
|
||||
},
|
||||
resumeThread: (threadId) => {
|
||||
void controllers.thread.selection.selectThreadFromToolbar(threadId);
|
||||
},
|
||||
startArchiveThread: (threadId) => {
|
||||
controllers.toolbar.panels.startArchive(threadId);
|
||||
},
|
||||
archiveThread: (threadId, saveMarkdown) => {
|
||||
void controllers.toolbar.panels.archiveThread(threadId, saveMarkdown);
|
||||
},
|
||||
startRenameThread: (threadId) => {
|
||||
controllers.thread.rename.start(threadId);
|
||||
},
|
||||
updateRenameDraft: (threadId, value) => {
|
||||
controllers.thread.rename.updateDraft(threadId, value);
|
||||
},
|
||||
saveRenameThread: (threadId, value) => {
|
||||
void controllers.thread.rename.save(threadId, value);
|
||||
},
|
||||
cancelRenameThread: (threadId) => {
|
||||
controllers.thread.rename.cancel(threadId);
|
||||
},
|
||||
autoNameThread: (threadId) => {
|
||||
void controllers.thread.rename.autoNameDraft(threadId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createGoalPanelActions(controllers: ChatViewControllers): ChatPanelGoalActions {
|
||||
return {
|
||||
saveObjective: (objective, tokenBudget) => this.saveGoalObjective(objective, tokenBudget),
|
||||
setStatus: (threadId, status) => controllers.runtime.goals.setStatus(threadId, status),
|
||||
clear: (threadId) => controllers.runtime.goals.clear(threadId),
|
||||
setEditingOpen: (open) => {
|
||||
this.setGoalEditingOpen(open);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async compactConversation(): Promise<void> {
|
||||
const threadId = this.state.activeThread.id;
|
||||
if (!threadId) {
|
||||
this.controllers.inbound.controller.addSystemMessage("No active thread to compact.");
|
||||
this.controllers.render.controller.render();
|
||||
return;
|
||||
}
|
||||
await this.controllers.thread.actions.compactThread(threadId);
|
||||
}
|
||||
|
||||
private setGoalEditingOpen(open: boolean, { closeToolbarPanel = false }: { closeToolbarPanel?: boolean } = {}): void {
|
||||
if (closeToolbarPanel) this.dispatch({ type: "ui/panel-set", panel: null });
|
||||
this.dispatch({ type: "ui/detail-open-set", key: "goal:editor", open });
|
||||
this.controllers.render.controller.render();
|
||||
}
|
||||
|
||||
private async saveGoalObjective(objective: string, tokenBudget: number | null): Promise<void> {
|
||||
let threadId = this.state.activeThread.id;
|
||||
if (!threadId) {
|
||||
try {
|
||||
await this.controllers.connection.controller.ensureConnected();
|
||||
const response = await this.controllers.serverActions.threads.startThread(objective, { syncGoal: false });
|
||||
threadId = response?.threadId ?? null;
|
||||
} catch (error) {
|
||||
this.controllers.inbound.controller.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
this.controllers.render.controller.render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!threadId) return;
|
||||
void this.controllers.runtime.goals.setObjective(threadId, objective, tokenBudget);
|
||||
}
|
||||
|
||||
private get state(): ChatState {
|
||||
return this.chatState.getState();
|
||||
}
|
||||
|
||||
private get turnBusy(): boolean {
|
||||
return chatTurnBusy(this.state);
|
||||
}
|
||||
|
||||
private dispatch(action: ChatAction): void {
|
||||
this.chatState.dispatch(action);
|
||||
}
|
||||
|
|
@ -448,7 +225,7 @@ export class CodexChatView extends ItemView {
|
|||
}
|
||||
|
||||
refreshSettings(): void {
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
}
|
||||
|
||||
refreshSharedThreadList(): Promise<void> {
|
||||
|
|
@ -458,17 +235,17 @@ export class CodexChatView extends ItemView {
|
|||
applyThreadListSnapshot(threads: readonly Thread[]): void {
|
||||
this.controllers.serverActions.threads.applyThreadList(threads);
|
||||
this.refreshTabHeader();
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
}
|
||||
|
||||
applyAppServerMetadataSnapshot(metadata: SharedServerMetadata): void {
|
||||
this.controllers.serverActions.metadata.applyAppServerMetadata(metadata);
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
}
|
||||
|
||||
applyAvailableModelsSnapshot(models: readonly ModelMetadata[]): void {
|
||||
this.dispatch({ type: "connection/metadata-applied", availableModels: models });
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
}
|
||||
|
||||
openPanelSnapshot(): OpenCodexPanelSnapshot {
|
||||
|
|
@ -518,7 +295,7 @@ export class CodexChatView extends ItemView {
|
|||
|
||||
setComposerText(text: string): void {
|
||||
this.controllers.composer.controller.setDraft(text, { focus: true });
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
|
|
@ -526,12 +303,12 @@ export class CodexChatView extends ItemView {
|
|||
}
|
||||
|
||||
async startNewThread(): Promise<void> {
|
||||
if (this.turnBusy) return;
|
||||
if (chatTurnBusy(this.state)) return;
|
||||
|
||||
this.controllers.thread.identity.clearActiveThreadContext();
|
||||
this.chatState.dispatch({ type: "ui/panel-set", panel: null });
|
||||
this.dispatch({ type: "connection/status-set", status: "New chat." });
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
this.focusComposer();
|
||||
}
|
||||
|
||||
|
|
@ -561,18 +338,6 @@ export class CodexChatView extends ItemView {
|
|||
void this.app.workspace.requestSaveLayout();
|
||||
}
|
||||
|
||||
private async requestModelFromUi(model: string): Promise<void> {
|
||||
await this.controllers.runtime.settings.requestModelFromUi(model);
|
||||
}
|
||||
|
||||
private async requestReasoningEffortFromUi(effort: ReasoningEffort): Promise<void> {
|
||||
await this.controllers.runtime.settings.requestReasoningEffortFromUi(effort);
|
||||
}
|
||||
|
||||
private async resetReasoningEffortToConfigFromUi(): Promise<void> {
|
||||
await this.controllers.runtime.settings.resetReasoningEffortToConfigFromUi();
|
||||
}
|
||||
|
||||
private async ensureRestoredThreadLoaded(): Promise<boolean> {
|
||||
return this.controllers.thread.restoration.ensureLoaded();
|
||||
}
|
||||
|
|
@ -581,18 +346,6 @@ export class CodexChatView extends ItemView {
|
|||
return this.controllers.thread.restoration.isPending(threadId);
|
||||
}
|
||||
|
||||
private scheduleDeferredRestoredThreadHydration(): void {
|
||||
this.controllers.thread.restoration.scheduleHydration();
|
||||
}
|
||||
|
||||
private clearDeferredRestoredThreadHydration(): void {
|
||||
this.controllers.thread.restoration.clearHydration();
|
||||
}
|
||||
|
||||
private scheduleDeferredAppServerWarmup(): void {
|
||||
this.controllers.connection.scheduleWarmup();
|
||||
}
|
||||
|
||||
private activeThreadTitle(): string | null {
|
||||
const threadId = this.state.activeThread.id;
|
||||
if (!threadId) return null;
|
||||
|
|
@ -619,24 +372,14 @@ export class CodexChatView extends ItemView {
|
|||
|
||||
private scheduleRender(): void {
|
||||
this.deferredTasks.scheduleRender(() => {
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleDeferredDiagnostics(): void {
|
||||
this.deferredTasks.scheduleDiagnostics(() => {
|
||||
void this.refreshDeferredDiagnostics();
|
||||
});
|
||||
}
|
||||
|
||||
private clearDeferredDiagnostics(): void {
|
||||
this.deferredTasks.clearDiagnostics();
|
||||
}
|
||||
|
||||
private async refreshDeferredDiagnostics(): Promise<void> {
|
||||
if (!this.controllers.connection.manager.isConnected()) return;
|
||||
await this.controllers.serverActions.diagnostics.refreshPublishedDiagnosticProbes({ cachedAppServerMetadata: true });
|
||||
this.controllers.render.controller.render();
|
||||
this.controllers.render.now();
|
||||
}
|
||||
|
||||
private panelRoot(): HTMLElement | null {
|
||||
|
|
@ -688,10 +431,6 @@ export class CodexChatView extends ItemView {
|
|||
}
|
||||
|
||||
private runtimeSnapshot(): RuntimeSnapshot {
|
||||
return this.runtimeSnapshotForState(this.state);
|
||||
}
|
||||
|
||||
private runtimeSnapshotForState(state: ChatState): RuntimeSnapshot {
|
||||
return runtimeSnapshotForChatState(state);
|
||||
return runtimeSnapshotForChatState(this.state);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,35 +17,30 @@ export type ThreadsViewConnectionLifecycleState = ConnectionWorkLifecycleState;
|
|||
export type ActiveThreadsViewConnection = ActiveConnectionWork;
|
||||
export type ThreadsViewConnectionLifecycleEvent = ConnectionWorkLifecycleEvent;
|
||||
|
||||
export class ThreadsViewDeferredTasks {
|
||||
private readonly renderTask: DeferredTask;
|
||||
private readonly refreshTask: DeferredTask;
|
||||
export interface ThreadsViewDeferredTasks {
|
||||
scheduleRender(callback: () => void): void;
|
||||
scheduleRefresh(callback: () => void): void;
|
||||
clearAll(): void;
|
||||
}
|
||||
|
||||
constructor(getWindow: () => DeferredTaskWindow) {
|
||||
this.renderTask = new DeferredTask(getWindow, 0);
|
||||
this.refreshTask = new DeferredTask(getWindow, 250);
|
||||
}
|
||||
export function createThreadsViewDeferredTasks(getWindow: () => DeferredTaskWindow): ThreadsViewDeferredTasks {
|
||||
const renderTask = new DeferredTask(getWindow, 0);
|
||||
const refreshTask = new DeferredTask(getWindow, 250);
|
||||
|
||||
scheduleRender(callback: () => void): void {
|
||||
this.renderTask.schedule(callback);
|
||||
}
|
||||
return {
|
||||
scheduleRender(callback): void {
|
||||
renderTask.schedule(callback);
|
||||
},
|
||||
|
||||
scheduleRefresh(callback: () => void): void {
|
||||
this.refreshTask.schedule(callback);
|
||||
}
|
||||
scheduleRefresh(callback): void {
|
||||
refreshTask.schedule(callback);
|
||||
},
|
||||
|
||||
clearAll(): void {
|
||||
this.clearRender();
|
||||
this.clearRefresh();
|
||||
}
|
||||
|
||||
private clearRender(): void {
|
||||
this.renderTask.clear();
|
||||
}
|
||||
|
||||
private clearRefresh(): void {
|
||||
this.refreshTask.clear();
|
||||
}
|
||||
clearAll(): void {
|
||||
renderTask.clear();
|
||||
refreshTask.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function transitionThreadsViewRefreshLifecycle(
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ import {
|
|||
type ThreadsRenameState,
|
||||
} from "./state";
|
||||
import {
|
||||
ThreadsViewDeferredTasks,
|
||||
createThreadsViewDeferredTasks,
|
||||
transitionThreadsViewConnectionLifecycle,
|
||||
transitionThreadsViewRefreshLifecycle,
|
||||
type ActiveThreadsViewConnection,
|
||||
type ActiveThreadsViewRefresh,
|
||||
type ThreadsViewDeferredTasks,
|
||||
type ThreadsViewConnectionLifecycleState,
|
||||
type ThreadsViewRefreshLifecycleState,
|
||||
} from "./view-lifecycle";
|
||||
|
|
@ -67,7 +68,7 @@ export class CodexThreadsView extends ItemView {
|
|||
private readonly plugin: CodexThreadsHost,
|
||||
) {
|
||||
super(leaf);
|
||||
this.deferredTasks = new ThreadsViewDeferredTasks(() => this.containerEl.win);
|
||||
this.deferredTasks = createThreadsViewDeferredTasks(() => this.containerEl.win);
|
||||
this.connection = new ConnectionManager(() => this.plugin.settings.codexPath, this.plugin.vaultPath);
|
||||
this.connection.setHandlers({
|
||||
onNotification: () => {
|
||||
|
|
|
|||
53
src/main.ts
53
src/main.ts
|
|
@ -13,7 +13,9 @@ import { DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormali
|
|||
import { CodexPanelSettingTab, type CodexPanelSettingTabHost } from "./settings/tab";
|
||||
import { persistedChatTurnDiffViewState, type ChatTurnDiffViewState } from "./features/chat/turn-diff/model";
|
||||
import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator";
|
||||
import { ThreadSurfaceCoordinator } from "./workspace/thread-surface-coordinator";
|
||||
import { createThreadSurfaceActions } from "./workspace/thread-surface-actions";
|
||||
import type { SharedServerMetadata } from "./domain/server/metadata";
|
||||
import type { Thread } from "./domain/threads/model";
|
||||
|
||||
export default class CodexPanelPlugin extends Plugin {
|
||||
settings: CodexPanelSettings = DEFAULT_SETTINGS;
|
||||
|
|
@ -26,7 +28,7 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
this.threadSurfaces.refreshThreadsViewLiveState();
|
||||
},
|
||||
});
|
||||
private readonly threadSurfaces = new ThreadSurfaceCoordinator({
|
||||
private readonly threadSurfaces = createThreadSurfaceActions({
|
||||
app: this.app,
|
||||
panels: this.panels,
|
||||
refreshThreadSurfaces: () => {
|
||||
|
|
@ -141,6 +143,26 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
this.appServerUserAgent = userAgent;
|
||||
}
|
||||
|
||||
private applyThreadListSnapshot(threads: readonly Thread[]): void {
|
||||
this.sharedAppServerCache.applyThreadListSnapshot(this.sharedAppServerCacheContext(), threads);
|
||||
this.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
}
|
||||
|
||||
private refreshThreadList(fetchThreads: () => Promise<readonly Thread[]>): Promise<readonly Thread[]> {
|
||||
return this.sharedAppServerCache.refreshThreadList(this.sharedAppServerCacheContext(), fetchThreads, (threads) => {
|
||||
this.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
});
|
||||
}
|
||||
|
||||
private cachedThreadList(): readonly Thread[] | null {
|
||||
return this.sharedAppServerCache.cachedThreadList(this.sharedAppServerCacheContext());
|
||||
}
|
||||
|
||||
private publishAppServerMetadata(metadata: SharedServerMetadata): void {
|
||||
this.sharedAppServerCache.applyAppServerMetadataSnapshot(this.sharedAppServerCacheContext(), metadata);
|
||||
this.threadSurfaces.publishAppServerMetadata(metadata);
|
||||
}
|
||||
|
||||
private chatHost(): CodexChatHost {
|
||||
return {
|
||||
settings: this.settings,
|
||||
|
|
@ -161,17 +183,12 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
this.threadSurfaces.refreshSharedThreadListFromOpenSurface();
|
||||
},
|
||||
applyThreadListSnapshot: (threads) => {
|
||||
this.sharedAppServerCache.applyThreadListSnapshot(this.sharedAppServerCacheContext(), threads);
|
||||
this.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
this.applyThreadListSnapshot(threads);
|
||||
},
|
||||
refreshThreadList: (fetchThreads) =>
|
||||
this.sharedAppServerCache.refreshThreadList(this.sharedAppServerCacheContext(), fetchThreads, (threads) => {
|
||||
this.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
}),
|
||||
cachedThreadList: () => this.sharedAppServerCache.cachedThreadList(this.sharedAppServerCacheContext()),
|
||||
refreshThreadList: (fetchThreads) => this.refreshThreadList(fetchThreads),
|
||||
cachedThreadList: () => this.cachedThreadList(),
|
||||
publishAppServerMetadata: (metadata) => {
|
||||
this.sharedAppServerCache.applyAppServerMetadataSnapshot(this.sharedAppServerCacheContext(), metadata);
|
||||
this.threadSurfaces.publishAppServerMetadata(metadata);
|
||||
this.publishAppServerMetadata(metadata);
|
||||
},
|
||||
publishAppServerIdentity: (userAgent) => {
|
||||
this.publishAppServerIdentity(userAgent);
|
||||
|
|
@ -196,11 +213,8 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
publishAppServerIdentity: (userAgent) => {
|
||||
this.publishAppServerIdentity(userAgent);
|
||||
},
|
||||
refreshThreadList: (fetchThreads) =>
|
||||
this.sharedAppServerCache.refreshThreadList(this.sharedAppServerCacheContext(), fetchThreads, (threads) => {
|
||||
this.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
}),
|
||||
cachedThreadList: () => this.sharedAppServerCache.cachedThreadList(this.sharedAppServerCacheContext()),
|
||||
refreshThreadList: (fetchThreads) => this.refreshThreadList(fetchThreads),
|
||||
cachedThreadList: () => this.cachedThreadList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -209,11 +223,8 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
app: this.app,
|
||||
settings: this.settings,
|
||||
vaultPath: this.vaultPath,
|
||||
cachedThreadList: () => this.sharedAppServerCache.cachedThreadList(this.sharedAppServerCacheContext()),
|
||||
refreshThreadList: (fetchThreads) =>
|
||||
this.sharedAppServerCache.refreshThreadList(this.sharedAppServerCacheContext(), fetchThreads, (threads) => {
|
||||
this.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
}),
|
||||
cachedThreadList: () => this.cachedThreadList(),
|
||||
refreshThreadList: (fetchThreads) => this.refreshThreadList(fetchThreads),
|
||||
openThreadInCurrentView: (threadId) => this.panels.openThreadInCurrentView(threadId),
|
||||
openThreadInAvailableView: (threadId) => this.panels.openThreadInAvailableView(threadId),
|
||||
};
|
||||
|
|
|
|||
96
src/workspace/thread-surface-actions.ts
Normal file
96
src/workspace/thread-surface-actions.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import type { App } from "obsidian";
|
||||
|
||||
import { VIEW_TYPE_CODEX_THREADS } from "../constants";
|
||||
import { CodexThreadsView } from "../features/threads-view/view";
|
||||
import type { ModelMetadata } from "../domain/catalog/metadata";
|
||||
import type { Thread } from "../domain/threads/model";
|
||||
import type { SharedServerMetadata } from "../domain/server/metadata";
|
||||
import type { WorkspacePanelCoordinator } from "./panel-coordinator";
|
||||
|
||||
export interface ThreadSurfaceActionsOptions {
|
||||
app: App;
|
||||
panels: WorkspacePanelCoordinator;
|
||||
refreshThreadSurfaces: () => void;
|
||||
}
|
||||
|
||||
export interface ThreadSurfaceActions {
|
||||
refreshOpenViews(): void;
|
||||
refreshSharedThreadListFromOpenSurface(): void;
|
||||
applyThreadListSnapshot(threads: readonly Thread[]): void;
|
||||
publishAppServerMetadata(metadata: SharedServerMetadata): void;
|
||||
publishModels(models: readonly ModelMetadata[]): void;
|
||||
refreshThreadsViewLiveState(): void;
|
||||
notifyThreadArchived(threadId: string, options?: { closeOpenPanels?: boolean }): void;
|
||||
notifyThreadRenamed(threadId: string, name: string | null): void;
|
||||
}
|
||||
|
||||
export function createThreadSurfaceActions(options: ThreadSurfaceActionsOptions): ThreadSurfaceActions {
|
||||
const threadsViews = (): CodexThreadsView[] =>
|
||||
options.app.workspace
|
||||
.getLeavesOfType(VIEW_TYPE_CODEX_THREADS)
|
||||
.flatMap((leaf) => (leaf.view instanceof CodexThreadsView ? [leaf.view] : []));
|
||||
|
||||
return {
|
||||
refreshOpenViews(): void {
|
||||
for (const view of options.panels.panelViews()) {
|
||||
view.refreshSettings();
|
||||
}
|
||||
},
|
||||
|
||||
refreshSharedThreadListFromOpenSurface(): void {
|
||||
const chatView = options.panels.panelViews().find((view) => view.openPanelSnapshot().connected);
|
||||
if (chatView) {
|
||||
void chatView.refreshSharedThreadList();
|
||||
return;
|
||||
}
|
||||
|
||||
const threadsView = threadsViews().at(0);
|
||||
if (threadsView) void threadsView.refresh();
|
||||
},
|
||||
|
||||
applyThreadListSnapshot(threads: readonly Thread[]): void {
|
||||
for (const view of options.panels.panelViews()) {
|
||||
view.applyThreadListSnapshot(threads);
|
||||
}
|
||||
for (const view of threadsViews()) {
|
||||
view.applyThreadListSnapshot(threads);
|
||||
}
|
||||
},
|
||||
|
||||
publishAppServerMetadata(metadata: SharedServerMetadata): void {
|
||||
for (const view of options.panels.panelViews()) {
|
||||
view.applyAppServerMetadataSnapshot(metadata);
|
||||
}
|
||||
},
|
||||
|
||||
publishModels(models: readonly ModelMetadata[]): void {
|
||||
for (const view of options.panels.panelViews()) {
|
||||
view.applyAvailableModelsSnapshot(models);
|
||||
}
|
||||
},
|
||||
|
||||
refreshThreadsViewLiveState(): void {
|
||||
for (const view of threadsViews()) {
|
||||
view.refreshLiveState();
|
||||
}
|
||||
},
|
||||
|
||||
notifyThreadArchived(threadId: string, archiveOptions: { closeOpenPanels?: boolean } = {}): void {
|
||||
const leavesToClose = archiveOptions.closeOpenPanels ? options.panels.panelLeavesForThread(threadId) : [];
|
||||
for (const view of options.panels.panelViews()) {
|
||||
view.notifyThreadArchived(threadId);
|
||||
}
|
||||
for (const leaf of leavesToClose) {
|
||||
leaf.detach();
|
||||
}
|
||||
options.refreshThreadSurfaces();
|
||||
},
|
||||
|
||||
notifyThreadRenamed(threadId: string, name: string | null): void {
|
||||
for (const view of options.panels.panelViews()) {
|
||||
view.notifyThreadRenamed(threadId, name);
|
||||
}
|
||||
options.refreshThreadSurfaces();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import type { App } from "obsidian";
|
||||
|
||||
import { VIEW_TYPE_CODEX_THREADS } from "../constants";
|
||||
import { CodexThreadsView } from "../features/threads-view/view";
|
||||
import type { ModelMetadata } from "../domain/catalog/metadata";
|
||||
import type { Thread } from "../domain/threads/model";
|
||||
import type { SharedServerMetadata } from "../domain/server/metadata";
|
||||
import type { WorkspacePanelCoordinator } from "./panel-coordinator";
|
||||
|
||||
export interface ThreadSurfaceCoordinatorOptions {
|
||||
app: App;
|
||||
panels: WorkspacePanelCoordinator;
|
||||
refreshThreadSurfaces: () => void;
|
||||
}
|
||||
|
||||
export class ThreadSurfaceCoordinator {
|
||||
constructor(private readonly options: ThreadSurfaceCoordinatorOptions) {}
|
||||
|
||||
refreshOpenViews(): void {
|
||||
for (const view of this.options.panels.panelViews()) {
|
||||
view.refreshSettings();
|
||||
}
|
||||
}
|
||||
|
||||
refreshSharedThreadListFromOpenSurface(): void {
|
||||
const chatView = this.options.panels.panelViews().find((view) => view.openPanelSnapshot().connected);
|
||||
if (chatView) {
|
||||
void chatView.refreshSharedThreadList();
|
||||
return;
|
||||
}
|
||||
|
||||
const threadsView = this.threadsViews().at(0);
|
||||
if (threadsView) void threadsView.refresh();
|
||||
}
|
||||
|
||||
applyThreadListSnapshot(threads: readonly Thread[]): void {
|
||||
for (const view of this.options.panels.panelViews()) {
|
||||
view.applyThreadListSnapshot(threads);
|
||||
}
|
||||
for (const view of this.threadsViews()) {
|
||||
view.applyThreadListSnapshot(threads);
|
||||
}
|
||||
}
|
||||
|
||||
publishAppServerMetadata(metadata: SharedServerMetadata): void {
|
||||
for (const view of this.options.panels.panelViews()) {
|
||||
view.applyAppServerMetadataSnapshot(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
publishModels(models: readonly ModelMetadata[]): void {
|
||||
for (const view of this.options.panels.panelViews()) {
|
||||
view.applyAvailableModelsSnapshot(models);
|
||||
}
|
||||
}
|
||||
|
||||
refreshThreadsViewLiveState(): void {
|
||||
for (const view of this.threadsViews()) {
|
||||
view.refreshLiveState();
|
||||
}
|
||||
}
|
||||
|
||||
notifyThreadArchived(threadId: string, options: { closeOpenPanels?: boolean } = {}): void {
|
||||
const leavesToClose = options.closeOpenPanels ? this.options.panels.panelLeavesForThread(threadId) : [];
|
||||
for (const view of this.options.panels.panelViews()) {
|
||||
view.notifyThreadArchived(threadId);
|
||||
}
|
||||
for (const leaf of leavesToClose) {
|
||||
leaf.detach();
|
||||
}
|
||||
this.options.refreshThreadSurfaces();
|
||||
}
|
||||
|
||||
notifyThreadRenamed(threadId: string, name: string | null): void {
|
||||
for (const view of this.options.panels.panelViews()) {
|
||||
view.notifyThreadRenamed(threadId, name);
|
||||
}
|
||||
this.options.refreshThreadSurfaces();
|
||||
}
|
||||
|
||||
private threadsViews(): CodexThreadsView[] {
|
||||
return this.options.app.workspace
|
||||
.getLeavesOfType(VIEW_TYPE_CODEX_THREADS)
|
||||
.flatMap((leaf) => (leaf.view instanceof CodexThreadsView ? [leaf.view] : []));
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { scheduleAppServerWarmup } from "../../../../src/features/chat/connection/app-server-warmup";
|
||||
import { ChatViewDeferredTasks } from "../../../../src/features/chat/lifecycle";
|
||||
import { createChatViewDeferredTasks } from "../../../../src/features/chat/lifecycle";
|
||||
|
||||
function createWarmupHost({
|
||||
opened = true,
|
||||
|
|
@ -12,7 +12,7 @@ function createWarmupHost({
|
|||
}: { opened?: boolean; closing?: boolean; connected?: boolean } = {}) {
|
||||
const ensureConnected = vi.fn().mockResolvedValue(undefined);
|
||||
const host = {
|
||||
deferredTasks: new ChatViewDeferredTasks(() => window),
|
||||
deferredTasks: createChatViewDeferredTasks(() => window),
|
||||
opened: () => opened,
|
||||
closing: () => closing,
|
||||
connected: () => connected,
|
||||
|
|
|
|||
|
|
@ -43,16 +43,10 @@ function createController({ client = {} as AppServerClient } = {}) {
|
|||
});
|
||||
const host: PlanImplementationHost = {
|
||||
stateStore,
|
||||
connection: {
|
||||
currentClient: () => client,
|
||||
ensureConnected,
|
||||
},
|
||||
submission: {
|
||||
sendTurnText,
|
||||
},
|
||||
runtime: {
|
||||
requestDefaultCollaborationModeForNextTurn,
|
||||
},
|
||||
currentClient: () => client,
|
||||
ensureConnected,
|
||||
sendTurnText,
|
||||
requestDefaultCollaborationModeForNextTurn,
|
||||
};
|
||||
return {
|
||||
controller: createPlanImplementation(host),
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ import { createChatState, createChatStateStore } from "../../../../../src/featur
|
|||
import {
|
||||
createSlashCommandHandler,
|
||||
type SlashCommandHandlerHost,
|
||||
type SlashCommandGoalPort,
|
||||
type SlashCommandRuntimePort,
|
||||
type SlashCommandStatusPort,
|
||||
type SlashCommandThreadPort,
|
||||
} from "../../../../../src/features/chat/conversation/turns/slash-command-handler";
|
||||
import type { Thread } from "../../../../../src/domain/threads/model";
|
||||
|
||||
|
|
@ -26,26 +22,17 @@ function thread(id: string, name: string | null = null): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
interface SlashCommandHostOverrides extends Partial<Omit<SlashCommandHandlerHost, "threads" | "runtime" | "goals" | "status">> {
|
||||
threads?: Partial<SlashCommandThreadPort>;
|
||||
runtime?: Partial<SlashCommandRuntimePort>;
|
||||
goals?: Partial<SlashCommandGoalPort>;
|
||||
status?: Partial<SlashCommandStatusPort>;
|
||||
}
|
||||
type SlashCommandHostOverrides = Partial<SlashCommandHandlerHost>;
|
||||
|
||||
function createHost(overrides: SlashCommandHostOverrides = {}) {
|
||||
const {
|
||||
threads: threadOverrides,
|
||||
runtime: runtimeOverrides,
|
||||
goals: goalOverrides,
|
||||
status: statusOverrides,
|
||||
...hostOverrides
|
||||
} = overrides;
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const threadTurnsList = vi.fn().mockResolvedValue({ data: [] });
|
||||
const client = { threadTurnsList } as unknown as AppServerClient;
|
||||
const compactThread = vi.fn().mockResolvedValue(undefined);
|
||||
const threads: SlashCommandThreadPort = {
|
||||
const host: SlashCommandHandlerHost = {
|
||||
stateStore,
|
||||
currentClient: () => client,
|
||||
codexInput: vi.fn((text: string) => textInput(text)),
|
||||
startNewThread: vi.fn().mockResolvedValue(undefined),
|
||||
startThreadForGoal: vi.fn().mockResolvedValue("thread-new"),
|
||||
resumeThread: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -55,9 +42,6 @@ function createHost(overrides: SlashCommandHostOverrides = {}) {
|
|||
archiveThread: vi.fn().mockResolvedValue(undefined),
|
||||
renameThread: vi.fn().mockResolvedValue(undefined),
|
||||
reconnect: vi.fn().mockResolvedValue(undefined),
|
||||
...threadOverrides,
|
||||
};
|
||||
const runtime: SlashCommandRuntimePort = {
|
||||
toggleFastMode: vi.fn(),
|
||||
toggleCollaborationMode: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
|
|
@ -65,9 +49,10 @@ function createHost(overrides: SlashCommandHostOverrides = {}) {
|
|||
resetModelToConfig: vi.fn(),
|
||||
requestReasoningEffort: vi.fn(),
|
||||
resetReasoningEffortToConfig: vi.fn(),
|
||||
...runtimeOverrides,
|
||||
};
|
||||
const status: SlashCommandStatusPort = {
|
||||
activeGoal: vi.fn(() => stateStore.getState().activeThread.goal),
|
||||
setGoalObjective: vi.fn().mockResolvedValue(true),
|
||||
setGoalStatus: vi.fn().mockResolvedValue(true),
|
||||
clearGoal: vi.fn().mockResolvedValue(true),
|
||||
addSystemMessage: vi.fn(),
|
||||
addStructuredSystemMessage: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
|
|
@ -76,24 +61,7 @@ function createHost(overrides: SlashCommandHostOverrides = {}) {
|
|||
mcpStatusLines: vi.fn().mockResolvedValue([]),
|
||||
modelStatusLines: () => [],
|
||||
effortStatusLines: () => [],
|
||||
...statusOverrides,
|
||||
};
|
||||
const goals: SlashCommandGoalPort = {
|
||||
activeGoal: vi.fn(() => stateStore.getState().activeThread.goal),
|
||||
setObjective: vi.fn().mockResolvedValue(true),
|
||||
setStatus: vi.fn().mockResolvedValue(true),
|
||||
clear: vi.fn().mockResolvedValue(true),
|
||||
...goalOverrides,
|
||||
};
|
||||
const host: SlashCommandHandlerHost = {
|
||||
stateStore,
|
||||
currentClient: () => client,
|
||||
codexInput: vi.fn((text: string) => textInput(text)),
|
||||
threads,
|
||||
runtime,
|
||||
goals,
|
||||
status,
|
||||
...hostOverrides,
|
||||
...overrides,
|
||||
};
|
||||
return { compactThread, host, stateStore, threadTurnsList };
|
||||
}
|
||||
|
|
@ -105,7 +73,7 @@ describe("createSlashCommandHandler", () => {
|
|||
|
||||
const result = await controller.execute("clear", "");
|
||||
|
||||
expect(host.threads.startNewThread).toHaveBeenCalledOnce();
|
||||
expect(host.startNewThread).toHaveBeenCalledOnce();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -159,8 +127,8 @@ describe("createSlashCommandHandler", () => {
|
|||
|
||||
await controller.execute("goal", "set Ship this");
|
||||
|
||||
expect(host.threads.startThreadForGoal).toHaveBeenCalledWith("Ship this");
|
||||
expect(host.goals.setObjective).toHaveBeenCalledWith("thread-new", "Ship this", null);
|
||||
expect(host.startThreadForGoal).toHaveBeenCalledWith("Ship this");
|
||||
expect(host.setGoalObjective).toHaveBeenCalledWith("thread-new", "Ship this", null);
|
||||
});
|
||||
|
||||
it("runs reconnect even when there is no current app-server client", async () => {
|
||||
|
|
@ -169,7 +137,7 @@ describe("createSlashCommandHandler", () => {
|
|||
|
||||
await controller.execute("reconnect", "");
|
||||
|
||||
expect(host.threads.reconnect).toHaveBeenCalledOnce();
|
||||
expect(host.reconnect).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reports unreadable referenced threads", async () => {
|
||||
|
|
@ -184,6 +152,6 @@ describe("createSlashCommandHandler", () => {
|
|||
|
||||
expect(threadTurnsList).toHaveBeenCalledWith("other", null, 20);
|
||||
expect(result).toBeUndefined();
|
||||
expect(host.status.addSystemMessage).toHaveBeenCalledWith("Referenced thread has no readable conversation turns.");
|
||||
expect(host.addSystemMessage).toHaveBeenCalledWith("Referenced thread has no readable conversation turns.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,13 +6,6 @@ import { createChatState, createChatStateStore } from "../../../../../src/featur
|
|||
import {
|
||||
TurnSubmissionController,
|
||||
type TurnSubmissionControllerHost,
|
||||
type TurnSubmissionComposerPort,
|
||||
type TurnSubmissionConnectionPort,
|
||||
type TurnSubmissionRestoredThreadPort,
|
||||
type TurnSubmissionRuntimePort,
|
||||
type TurnSubmissionStatusPort,
|
||||
type TurnSubmissionThreadPort,
|
||||
type TurnSubmissionViewPort,
|
||||
} from "../../../../../src/features/chat/conversation/turns/turn-submission-controller";
|
||||
import type { Thread } from "../../../../../src/domain/threads/model";
|
||||
|
||||
|
|
@ -29,29 +22,9 @@ function thread(id: string): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
interface TurnSubmissionHostOverrides extends Partial<
|
||||
Omit<TurnSubmissionControllerHost, "connection" | "restoredThread" | "thread" | "runtime" | "composer" | "view" | "status">
|
||||
> {
|
||||
connection?: Partial<TurnSubmissionConnectionPort>;
|
||||
restoredThread?: Partial<TurnSubmissionRestoredThreadPort>;
|
||||
thread?: Partial<TurnSubmissionThreadPort>;
|
||||
runtime?: Partial<TurnSubmissionRuntimePort>;
|
||||
composer?: Partial<TurnSubmissionComposerPort>;
|
||||
view?: Partial<TurnSubmissionViewPort>;
|
||||
status?: Partial<TurnSubmissionStatusPort>;
|
||||
}
|
||||
type TurnSubmissionHostOverrides = Partial<TurnSubmissionControllerHost>;
|
||||
|
||||
function createHost(overrides: TurnSubmissionHostOverrides = {}) {
|
||||
const {
|
||||
connection: connectionOverrides,
|
||||
restoredThread: restoredThreadOverrides,
|
||||
thread: threadOverrides,
|
||||
runtime: runtimeOverrides,
|
||||
composer: composerOverrides,
|
||||
view: viewOverrides,
|
||||
status: statusOverrides,
|
||||
...hostOverrides
|
||||
} = overrides;
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const startTurn = vi.fn().mockResolvedValue({ turn: { id: "turn" } });
|
||||
const steerTurn = vi.fn().mockResolvedValue({});
|
||||
|
|
@ -59,16 +32,11 @@ function createHost(overrides: TurnSubmissionHostOverrides = {}) {
|
|||
startTurn,
|
||||
steerTurn,
|
||||
} as unknown as AppServerClient;
|
||||
const connection: TurnSubmissionConnectionPort = {
|
||||
const host: TurnSubmissionControllerHost = {
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...connectionOverrides,
|
||||
};
|
||||
const restoredThread: TurnSubmissionRestoredThreadPort = {
|
||||
ensureRestoredThreadLoaded: vi.fn().mockResolvedValue(true),
|
||||
...restoredThreadOverrides,
|
||||
};
|
||||
const threadPort: TurnSubmissionThreadPort = {
|
||||
startThread: vi.fn().mockImplementation(async () => {
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
|
|
@ -85,37 +53,14 @@ function createHost(overrides: TurnSubmissionHostOverrides = {}) {
|
|||
}),
|
||||
notifyActiveThreadIdentityChanged: vi.fn(),
|
||||
resetThreadTurnPresence: vi.fn(),
|
||||
...threadOverrides,
|
||||
};
|
||||
const runtime: TurnSubmissionRuntimePort = {
|
||||
applyPendingThreadSettings: vi.fn().mockResolvedValue(true),
|
||||
...runtimeOverrides,
|
||||
};
|
||||
const composer: TurnSubmissionComposerPort = {
|
||||
codexInput: vi.fn((text: string) => textInput(text)),
|
||||
setDraft: vi.fn(),
|
||||
...composerOverrides,
|
||||
};
|
||||
const view: TurnSubmissionViewPort = {
|
||||
render: vi.fn(),
|
||||
scheduleRender: vi.fn(),
|
||||
...viewOverrides,
|
||||
};
|
||||
const status: TurnSubmissionStatusPort = {
|
||||
setStatus: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
...statusOverrides,
|
||||
};
|
||||
const host: TurnSubmissionControllerHost = {
|
||||
stateStore,
|
||||
connection,
|
||||
restoredThread,
|
||||
thread: threadPort,
|
||||
runtime,
|
||||
composer,
|
||||
view,
|
||||
status,
|
||||
...hostOverrides,
|
||||
...overrides,
|
||||
};
|
||||
return { host, startTurn, stateStore, steerTurn };
|
||||
}
|
||||
|
|
@ -127,9 +72,9 @@ describe("TurnSubmissionController", () => {
|
|||
|
||||
await controller.sendTurnText("hello");
|
||||
|
||||
expect(host.thread.startThread).toHaveBeenCalledWith("hello");
|
||||
expect(host.thread.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce();
|
||||
expect(host.thread.resetThreadTurnPresence).toHaveBeenCalledWith(false);
|
||||
expect(host.startThread).toHaveBeenCalledWith("hello");
|
||||
expect(host.notifyActiveThreadIdentityChanged).toHaveBeenCalledOnce();
|
||||
expect(host.resetThreadTurnPresence).toHaveBeenCalledWith(false);
|
||||
expect(startTurn).toHaveBeenCalledWith({
|
||||
threadId: "thread",
|
||||
cwd: "/vault",
|
||||
|
|
@ -137,9 +82,9 @@ describe("TurnSubmissionController", () => {
|
|||
clientUserMessageId: expect.stringMatching(/^local-user-\d+-\d+$/),
|
||||
});
|
||||
expect(stateStore.getState().turn.lifecycle).toEqual({ kind: "running", turnId: "turn" });
|
||||
expect(host.composer.setDraft).toHaveBeenCalledWith("");
|
||||
expect(host.status.setStatus).toHaveBeenCalledWith("Turn running...");
|
||||
expect(host.view.scheduleRender).toHaveBeenCalledOnce();
|
||||
expect(host.setDraft).toHaveBeenCalledWith("");
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Turn running...");
|
||||
expect(host.scheduleRender).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not restore stale drafts or report stale start failures after the active thread changes", async () => {
|
||||
|
|
@ -163,9 +108,9 @@ describe("TurnSubmissionController", () => {
|
|||
|
||||
await controller.sendTurnText("hello");
|
||||
|
||||
expect(host.composer.setDraft).toHaveBeenCalledWith("");
|
||||
expect(host.composer.setDraft).not.toHaveBeenCalledWith("hello");
|
||||
expect(host.status.addSystemMessage).not.toHaveBeenCalled();
|
||||
expect(host.setDraft).toHaveBeenCalledWith("");
|
||||
expect(host.setDraft).not.toHaveBeenCalledWith("hello");
|
||||
expect(host.addSystemMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("steers a running turn instead of starting another turn", async () => {
|
||||
|
|
@ -188,7 +133,7 @@ describe("TurnSubmissionController", () => {
|
|||
|
||||
expect(steerTurn).toHaveBeenCalledWith("thread", "turn", textInput("follow up"), expect.stringMatching(/^local-steer-\d+-\d+$/));
|
||||
expect(startTurn).not.toHaveBeenCalled();
|
||||
expect(host.status.setStatus).toHaveBeenCalledWith("Steered current turn.");
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Steered current turn.");
|
||||
const localSteerId = steerTurn.mock.calls[0]?.[3];
|
||||
expect(
|
||||
stateStore
|
||||
|
|
@ -252,8 +197,8 @@ describe("TurnSubmissionController", () => {
|
|||
await controller.sendTurnText("follow up");
|
||||
|
||||
expect(startTurn).not.toHaveBeenCalled();
|
||||
expect(host.composer.setDraft).toHaveBeenCalledWith("", { clearSuggestions: true });
|
||||
expect(host.status.setStatus).not.toHaveBeenCalledWith("Steered current turn.");
|
||||
expect(host.setDraft).toHaveBeenCalledWith("", { clearSuggestions: true });
|
||||
expect(host.setStatus).not.toHaveBeenCalledWith("Steered current turn.");
|
||||
expect(stateStore.getState().messageStream.displayItems).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
@ -280,8 +225,8 @@ describe("TurnSubmissionController", () => {
|
|||
await controller.sendTurnText("follow up");
|
||||
|
||||
expect(startTurn).not.toHaveBeenCalled();
|
||||
expect(host.composer.setDraft).toHaveBeenCalledWith("", { clearSuggestions: true });
|
||||
expect(host.composer.setDraft).not.toHaveBeenCalledWith("follow up", { focus: true });
|
||||
expect(host.status.addSystemMessage).not.toHaveBeenCalled();
|
||||
expect(host.setDraft).toHaveBeenCalledWith("", { clearSuggestions: true });
|
||||
expect(host.setDraft).not.toHaveBeenCalledWith("follow up", { focus: true });
|
||||
expect(host.addSystemMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createServerDiagnostics } from "../../../../src/domain/server/diagnostics";
|
||||
import { emptyRuntimeConfigSnapshot } from "../../../../src/app-server/protocol/runtime-config";
|
||||
import { applyCachedSharedAppServerState } from "../../../../src/features/chat/panel/cached-app-server-state";
|
||||
import type { SharedServerMetadata } from "../../../../src/app-server/services/shared-cache-state";
|
||||
import type { Thread } from "../../../../src/domain/threads/model";
|
||||
|
||||
describe("cached app-server state", () => {
|
||||
it("does not apply missing cached thread snapshots as shared truth", () => {
|
||||
const serverThreads = { applyThreadList: vi.fn(), loadThreadList: vi.fn(), startThread: vi.fn() };
|
||||
const serverMetadata = { ...metadataActions(), applyAppServerMetadata: vi.fn() };
|
||||
|
||||
applyCachedSharedAppServerState(
|
||||
{
|
||||
cachedThreadList: () => null,
|
||||
cachedAppServerMetadata: () => null,
|
||||
},
|
||||
serverThreads,
|
||||
serverMetadata,
|
||||
);
|
||||
|
||||
expect(serverThreads.applyThreadList).not.toHaveBeenCalled();
|
||||
expect(serverMetadata.applyAppServerMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies cached thread lists, including empty ready snapshots, and metadata independently when present", () => {
|
||||
const threads: Thread[] = [];
|
||||
const metadata = metadataSnapshot();
|
||||
const serverThreads = { applyThreadList: vi.fn(), loadThreadList: vi.fn(), startThread: vi.fn() };
|
||||
const serverMetadata = { ...metadataActions(), applyAppServerMetadata: vi.fn() };
|
||||
|
||||
applyCachedSharedAppServerState(
|
||||
{
|
||||
cachedThreadList: () => threads,
|
||||
cachedAppServerMetadata: () => metadata,
|
||||
},
|
||||
serverThreads,
|
||||
serverMetadata,
|
||||
);
|
||||
|
||||
expect(serverThreads.applyThreadList).toHaveBeenCalledWith(threads);
|
||||
expect(serverMetadata.applyAppServerMetadata).toHaveBeenCalledWith(metadata);
|
||||
});
|
||||
});
|
||||
|
||||
function metadataActions() {
|
||||
return {
|
||||
serverMetadataSnapshot: vi.fn(),
|
||||
loadAppServerMetadata: vi.fn(),
|
||||
refreshAppServerMetadata: vi.fn(),
|
||||
refreshPublishedAppServerMetadata: vi.fn(),
|
||||
publishAppServerMetadataSnapshot: vi.fn(),
|
||||
refreshModels: vi.fn(),
|
||||
loadModels: vi.fn(),
|
||||
refreshSkills: vi.fn(),
|
||||
refreshPublishedSkills: vi.fn(),
|
||||
loadSkills: vi.fn(),
|
||||
refreshRateLimits: vi.fn(),
|
||||
refreshPublishedRateLimits: vi.fn(),
|
||||
loadRateLimit: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function metadataSnapshot(): SharedServerMetadata {
|
||||
return {
|
||||
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
||||
availableModels: [],
|
||||
availableSkills: [],
|
||||
rateLimit: null,
|
||||
serverDiagnostics: createServerDiagnostics(),
|
||||
};
|
||||
}
|
||||
|
|
@ -3,45 +3,45 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createChatState, createChatStateStore } from "../../../../../src/features/chat/state/reducer";
|
||||
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "../../../../../src/features/chat/panel/regions/toolbar";
|
||||
import type { ChatThreadActions } from "../../../../../src/features/chat/threads/actions";
|
||||
import { createToolbarArchiveConfirmState, createToolbarPanelActions } from "../../../../../src/features/chat/panel/regions/toolbar";
|
||||
import type { ChatThreadActions } from "../../../../../src/features/chat/threads/action-context";
|
||||
|
||||
describe("ToolbarPanelController", () => {
|
||||
describe("createToolbarPanelActions", () => {
|
||||
it("tracks archive confirmation and delegates archive actions", async () => {
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const archiveThread = vi.fn().mockResolvedValue(undefined);
|
||||
const scheduleRender = vi.fn();
|
||||
const controller = new ToolbarPanelController({
|
||||
const actions = createToolbarPanelActions({
|
||||
stateStore,
|
||||
threadActions: { archiveThread } as unknown as ChatThreadActions,
|
||||
archiveConfirm: createToolbarArchiveConfirmState(),
|
||||
scheduleRender,
|
||||
});
|
||||
|
||||
controller.startArchive("thread");
|
||||
expect(controller.archiveConfirmId()).toBe("thread");
|
||||
actions.startArchive("thread");
|
||||
expect(actions.archiveConfirmId()).toBe("thread");
|
||||
expect(scheduleRender).not.toHaveBeenCalled();
|
||||
|
||||
await controller.archiveThread("thread", true);
|
||||
await actions.archiveThread("thread", true);
|
||||
|
||||
expect(archiveThread).toHaveBeenCalledWith("thread", true);
|
||||
expect(controller.archiveConfirmId()).toBeNull();
|
||||
expect(actions.archiveConfirmId()).toBeNull();
|
||||
expect(scheduleRender).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("closes mutually exclusive toolbar panels on outside pointers", () => {
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const scheduleRender = vi.fn();
|
||||
const controller = new ToolbarPanelController({
|
||||
const actions = createToolbarPanelActions({
|
||||
stateStore,
|
||||
threadActions: { archiveThread: vi.fn() } as unknown as ChatThreadActions,
|
||||
archiveConfirm: createToolbarArchiveConfirmState(),
|
||||
scheduleRender,
|
||||
});
|
||||
controller.toggleHistory();
|
||||
actions.toggleHistory();
|
||||
expect(stateStore.getState().ui.toolbarPanel).toBe("history");
|
||||
|
||||
controller.closeOnOutsidePointer({
|
||||
actions.closeOnOutsidePointer({
|
||||
target: document.createElement("button"),
|
||||
viewWindow: window,
|
||||
contains: () => false,
|
||||
|
|
|
|||
|
|
@ -13,28 +13,40 @@ vi.mock("../../../../src/features/chat/ui/shell", () => ({
|
|||
function createHost(overrides: Partial<ChatViewLifecycleHost> = {}) {
|
||||
const root = document.createElement("div");
|
||||
const host: ChatViewLifecycleHost = {
|
||||
setOpened: vi.fn(),
|
||||
setClosing: vi.fn(),
|
||||
registerEvent: vi.fn(),
|
||||
registerComposerNoteIndexInvalidation: vi.fn((register) => {
|
||||
register({} as EventRef);
|
||||
}),
|
||||
registerPointerDown: vi.fn(),
|
||||
applyCachedSharedAppServerState: vi.fn(),
|
||||
render: vi.fn(),
|
||||
scheduleDeferredAppServerWarmup: vi.fn(),
|
||||
scheduleDeferredRestoredThreadHydration: vi.fn(),
|
||||
closeToolbarPanelOnOutsidePointer: vi.fn(),
|
||||
invalidateConnectionWork: vi.fn(),
|
||||
invalidateResumeWork: vi.fn(),
|
||||
clearDeferredTasks: vi.fn(),
|
||||
panelRoot: () => root,
|
||||
disposeMessages: vi.fn(),
|
||||
disposeComposer: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
clearClient: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
deferRefreshLiveState: vi.fn(),
|
||||
lifecycle: {
|
||||
setOpened: vi.fn(),
|
||||
setClosing: vi.fn(),
|
||||
invalidateConnectionWork: vi.fn(),
|
||||
invalidateResumeWork: vi.fn(),
|
||||
clearDeferredTasks: vi.fn(),
|
||||
scheduleDeferredAppServerWarmup: vi.fn(),
|
||||
scheduleDeferredRestoredThreadHydration: vi.fn(),
|
||||
},
|
||||
events: {
|
||||
registerEvent: vi.fn(),
|
||||
registerComposerNoteIndexInvalidation: vi.fn((register) => {
|
||||
register({} as EventRef);
|
||||
}),
|
||||
registerPointerDown: vi.fn(),
|
||||
closeToolbarPanelOnOutsidePointer: vi.fn(),
|
||||
},
|
||||
render: {
|
||||
panelRoot: () => root,
|
||||
now: vi.fn(),
|
||||
},
|
||||
sharedState: {
|
||||
applyCachedAppServerState: vi.fn(),
|
||||
},
|
||||
resources: {
|
||||
disposeMessages: vi.fn(),
|
||||
disposeComposer: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
clearClient: vi.fn(),
|
||||
},
|
||||
liveState: {
|
||||
refresh: vi.fn(),
|
||||
deferRefresh: vi.fn(),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
return { host, root };
|
||||
|
|
@ -50,14 +62,14 @@ describe("chat view lifecycle", () => {
|
|||
|
||||
openChatView(host);
|
||||
|
||||
expect(host.setOpened).toHaveBeenCalledWith(true);
|
||||
expect(host.setClosing).toHaveBeenCalledWith(false);
|
||||
expect(host.registerEvent).toHaveBeenCalledOnce();
|
||||
expect(host.registerPointerDown).toHaveBeenCalledOnce();
|
||||
expect(host.applyCachedSharedAppServerState).toHaveBeenCalledOnce();
|
||||
expect(host.render).toHaveBeenCalledOnce();
|
||||
expect(host.scheduleDeferredAppServerWarmup).toHaveBeenCalledOnce();
|
||||
expect(host.scheduleDeferredRestoredThreadHydration).toHaveBeenCalledOnce();
|
||||
expect(host.lifecycle.setOpened).toHaveBeenCalledWith(true);
|
||||
expect(host.lifecycle.setClosing).toHaveBeenCalledWith(false);
|
||||
expect(host.events.registerEvent).toHaveBeenCalledOnce();
|
||||
expect(host.events.registerPointerDown).toHaveBeenCalledOnce();
|
||||
expect(host.sharedState.applyCachedAppServerState).toHaveBeenCalledOnce();
|
||||
expect(host.render.now).toHaveBeenCalledOnce();
|
||||
expect(host.lifecycle.scheduleDeferredAppServerWarmup).toHaveBeenCalledOnce();
|
||||
expect(host.lifecycle.scheduleDeferredRestoredThreadHydration).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("disposes mounted resources and refreshes live state on close", () => {
|
||||
|
|
@ -65,15 +77,15 @@ describe("chat view lifecycle", () => {
|
|||
|
||||
closeChatView(host);
|
||||
|
||||
expect(host.setOpened).toHaveBeenCalledWith(false);
|
||||
expect(host.setClosing).toHaveBeenCalledWith(true);
|
||||
expect(host.clearDeferredTasks).toHaveBeenCalledOnce();
|
||||
expect(host.disposeMessages).toHaveBeenCalledOnce();
|
||||
expect(host.disposeComposer).toHaveBeenCalledOnce();
|
||||
expect(host.lifecycle.setOpened).toHaveBeenCalledWith(false);
|
||||
expect(host.lifecycle.setClosing).toHaveBeenCalledWith(true);
|
||||
expect(host.lifecycle.clearDeferredTasks).toHaveBeenCalledOnce();
|
||||
expect(host.resources.disposeMessages).toHaveBeenCalledOnce();
|
||||
expect(host.resources.disposeComposer).toHaveBeenCalledOnce();
|
||||
expect(unmountChatPanelShell).toHaveBeenCalledWith(root);
|
||||
expect(host.disconnect).toHaveBeenCalledOnce();
|
||||
expect(host.clearClient).toHaveBeenCalledOnce();
|
||||
expect(host.refreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(host.deferRefreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(host.resources.disconnect).toHaveBeenCalledOnce();
|
||||
expect(host.resources.clearClient).toHaveBeenCalledOnce();
|
||||
expect(host.liveState.refresh).toHaveBeenCalledOnce();
|
||||
expect(host.liveState.deferRefresh).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ChatViewRenderController, type ChatViewRenderControllerHost } from "../../../../src/features/chat/panel/view-render-controller";
|
||||
|
||||
describe("ChatViewRenderController", () => {
|
||||
it("renders the shell through the configured panel root", () => {
|
||||
const root = document.createElement("div");
|
||||
const renderShell = vi.fn();
|
||||
const host = renderHost(root, renderShell);
|
||||
const controller = new ChatViewRenderController(host);
|
||||
|
||||
controller.render();
|
||||
|
||||
expect(host.clearScheduledRender).toHaveBeenCalledOnce();
|
||||
expect(renderShell).toHaveBeenCalledWith(root);
|
||||
});
|
||||
});
|
||||
|
||||
function renderHost(root: HTMLElement, renderShell: (root: HTMLElement) => void): ChatViewRenderControllerHost {
|
||||
return {
|
||||
shell: {
|
||||
render: renderShell,
|
||||
},
|
||||
panelRoot: () => root,
|
||||
clearScheduledRender: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
|
@ -4,7 +4,12 @@ import type { AppServerClient } from "../../../../src/app-server/connection/clie
|
|||
import type { ThreadRecord } from "../../../../src/app-server/protocol/thread";
|
||||
import type { ArchiveExportAdapter } from "../../../../src/features/thread-export/archive-markdown";
|
||||
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
|
||||
import { createChatThreadActions, type ChatThreadActionsHost } from "../../../../src/features/chat/threads/actions";
|
||||
import { archiveThread } from "../../../../src/features/chat/threads/archive-actions";
|
||||
import { compactThread } from "../../../../src/features/chat/threads/compact-actions";
|
||||
import { forkThreadFromTurn } from "../../../../src/features/chat/threads/fork-actions";
|
||||
import { renameThread } from "../../../../src/features/chat/threads/rename-actions";
|
||||
import { rollbackThread as rollbackThreadAction } from "../../../../src/features/chat/threads/rollback-actions";
|
||||
import type { ChatThreadActions, ChatThreadActionsHost } from "../../../../src/features/chat/threads/action-context";
|
||||
import type { DisplayItem } from "../../../../src/features/chat/display/types";
|
||||
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
|
||||
import { notices } from "../../../mocks/obsidian";
|
||||
|
|
@ -16,7 +21,7 @@ type MockArchiveExportAdapter = ArchiveExportAdapter & {
|
|||
write: ReturnType<typeof vi.fn<ArchiveExportAdapter["write"]>>;
|
||||
};
|
||||
|
||||
describe("createChatThreadActions", () => {
|
||||
describe("chat thread actions", () => {
|
||||
beforeEach(() => {
|
||||
notices.length = 0;
|
||||
});
|
||||
|
|
@ -24,7 +29,7 @@ describe("createChatThreadActions", () => {
|
|||
it("requests thread compaction and reports the shared status", async () => {
|
||||
const client = clientMock();
|
||||
const host = hostMock({ client, displayItems: [] });
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.compactThread("source");
|
||||
|
||||
|
|
@ -50,7 +55,7 @@ describe("createChatThreadActions", () => {
|
|||
approvalsReviewer: null,
|
||||
activePermissionProfile: null,
|
||||
});
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
const pendingCompact = controller.compactThread("source");
|
||||
await waitForAsyncWork(() => {
|
||||
|
|
@ -88,7 +93,7 @@ describe("createChatThreadActions", () => {
|
|||
archiveExportFilenameTemplate: "{{title}} {{shortId}}",
|
||||
},
|
||||
});
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.archiveThread("source");
|
||||
|
||||
|
|
@ -120,7 +125,7 @@ describe("createChatThreadActions", () => {
|
|||
archiveExportFilenameTemplate: "{{title}} {{shortId}}",
|
||||
},
|
||||
});
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.archiveThread("source");
|
||||
|
||||
|
|
@ -133,7 +138,7 @@ describe("createChatThreadActions", () => {
|
|||
it("forks from a selected turn by dropping later turns on the fork", async () => {
|
||||
const client = clientMock();
|
||||
const host = hostMock({ client, displayItems: turnItems() });
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.forkThreadFromTurn("source", "turn-1", false);
|
||||
|
||||
|
|
@ -158,7 +163,7 @@ describe("createChatThreadActions", () => {
|
|||
archiveExportFilenameTemplate: "{{title}} {{shortId}}",
|
||||
},
|
||||
});
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.forkThreadFromTurn("source", "turn-3", true);
|
||||
|
||||
|
|
@ -177,7 +182,7 @@ describe("createChatThreadActions", () => {
|
|||
const client = clientMock();
|
||||
client.archiveThread.mockRejectedValue(new Error("archive failed"));
|
||||
const host = hostMock({ client, displayItems: turnItems() });
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.forkThreadFromTurn("source", "turn-3", true);
|
||||
|
||||
|
|
@ -192,7 +197,7 @@ describe("createChatThreadActions", () => {
|
|||
const client = clientMock();
|
||||
const host = hostMock({ client, displayItems: turnItems() });
|
||||
host.openThreadInCurrentPanel.mockRejectedValue(new Error("resume failed"));
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.forkThreadFromTurn("source", "turn-3", true);
|
||||
|
||||
|
|
@ -222,7 +227,7 @@ describe("createChatThreadActions", () => {
|
|||
threads: [{ ...panelThread("source"), name: "Source name" }],
|
||||
threadsLoaded: true,
|
||||
});
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
const pendingFork = controller.forkThreadFromTurn("source", null, true);
|
||||
await waitForAsyncWork(() => {
|
||||
|
|
@ -253,7 +258,7 @@ describe("createChatThreadActions", () => {
|
|||
const client = clientMock();
|
||||
const host = hostMock({ client, displayItems: [] });
|
||||
host.stateStore.dispatch({ type: "thread-list/applied", threads: [{ ...panelThread("thread"), name: "Old" }] });
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await expect(controller.renameThread("thread", " Slash command title ")).resolves.toBe(true);
|
||||
|
||||
|
|
@ -267,7 +272,7 @@ describe("createChatThreadActions", () => {
|
|||
it("ignores empty thread rename titles", async () => {
|
||||
const client = clientMock();
|
||||
const host = hostMock({ client, displayItems: [] });
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await expect(controller.renameThread("thread", " ")).resolves.toBe(false);
|
||||
|
||||
|
|
@ -291,7 +296,7 @@ describe("createChatThreadActions", () => {
|
|||
activePermissionProfile: null,
|
||||
});
|
||||
host.stateStore.dispatch({ type: "message-stream/items-replaced", items: turnItems(), historyCursor: null, loadingHistory: false });
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
await controller.rollbackThread("source");
|
||||
|
||||
|
|
@ -324,7 +329,7 @@ describe("createChatThreadActions", () => {
|
|||
activePermissionProfile: null,
|
||||
});
|
||||
host.stateStore.dispatch({ type: "message-stream/items-replaced", items: turnItems(), historyCursor: null, loadingHistory: false });
|
||||
const controller = createChatThreadActions(host);
|
||||
const controller = chatThreadActions(host);
|
||||
|
||||
const pendingRollback = controller.rollbackThread("source");
|
||||
await waitForAsyncWork(() => {
|
||||
|
|
@ -397,6 +402,17 @@ function clientMock() {
|
|||
};
|
||||
}
|
||||
|
||||
function chatThreadActions(host: ChatThreadActionsHost): ChatThreadActions {
|
||||
return {
|
||||
compactThread: (threadId) => compactThread(host, threadId),
|
||||
archiveThread: (threadId, saveMarkdown) => archiveThread(host, threadId, saveMarkdown),
|
||||
forkThread: (threadId) => forkThreadFromTurn(host, threadId, null, false),
|
||||
forkThreadFromTurn: (threadId, turnId, archiveSource) => forkThreadFromTurn(host, threadId, turnId, archiveSource),
|
||||
renameThread: (threadId, name) => renameThread(host, threadId, name),
|
||||
rollbackThread: (threadId) => rollbackThreadAction(host, threadId),
|
||||
};
|
||||
}
|
||||
|
||||
function hostMock({
|
||||
client,
|
||||
displayItems,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { describe, expect, it, vi } from "vitest";
|
|||
|
||||
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
|
||||
import { RestorationController } from "../../../../src/features/chat/threads/restoration-controller";
|
||||
import { ChatViewDeferredTasks } from "../../../../src/features/chat/lifecycle";
|
||||
import { createChatViewDeferredTasks } from "../../../../src/features/chat/lifecycle";
|
||||
import { deferred } from "../../../support/async";
|
||||
|
||||
describe("RestorationController", () => {
|
||||
|
|
@ -13,7 +13,7 @@ describe("RestorationController", () => {
|
|||
const stateStore = createChatStateStore(createChatState());
|
||||
const resumeThread = vi.fn().mockResolvedValue(undefined);
|
||||
const controller = new RestorationController({
|
||||
deferredTasks: new ChatViewDeferredTasks(() => window),
|
||||
deferredTasks: createChatViewDeferredTasks(() => window),
|
||||
opened: () => true,
|
||||
resumeThread,
|
||||
invalidateResumeWork: vi.fn(),
|
||||
|
|
@ -59,7 +59,7 @@ describe("RestorationController", () => {
|
|||
|
||||
function restoredThreadControllerFixture(overrides: Partial<ConstructorParameters<typeof RestorationController>[0]> = {}) {
|
||||
return new RestorationController({
|
||||
deferredTasks: new ChatViewDeferredTasks(() => window),
|
||||
deferredTasks: createChatViewDeferredTasks(() => window),
|
||||
opened: () => false,
|
||||
resumeThread: vi.fn().mockResolvedValue(undefined),
|
||||
invalidateResumeWork: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ChatMessageScrollIntentController } from "../../../../../src/features/chat/ui/message-stream/scroll-intent-controller";
|
||||
|
||||
describe("ChatMessageScrollIntentController", () => {
|
||||
it("consumes one-shot scroll intents", () => {
|
||||
const controller = new ChatMessageScrollIntentController();
|
||||
|
||||
controller.preservePosition();
|
||||
|
||||
expect(controller.consumeIntent()).toBe("preserve");
|
||||
expect(controller.consumeIntent()).toBe("auto");
|
||||
});
|
||||
|
||||
it("emits a one-shot force-bottom intent", () => {
|
||||
const controller = new ChatMessageScrollIntentController();
|
||||
|
||||
controller.forceBottom();
|
||||
|
||||
expect(controller.consumeIntent()).toBe("force-bottom");
|
||||
});
|
||||
|
||||
it("uses the latest scroll intent before consumption", () => {
|
||||
const controller = new ChatMessageScrollIntentController();
|
||||
|
||||
controller.preservePosition();
|
||||
controller.followBottom();
|
||||
controller.forceBottom();
|
||||
|
||||
expect(controller.consumeIntent()).toBe("force-bottom");
|
||||
expect(controller.consumeIntent()).toBe("auto");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createChatMessageScrollIntentState } from "../../../../../src/features/chat/ui/message-stream/scroll-intent-state";
|
||||
|
||||
describe("createChatMessageScrollIntentState", () => {
|
||||
it("consumes one-shot scroll intents", () => {
|
||||
const scrollIntent = createChatMessageScrollIntentState();
|
||||
|
||||
scrollIntent.preservePosition();
|
||||
|
||||
expect(scrollIntent.consumeIntent()).toBe("preserve");
|
||||
expect(scrollIntent.consumeIntent()).toBe("auto");
|
||||
});
|
||||
|
||||
it("emits a one-shot force-bottom intent", () => {
|
||||
const scrollIntent = createChatMessageScrollIntentState();
|
||||
|
||||
scrollIntent.forceBottom();
|
||||
|
||||
expect(scrollIntent.consumeIntent()).toBe("force-bottom");
|
||||
});
|
||||
|
||||
it("uses the latest scroll intent before consumption", () => {
|
||||
const scrollIntent = createChatMessageScrollIntentState();
|
||||
|
||||
scrollIntent.preservePosition();
|
||||
scrollIntent.followBottom();
|
||||
scrollIntent.forceBottom();
|
||||
|
||||
expect(scrollIntent.consumeIntent()).toBe("force-bottom");
|
||||
expect(scrollIntent.consumeIntent()).toBe("auto");
|
||||
});
|
||||
});
|
||||
|
|
@ -6,11 +6,15 @@ import { act } from "preact/test-utils";
|
|||
import type { Thread } from "../../../../src/domain/threads/model";
|
||||
import { createChatStateStore } from "../../../../src/features/chat/state/reducer";
|
||||
import { runtimeSnapshotForChatState } from "../../../../src/features/chat/runtime/snapshot";
|
||||
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "../../../../src/features/chat/panel/regions/toolbar";
|
||||
import {
|
||||
createToolbarArchiveConfirmState,
|
||||
createToolbarPanelActions,
|
||||
type ToolbarPanelActions,
|
||||
} from "../../../../src/features/chat/panel/regions/toolbar";
|
||||
import type { ChatPanelToolbarPorts } from "../../../../src/features/chat/panel/regions/ports";
|
||||
import type { ChatThreadActions } from "../../../../src/features/chat/threads/actions";
|
||||
import type { ChatThreadActions } from "../../../../src/features/chat/threads/action-context";
|
||||
import { renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/ui/shell";
|
||||
import { chatPanelToolbarRegionNode } from "../../../../src/features/chat/panel/regions/render";
|
||||
import { chatPanelToolbarRegionNode } from "../../../../src/features/chat/panel/regions/toolbar";
|
||||
import { installObsidianDomShims } from "../../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -22,7 +26,7 @@ describe("chat toolbar archive confirmation signal", () => {
|
|||
const container = document.createElement("div");
|
||||
const archiveConfirm = createToolbarArchiveConfirmState();
|
||||
const scheduleRender = vi.fn();
|
||||
const controller = new ToolbarPanelController({
|
||||
const toolbarActions = createToolbarPanelActions({
|
||||
stateStore: store,
|
||||
threadActions: { archiveThread: vi.fn() } as unknown as ChatThreadActions,
|
||||
archiveConfirm,
|
||||
|
|
@ -36,7 +40,7 @@ describe("chat toolbar archive confirmation signal", () => {
|
|||
renderChatPanelShell(container, {
|
||||
stateStore: store,
|
||||
showToolbar: true,
|
||||
toolbarNode: () => chatPanelToolbarRegionNode(toolbarPorts(store, controller)),
|
||||
toolbarNode: () => chatPanelToolbarRegionNode(toolbarPorts(store, toolbarActions)),
|
||||
goalNode: () => null,
|
||||
messageStreamNode: () => <div className="codex-panel__region codex-panel__region--message-stream codex-panel__messages" />,
|
||||
composerNode: () => null,
|
||||
|
|
@ -47,7 +51,7 @@ describe("chat toolbar archive confirmation signal", () => {
|
|||
expect(container.querySelector(".codex-panel__archive-confirm")).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
controller.startArchive("thread-1");
|
||||
toolbarActions.startArchive("thread-1");
|
||||
await settle();
|
||||
});
|
||||
|
||||
|
|
@ -60,7 +64,7 @@ describe("chat toolbar archive confirmation signal", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function toolbarPorts(store: ReturnType<typeof createChatStateStore>, controller: ToolbarPanelController): ChatPanelToolbarPorts {
|
||||
function toolbarPorts(store: ReturnType<typeof createChatStateStore>, toolbarActions: ToolbarPanelActions): ChatPanelToolbarPorts {
|
||||
return {
|
||||
state: {
|
||||
chat: () => store.getState(),
|
||||
|
|
@ -77,8 +81,8 @@ function toolbarPorts(store: ReturnType<typeof createChatStateStore>, controller
|
|||
},
|
||||
view: {
|
||||
toolbar: {
|
||||
archiveConfirmId: () => controller.archiveConfirmId(),
|
||||
archiveConfirmSubscribe: (listener) => controller.onArchiveConfirmChange(listener),
|
||||
archiveConfirmId: () => toolbarActions.archiveConfirmId(),
|
||||
archiveConfirmSubscribe: (listener) => toolbarActions.onArchiveConfirmChange(listener),
|
||||
renameState: () => null,
|
||||
renameSubscribe: () => () => undefined,
|
||||
},
|
||||
|
|
@ -95,7 +99,7 @@ function toolbarPorts(store: ReturnType<typeof createChatStateStore>, controller
|
|||
refreshStatus: vi.fn(),
|
||||
resumeThread: vi.fn(),
|
||||
startArchiveThread: (threadId) => {
|
||||
controller.startArchive(threadId);
|
||||
toolbarActions.startArchive(threadId);
|
||||
},
|
||||
archiveThread: vi.fn(),
|
||||
startRenameThread: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -5,20 +5,20 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import {
|
||||
ChatConnectionWorkTracker,
|
||||
ChatResumeWorkTracker,
|
||||
ChatViewDeferredTasks,
|
||||
createChatViewDeferredTasks,
|
||||
transitionChatConnectionLifecycle,
|
||||
transitionChatResumeLifecycle,
|
||||
transitionRestoredThreadLifecycle,
|
||||
type ActiveChatConnection,
|
||||
} from "../../../src/features/chat/lifecycle";
|
||||
|
||||
describe("ChatViewDeferredTasks", () => {
|
||||
describe("createChatViewDeferredTasks", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("coalesces scheduled renders", async () => {
|
||||
const tasks = new ChatViewDeferredTasks(() => window);
|
||||
const tasks = createChatViewDeferredTasks(() => window);
|
||||
const render = vi.fn();
|
||||
|
||||
tasks.scheduleRender(render);
|
||||
|
|
@ -30,7 +30,7 @@ describe("ChatViewDeferredTasks", () => {
|
|||
});
|
||||
|
||||
it("clears scheduled deferred work", async () => {
|
||||
const tasks = new ChatViewDeferredTasks(() => window);
|
||||
const tasks = createChatViewDeferredTasks(() => window);
|
||||
const render = vi.fn();
|
||||
const diagnostics = vi.fn();
|
||||
const hydration = vi.fn();
|
||||
|
|
|
|||
|
|
@ -30,15 +30,15 @@ import type {
|
|||
AppServerClientHandlers,
|
||||
AppServerStartStructuredTurnOptions,
|
||||
} from "../../../src/app-server/connection/client";
|
||||
import type { AppServerInitialization } from "../../../src/app-server/protocol/initialization";
|
||||
import type { RequestId, ServerNotification } from "../../../src/app-server/connection/rpc-messages";
|
||||
import type { TurnItem, TurnRecord } from "../../../src/app-server/protocol/turn";
|
||||
import type { ModelMetadata, ReasoningEffort } from "../../../src/domain/catalog/metadata";
|
||||
import type { ServerInitialization } from "../../../src/domain/server/initialization";
|
||||
import type { ComposerSendKeyEvent } from "../../../src/shared/ui/keyboard";
|
||||
import { deferred } from "../../support/async";
|
||||
import { installObsidianDomShims } from "../../support/dom";
|
||||
|
||||
type InitializeResponse = AppServerInitialization;
|
||||
type InitializeResponse = ServerInitialization;
|
||||
type ModelListResponse = Awaited<ReturnType<AppServerClient["listModels"]>>;
|
||||
type ThreadStartResponse = Awaited<ReturnType<AppServerClient["startEphemeralThread"]>>;
|
||||
type Turn = TurnRecord;
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import type {
|
|||
AppServerClientHandlers,
|
||||
AppServerStartStructuredTurnOptions,
|
||||
} from "../../../src/app-server/connection/client";
|
||||
import type { AppServerInitialization } from "../../../src/app-server/protocol/initialization";
|
||||
import type { TurnItem, TurnRecord } from "../../../src/app-server/protocol/turn";
|
||||
import type { RequestId, ServerNotification } from "../../../src/app-server/connection/rpc-messages";
|
||||
import type { ModelMetadata } from "../../../src/domain/catalog/metadata";
|
||||
import type { ServerInitialization } from "../../../src/domain/server/initialization";
|
||||
import {
|
||||
generateThreadTitleWithCodex,
|
||||
threadTitleFromGenerationTurn,
|
||||
|
|
@ -27,7 +27,7 @@ import {
|
|||
threadTitlePrompt,
|
||||
} from "../../../src/features/thread-title/model";
|
||||
|
||||
type InitializeResponse = AppServerInitialization;
|
||||
type InitializeResponse = ServerInitialization;
|
||||
type ModelListResponse = Awaited<ReturnType<AppServerClient["listModels"]>>;
|
||||
type ThreadStartResponse = Awaited<ReturnType<AppServerClient["startEphemeralThread"]>>;
|
||||
type Turn = TurnRecord;
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
ThreadsViewDeferredTasks,
|
||||
createThreadsViewDeferredTasks,
|
||||
transitionThreadsViewConnectionLifecycle,
|
||||
transitionThreadsViewRefreshLifecycle,
|
||||
type ActiveThreadsViewConnection,
|
||||
type ActiveThreadsViewRefresh,
|
||||
} from "../../../src/features/threads-view/view-lifecycle";
|
||||
|
||||
describe("ThreadsViewDeferredTasks", () => {
|
||||
describe("createThreadsViewDeferredTasks", () => {
|
||||
it("coalesces render and refresh callbacks", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const tasks = new ThreadsViewDeferredTasks(() => window);
|
||||
const tasks = createThreadsViewDeferredTasks(() => window);
|
||||
const render = vi.fn();
|
||||
const refresh = vi.fn();
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ describe("ThreadsViewDeferredTasks", () => {
|
|||
it("clears pending callbacks", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const tasks = new ThreadsViewDeferredTasks(() => window);
|
||||
const tasks = createThreadsViewDeferredTasks(() => window);
|
||||
const render = vi.fn();
|
||||
const refresh = vi.fn();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { Thread } from "../src/domain/threads/model";
|
|||
import type { SharedAppServerCache } from "../src/app-server/services/shared-cache";
|
||||
import type { SharedAppServerCacheContext } from "../src/app-server/services/shared-cache-state";
|
||||
import type { WorkspacePanelCoordinator } from "../src/workspace/panel-coordinator";
|
||||
import type { ThreadSurfaceCoordinator } from "../src/workspace/thread-surface-coordinator";
|
||||
import type { ThreadSurfaceActions } from "../src/workspace/thread-surface-actions";
|
||||
import { installObsidianDomShims } from "./support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -21,8 +21,8 @@ function panels(plugin: CodexPanelPlugin): WorkspacePanelCoordinator {
|
|||
return (plugin as unknown as { panels: WorkspacePanelCoordinator }).panels;
|
||||
}
|
||||
|
||||
function threadSurfaces(plugin: CodexPanelPlugin): ThreadSurfaceCoordinator {
|
||||
return (plugin as unknown as { threadSurfaces: ThreadSurfaceCoordinator }).threadSurfaces;
|
||||
function threadSurfaces(plugin: CodexPanelPlugin): ThreadSurfaceActions {
|
||||
return (plugin as unknown as { threadSurfaces: ThreadSurfaceActions }).threadSurfaces;
|
||||
}
|
||||
|
||||
function sharedAppServerCache(plugin: CodexPanelPlugin): SharedAppServerCache {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { describe, expect, it, vi } from "vitest";
|
|||
|
||||
import { VIEW_TYPE_CODEX_THREADS } from "../../src/constants";
|
||||
import { CodexThreadsView } from "../../src/features/threads-view/view";
|
||||
import { ThreadSurfaceCoordinator } from "../../src/workspace/thread-surface-coordinator";
|
||||
import { createThreadSurfaceActions } from "../../src/workspace/thread-surface-actions";
|
||||
import type { OpenCodexPanelSnapshot } from "../../src/workspace/open-panel-snapshot";
|
||||
|
||||
describe("ThreadSurfaceCoordinator", () => {
|
||||
describe("createThreadSurfaceActions", () => {
|
||||
it("falls back to the threads view when no connected chat panel can refresh shared threads", () => {
|
||||
const disconnectedPanelRefresh = vi.fn().mockResolvedValue(undefined);
|
||||
const threadsRefresh = vi.fn().mockResolvedValue(undefined);
|
||||
const coordinator = new ThreadSurfaceCoordinator({
|
||||
const threadSurfaces = createThreadSurfaceActions({
|
||||
app: {
|
||||
workspace: {
|
||||
getLeavesOfType: vi.fn((type: string) =>
|
||||
|
|
@ -28,7 +28,7 @@ describe("ThreadSurfaceCoordinator", () => {
|
|||
refreshThreadSurfaces: vi.fn(),
|
||||
});
|
||||
|
||||
coordinator.refreshSharedThreadListFromOpenSurface();
|
||||
threadSurfaces.refreshSharedThreadListFromOpenSurface();
|
||||
|
||||
expect(disconnectedPanelRefresh).not.toHaveBeenCalled();
|
||||
expect(threadsRefresh).toHaveBeenCalledOnce();
|
||||
|
|
@ -38,7 +38,7 @@ describe("ThreadSurfaceCoordinator", () => {
|
|||
const disconnectedPanelRefresh = vi.fn().mockResolvedValue(undefined);
|
||||
const connectedPanelRefresh = vi.fn().mockResolvedValue(undefined);
|
||||
const threadsRefresh = vi.fn().mockResolvedValue(undefined);
|
||||
const coordinator = new ThreadSurfaceCoordinator({
|
||||
const threadSurfaces = createThreadSurfaceActions({
|
||||
app: {
|
||||
workspace: {
|
||||
getLeavesOfType: vi.fn((type: string) =>
|
||||
|
|
@ -61,7 +61,7 @@ describe("ThreadSurfaceCoordinator", () => {
|
|||
refreshThreadSurfaces: vi.fn(),
|
||||
});
|
||||
|
||||
coordinator.refreshSharedThreadListFromOpenSurface();
|
||||
threadSurfaces.refreshSharedThreadListFromOpenSurface();
|
||||
|
||||
expect(disconnectedPanelRefresh).not.toHaveBeenCalled();
|
||||
expect(connectedPanelRefresh).toHaveBeenCalledOnce();
|
||||
Loading…
Reference in a new issue