From ca4db5f0cc3b51fa9b8ecb6cc900398f7fa640b0 Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 17 Jun 2026 10:41:38 +0900 Subject: [PATCH] Extract chat panel session composition parts --- src/features/chat/host/connection-bundle.ts | 238 +++++ src/features/chat/host/session-parts.ts | 790 ++++++++++++++++ src/features/chat/host/session.ts | 942 +------------------- 3 files changed, 1052 insertions(+), 918 deletions(-) create mode 100644 src/features/chat/host/connection-bundle.ts create mode 100644 src/features/chat/host/session-parts.ts diff --git a/src/features/chat/host/connection-bundle.ts b/src/features/chat/host/connection-bundle.ts new file mode 100644 index 00000000..20c12bee --- /dev/null +++ b/src/features/chat/host/connection-bundle.ts @@ -0,0 +1,238 @@ +import { Notice } from "obsidian"; + +import type { AppServerClient } from "../../../app-server/connection/client"; +import type { ConnectionManager } from "../../../app-server/connection/connection-manager"; +import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; +import { + createChatConnectionController, + handleChatConnectionExit, + type ChatConnectionController, +} from "../application/connection/connection-controller"; +import type { ChatViewDeferredTasks } from "../application/lifecycle"; +import type { ChatConnectionPhase } from "../application/state/root-reducer"; +import type { ChatStateStore } from "../application/state/store"; +import type { createThreadGoalSyncActions } from "../application/threads/goal-actions"; +import type { AutoTitleController } from "../application/threads/auto-title-controller"; +import { runtimeSnapshotForChatState } from "../application/runtime/snapshot"; +import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics"; +import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../app-server/actions/metadata"; +import { createChatServerThreadActions, type ChatServerThreadActions } from "../app-server/actions/threads"; +import { ChatInboundController } from "../app-server/inbound/controller"; +import type { ChatPanelEnvironment } from "./runtime"; + +export type CurrentAppServerClient = () => AppServerClient | null; + +type RespondRequestId = Parameters[0]; +type RejectRequestId = Parameters[0]; +type ChatPanelGoalSyncActions = ReturnType; + +interface ChatPanelConnectionStatus { + set: (statusText: string, phase?: ChatConnectionPhase) => void; + addSystemMessage: (text: string) => void; +} + +interface ChatPanelConnectionBundleInput { + connection: ConnectionManager; + currentClient: CurrentAppServerClient; + status: ChatPanelConnectionStatus; + goalSync: ChatPanelGoalSyncActions; + autoTitle: AutoTitleController; +} + +interface ChatPanelConnectionBundleHost { + environment: ChatPanelEnvironment; + stateStore: ChatStateStore; + connectionWork: ConnectionWorkTracker; + deferredTasks: ChatViewDeferredTasks; + invalidateResumeWork: () => void; + loadSharedThreadList: () => Promise; + deferLiveStateRefresh: () => void; + refreshTabHeader: () => void; + refreshLiveState: () => void; +} + +export interface ChatPanelConnectionBundle { + connection: { + manager: ConnectionManager; + controller: ChatConnectionController; + }; + inboundController: ChatInboundController; + serverActions: { + threads: ChatServerThreadActions; + metadata: ChatServerMetadataActions; + diagnostics: ChatServerDiagnosticsActions; + }; +} + +function respondToCurrentServerRequest(currentClient: CurrentAppServerClient, requestId: RespondRequestId, result: unknown): boolean { + try { + const client = currentClient(); + client?.respondToServerRequest(requestId, result); + return Boolean(client); + } catch { + return false; + } +} + +function rejectCurrentServerRequest( + currentClient: CurrentAppServerClient, + requestId: RejectRequestId, + code: number, + message: string, +): boolean { + try { + const client = currentClient(); + client?.rejectServerRequest(requestId, code, message); + return Boolean(client); + } catch { + return false; + } +} + +export function createConnectionBundle( + host: ChatPanelConnectionBundleHost, + input: ChatPanelConnectionBundleInput, +): ChatPanelConnectionBundle { + const { environment, stateStore } = host; + const { connection, currentClient, status, goalSync, autoTitle } = input; + const serverMetadata = createChatServerMetadataActions({ + stateStore, + vaultPath: environment.plugin.settingsRef.vaultPath, + currentClient, + updateAppServerMetadata: (updater) => environment.plugin.appServerData.updateAppServerMetadata(updater), + appServerMetadataSnapshot: () => environment.plugin.appServerData.appServerMetadataSnapshot(), + refreshAppServerMetadata: (options) => environment.plugin.appServerData.refreshAppServerMetadata(options), + }); + const serverDiagnostics = createChatServerDiagnosticsActions({ + stateStore, + vaultPath: environment.plugin.settingsRef.vaultPath, + currentClient, + updateAppServerMetadata: (updater) => environment.plugin.appServerData.updateAppServerMetadata(updater), + appServerMetadataSnapshot: () => environment.plugin.appServerData.appServerMetadataSnapshot(), + }); + const serverThreads = createChatServerThreadActions({ + stateStore, + vaultPath: environment.plugin.settingsRef.vaultPath, + currentClient, + runtimeSnapshotForState: runtimeSnapshotForChatState, + recordStartedThread: (thread) => { + environment.plugin.threadCatalog.upsertFromAppServer(thread); + }, + syncThreadGoal: (threadId) => { + void goalSync.syncThreadGoal(threadId); + }, + }); + const inboundController = new ChatInboundController(stateStore, { + refreshActiveThreads: () => { + void host.loadSharedThreadList(); + }, + refreshRateLimits: () => { + void serverMetadata.refreshRateLimits({ preserveExistingOnFailure: true }); + }, + refreshSkills: (forceReload) => void serverMetadata.refreshSkills(forceReload), + applyAppServerMetadataSnapshot: () => { + serverMetadata.applyAppServerMetadataSnapshot(); + }, + maybeNameThread: (threadId, turnId, completedSummary) => { + autoTitle.maybeAutoTitleThread(threadId, turnId, completedSummary); + }, + upsertActiveThread: (thread) => { + environment.plugin.threadCatalog.upsertFromAppServer(thread); + }, + applyThreadArchived: (threadId) => { + environment.plugin.threadCatalog.recordThreadArchived(threadId); + }, + recordActiveThreadDeleted: (threadId) => { + environment.plugin.threadCatalog.recordThreadDeleted(threadId); + }, + applyThreadRenamed: (threadId, name) => { + environment.plugin.threadCatalog.recordThreadRenamed(threadId, name); + }, + recordMcpStartupStatus: (name, mcpStatus, message) => { + serverDiagnostics.recordMcpStartupStatus(name, mcpStatus, message); + }, + respondToServerRequest: (requestId, result) => respondToCurrentServerRequest(currentClient, requestId, result), + rejectServerRequest: (requestId, code, message) => rejectCurrentServerRequest(currentClient, requestId, code, message), + }); + const connectionExitHost = { + stateStore, + connectionWork: host.connectionWork, + invalidateResumeWork: () => { + host.invalidateResumeWork(); + }, + setStatus: status.set, + resetThreadTurnPresence: (hadTurns: boolean) => { + autoTitle.resetThreadTurnPresence(hadTurns); + }, + refreshLiveState: () => { + host.refreshLiveState(); + }, + }; + const connectionController = createChatConnectionController({ + ...connectionExitHost, + connection: { + connect: () => + connection.connect({ + onNotification: (notification) => { + inboundController.handleNotification(notification); + host.deferLiveStateRefresh(); + }, + onServerRequest: (request) => { + inboundController.handleServerRequest(request); + host.deferLiveStateRefresh(); + }, + onLog: (message) => { + inboundController.handleAppServerLog(message); + }, + onExit: () => { + handleChatConnectionExit(connectionExitHost); + }, + }), + currentClient, + isConnected: () => connection.isConnected(), + }, + metadata: { + refreshAppServerMetadata: () => serverMetadata.refreshAppServerMetadata(), + refreshSkills: (forceReload) => serverMetadata.refreshSkills(forceReload), + }, + diagnostics: { + refreshDiagnosticProbes: (options) => serverDiagnostics.refreshDiagnosticProbes(options), + }, + loadSharedThreadList: () => host.loadSharedThreadList(), + scheduleDeferredDiagnostics: () => { + host.deferredTasks.scheduleDiagnostics(() => { + if (connection.isConnected()) { + void serverDiagnostics.refreshDiagnosticProbes({ appServerMetadataSnapshot: true }); + } + }); + }, + clearDeferredDiagnostics: () => { + host.deferredTasks.clearDiagnostics(); + }, + refreshTabHeader: () => { + host.refreshTabHeader(); + }, + setStatus: status.set, + addSystemMessage: status.addSystemMessage, + configuredCommand: () => environment.plugin.settingsRef.settings.codexPath, + refreshLiveState: () => { + host.refreshLiveState(); + }, + notifyConnectionFailed: () => { + new Notice("Codex app-server connection failed."); + }, + }); + + return { + connection: { + manager: connection, + controller: connectionController, + }, + inboundController, + serverActions: { + threads: serverThreads, + metadata: serverMetadata, + diagnostics: serverDiagnostics, + }, + }; +} diff --git a/src/features/chat/host/session-parts.ts b/src/features/chat/host/session-parts.ts new file mode 100644 index 00000000..43a91bd0 --- /dev/null +++ b/src/features/chat/host/session-parts.ts @@ -0,0 +1,790 @@ +import { Notice } from "obsidian"; + +import { ConnectionManager } from "../../../app-server/connection/connection-manager"; +import type { MessageStreamItem, MessageStreamNoticeSection } from "../domain/message-stream/items"; +import { createThreadOperations, type ThreadOperations } from "../../threads/thread-operations"; +import { createThreadTitleService, type ThreadTitleService } from "../../threads/thread-title-service"; +import { PendingRequestController } from "../application/pending-requests/controller"; +import { + createConversationTurnActions, + type ConversationTurnActions as ChatPanelConversationTurnActions, +} from "../application/conversation/composition"; +import type { ComposerSubmitActions } from "../application/conversation/composer-submit-actions"; +import { reconnectPanel, type ChatReconnectActionsHost } from "../application/connection/reconnect-actions"; +import { runtimeSnapshotForChatState } from "../application/runtime/snapshot"; +import { createChatRuntimeSettingsActions } from "../application/runtime/settings-actions"; +import { activeTurnId, type ChatConnectionPhase } from "../application/state/root-reducer"; +import { messageStreamItems } from "../application/state/message-stream"; +import type { ChatStateStore } from "../application/state/store"; +import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../application/lifecycle"; +import { createGoalActions, createThreadGoalSyncActions } from "../application/threads/goal-actions"; +import { AutoTitleController } from "../application/threads/auto-title-controller"; +import type { HistoryController } from "../application/threads/history-controller"; +import type { IdentitySync } from "../application/threads/identity-sync"; +import { createThreadLifecycleParts } from "../application/threads/lifecycle-parts"; +import { + activeThreadRenameTitleContext, + ThreadRenameEditorController, + type ThreadRenameEditorController as ThreadRenameEditorControllerInstance, +} from "../application/threads/rename-editor-controller"; +import type { RestorationController } from "../application/threads/restoration-controller"; +import type { ResumeController } from "../application/threads/resume-controller"; +import { createSelectionActions } from "../application/threads/selection-actions"; +import { createThreadManagementActions, type ThreadManagementActionsHost } from "../application/threads/thread-management-actions"; +import type { ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics"; +import type { ChatServerMetadataActions } from "../app-server/actions/metadata"; +import type { ChatServerThreadActions } from "../app-server/actions/threads"; +import type { ChatInboundController } from "../app-server/inbound/controller"; +import { ChatComposerController } from "../panel/composer-controller"; +import { chatPanelComposerProjection, type ChatPanelComposerSurface } from "../panel/surface/composer-projection"; +import { createChatPanelGoalSurface, type ChatPanelGoalSurface } from "../panel/surface/goal-projection"; +import { MessageStreamPresenter } from "../panel/surface/message-stream-presenter"; +import { MessageStreamScrollBridge, type ChatMessageScrollIntentState } from "../panel/surface/message-stream-scroll"; +import type { ChatPanelToolbarSurface } from "../panel/surface/toolbar-projection"; +import { createChatPanelToolbarActions, createToolbarPanelActions, type ToolbarPanelActions } from "../panel/toolbar-actions"; +import type { ToolbarActions } from "../ui/toolbar"; +import { pendingRequestsSignature } from "../domain/pending-requests/signatures"; +import { currentModel, runtimeConfigOrDefault } from "../domain/runtime/effective"; +import { threadTitleContextFromMessageStreamItems } from "../application/threads/title-context"; +import { normalizeExplicitThreadName } from "../../../domain/threads/model"; +import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; +import { collaborationModeLabel as formatCollaborationModeLabel } from "../presentation/runtime/messages"; +import type { ChatPanelEnvironment } from "./runtime"; +import { createConnectionBundle, type ChatPanelConnectionBundle, type CurrentAppServerClient } from "./connection-bundle"; + +export interface ChatPanelSessionParts { + connection: { + manager: ConnectionManager; + controller: ChatPanelConnectionBundle["connection"]["controller"]; + }; + serverActions: { + threads: ChatServerThreadActions; + metadata: ChatServerMetadataActions; + diagnostics: ChatServerDiagnosticsActions; + }; + thread: { + history: HistoryController; + resume: ResumeController; + restoration: RestorationController; + identity: IdentitySync; + rename: ThreadRenameEditorControllerInstance; + }; + toolbar: { + panels: ToolbarPanelActions; + actions: ToolbarActions; + }; + composer: { + controller: ChatComposerController; + submission: ComposerSubmitActions; + }; + render: { + messageStreamPresenter: MessageStreamPresenter; + }; + surface: { + toolbar: ChatPanelToolbarSurface; + goal: ChatPanelGoalSurface; + composer: ChatPanelComposerSurface; + }; +} + +export interface ChatPanelSessionStatus { + set: (statusText: string, phase?: ChatConnectionPhase) => void; + addSystemMessage: (text: string) => void; + addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void; +} + +interface ChatPanelSessionPartsHost { + environment: ChatPanelEnvironment; + stateStore: ChatStateStore; + deferredTasks: ChatViewDeferredTasks; + resumeWork: ChatResumeWorkTracker; + connectionWork: ConnectionWorkTracker; + messageScrollIntent: ChatMessageScrollIntentState; + getOpened: () => boolean; + getClosing: () => boolean; + invalidateResumeWork: () => void; + loadSharedThreadList: () => Promise; + notifyActiveThreadIdentityChanged: () => void; + refreshTabHeader: () => void; + refreshLiveState: () => void; + deferLiveStateRefresh: () => void; + startNewThread: () => Promise; + statusSummaryLines: () => string[]; + modelStatusLines: () => string[]; + effortStatusLines: () => string[]; + connectionDiagnosticDetails: () => MessageStreamNoticeSection[]; +} + +type ChatPanelGoalSyncActions = ReturnType; +type ChatPanelRuntimeSettingsActions = ReturnType; +type ChatPanelGoalActions = ReturnType; +type ChatPanelThreadLifecycle = ReturnType; +type ChatPanelThreadActions = ReturnType; +type ChatPanelSelectionActions = ReturnType; + +interface ChatPanelThreadActionParts { + actions: ChatPanelThreadActions; + toolbarPanels: ToolbarPanelActions; + selection: ChatPanelSelectionActions; +} + +interface ChatPanelComposerAndTurnParts { + pendingRequests: PendingRequestController; + reconnect: () => Promise; + turnActions: ChatPanelConversationTurnActions; +} + +interface ChatPanelSurfacePresenterParts { + toolbarActions: ToolbarActions; + toolbarSurface: ChatPanelToolbarSurface; + goalSurface: ChatPanelGoalSurface; + messageStreamPresenter: MessageStreamPresenter; +} + +export function createChatPanelSessionParts(host: ChatPanelSessionPartsHost, status: ChatPanelSessionStatus): ChatPanelSessionParts { + const { environment, stateStore } = host; + const connection = createConnectionManager(environment); + const currentClient = () => connection.currentClient(); + const titleService = createSessionThreadTitleService(host, currentClient); + const autoTitle = createSessionAutoTitleController(host, currentClient, titleService); + const goalSync = createSessionGoalSyncActions(host, currentClient, status); + const serverParts = createConnectionBundle(host, { + connection, + currentClient, + goalSync, + autoTitle, + status, + }); + const { + connection: { controller }, + inboundController, + } = serverParts; + const connectionController = controller; + const { threads: serverThreads, diagnostics: serverDiagnostics } = serverParts.serverActions; + const ensureConnected = () => connectionController.ensureConnected(); + const refreshActiveThreads = () => connectionController.refreshActiveThreads(); + const threadOperations = createSessionThreadOperations(environment, currentClient, ensureConnected); + const runtimeSettings = createSessionRuntimeSettingsActions(host, currentClient, status); + const goals = createSessionGoalActions(host, currentClient, ensureConnected, status); + const rename = createSessionThreadRenameEditor(stateStore, threadOperations, titleService, ensureConnected, status); + const threadLifecycle = createSessionThreadLifecycle(host, currentClient, ensureConnected, status, goals, autoTitle); + const { history, identity, restoration, resume } = threadLifecycle; + + const composerSurface = createSessionComposerSurface(threadLifecycle, runtimeSettings); + const messageStreamScrollBridge = new MessageStreamScrollBridge(); + const composerController = createSessionComposerController(host, composerSurface, runtimeSettings, messageStreamScrollBridge); + const threadActionParts = createThreadActionParts(host, { + operations: threadOperations, + ensureConnected, + currentClient, + status, + composerController, + resume, + refreshActiveThreads, + }); + const composerAndTurn = createComposerAndTurnActions(host, { + connection, + ensureConnected, + currentClient, + status, + inboundController, + threadLifecycle, + threadActions: threadActionParts.actions, + selection: threadActionParts.selection, + composerController, + runtimeSettings, + serverThreads, + serverDiagnostics, + goals, + autoTitle, + }); + const surfaceAndPresenter = createSurfacesAndPresenter(host, { + connection, + connectionController, + inboundController, + serverThreads, + goals, + rename, + threadActions: threadActionParts.actions, + toolbarPanels: threadActionParts.toolbarPanels, + selection: threadActionParts.selection, + reconnect: composerAndTurn.reconnect, + history, + pendingRequests: composerAndTurn.pendingRequests, + turnActions: composerAndTurn.turnActions, + messageStreamScrollBridge, + }); + + return { + connection: { + manager: connection, + controller: connectionController, + }, + serverActions: serverParts.serverActions, + thread: { + history, + resume, + restoration, + identity, + rename, + }, + toolbar: { + panels: threadActionParts.toolbarPanels, + actions: surfaceAndPresenter.toolbarActions, + }, + composer: { + controller: composerController, + submission: composerAndTurn.turnActions.composerSubmit, + }, + render: { + messageStreamPresenter: surfaceAndPresenter.messageStreamPresenter, + }, + surface: { + toolbar: surfaceAndPresenter.toolbarSurface, + goal: surfaceAndPresenter.goalSurface, + composer: composerSurface, + }, + }; +} + +function createConnectionManager(environment: ChatPanelEnvironment): ConnectionManager { + return new ConnectionManager(() => environment.plugin.settingsRef.settings.codexPath, environment.plugin.settingsRef.vaultPath); +} + +function createSessionThreadTitleService(host: ChatPanelSessionPartsHost, currentClient: CurrentAppServerClient): ThreadTitleService { + const { environment, stateStore } = host; + return createThreadTitleService({ + settings: { + current: () => environment.plugin.settingsRef.settings, + vaultPath: environment.plugin.settingsRef.vaultPath, + }, + currentClient, + visibleContext: (threadId) => activeThreadRenameTitleContext(stateStore.getState(), threadId), + visibleCompletedTurnContext: (turnId) => + threadTitleContextFromMessageStreamItems(turnId, messageStreamItems(stateStore.getState().messageStream)), + }); +} + +function createSessionAutoTitleController( + host: ChatPanelSessionPartsHost, + currentClient: CurrentAppServerClient, + titleService: ThreadTitleService, +): AutoTitleController { + return new AutoTitleController({ + stateStore: host.stateStore, + completedTurnTitleContext: (turnId, completedSummary) => titleService.completedTurnContext(turnId, completedSummary), + generateTitleFromContext: (context) => titleService.generate(context), + renameGeneratedTitle: async (threadId, title, options) => { + const name = normalizeExplicitThreadName(title); + if (!name) return false; + const client = currentClient(); + if (!client) return false; + + await client.setThreadName(threadId, name); + if (currentClient() !== client) return false; + if (options.shouldPublish()) { + host.environment.plugin.threadCatalog.recordThreadRenamed(threadId, name); + } + return true; + }, + }); +} + +function createSessionGoalSyncActions( + host: ChatPanelSessionPartsHost, + currentClient: CurrentAppServerClient, + status: ChatPanelSessionStatus, +): ChatPanelGoalSyncActions { + return createThreadGoalSyncActions({ + stateStore: host.stateStore, + currentClient, + addSystemMessage: (text) => { + status.addSystemMessage(text); + }, + addGoalEvent: (item) => { + host.stateStore.dispatch({ type: "message-stream/item-upserted", item }); + }, + refreshLiveState: () => { + host.refreshLiveState(); + }, + }); +} + +function createSessionThreadOperations( + environment: ChatPanelEnvironment, + currentClient: CurrentAppServerClient, + ensureConnected: () => Promise, +): ThreadOperations { + return createThreadOperations({ + connection: { + ensureConnected, + currentClient, + }, + settings: { + current: () => environment.plugin.settingsRef.settings, + vaultPath: environment.plugin.settingsRef.vaultPath, + }, + archiveAdapter: environment.obsidian.archiveAdapter, + catalog: environment.plugin.threadCatalog, + notice: (text) => { + new Notice(text); + }, + }); +} + +function createSessionRuntimeSettingsActions( + host: ChatPanelSessionPartsHost, + currentClient: CurrentAppServerClient, + status: ChatPanelSessionStatus, +): ChatPanelRuntimeSettingsActions { + return createChatRuntimeSettingsActions({ + stateStore: host.stateStore, + currentClient, + runtimeSnapshotForState: runtimeSnapshotForChatState, + collaborationModeLabel: () => collaborationModeLabel(host.stateStore), + addSystemMessage: (text) => { + status.addSystemMessage(text); + }, + }); +} + +function createSessionGoalActions( + host: ChatPanelSessionPartsHost, + currentClient: CurrentAppServerClient, + ensureConnected: () => Promise, + status: ChatPanelSessionStatus, +): ChatPanelGoalActions { + return createGoalActions({ + stateStore: host.stateStore, + currentClient, + ensureConnected, + addSystemMessage: (text) => { + status.addSystemMessage(text); + }, + addGoalEvent: (item) => { + host.stateStore.dispatch({ type: "message-stream/item-upserted", item }); + }, + refreshLiveState: () => { + host.refreshLiveState(); + }, + }); +} + +function createSessionThreadRenameEditor( + stateStore: ChatStateStore, + operations: ThreadOperations, + titleService: ThreadTitleService, + ensureConnected: () => Promise, + status: ChatPanelSessionStatus, +): ThreadRenameEditorControllerInstance { + return new ThreadRenameEditorController({ + stateStore, + ensureConnected, + addSystemMessage: status.addSystemMessage, + renameThread: (threadId, value, options) => operations.renameThread(threadId, value, options), + generateThreadTitle: (threadId) => titleService.generateTitle(threadId), + }); +} + +function createSessionThreadLifecycle( + host: ChatPanelSessionPartsHost, + currentClient: CurrentAppServerClient, + ensureConnected: () => Promise, + status: ChatPanelSessionStatus, + goals: ChatPanelGoalActions, + autoTitle: AutoTitleController, +): ChatPanelThreadLifecycle { + return createThreadLifecycleParts({ + settingsRef: host.environment.plugin.settingsRef, + stateStore: host.stateStore, + client: { + currentClient, + ensureConnected, + }, + lifecycle: { + deferredTasks: host.deferredTasks, + resumeWork: host.resumeWork, + getOpened: host.getOpened, + getClosing: host.getClosing, + }, + thread: { + notifyIdentityChanged: () => { + host.notifyActiveThreadIdentityChanged(); + }, + refreshTabHeader: () => { + host.refreshTabHeader(); + }, + }, + status, + liveState: { + refresh: () => { + host.refreshLiveState(); + }, + }, + scroll: { + preservePosition: () => { + host.messageScrollIntent.preservePosition(); + }, + forceBottom: () => { + host.messageScrollIntent.forceBottom(); + }, + }, + goals, + resetThreadTurnPresence: (hadTurns) => { + autoTitle.resetThreadTurnPresence(hadTurns); + }, + }); +} + +function createSessionComposerSurface( + threadLifecycle: ChatPanelThreadLifecycle, + runtimeSettings: ChatPanelRuntimeSettingsActions, +): ChatPanelComposerSurface { + return { + thread: { + restoredPlaceholder: () => threadLifecycle.restoration.placeholder(), + }, + runtime: { + requestModel: (model) => runtimeSettings.requestModelFromUi(model), + requestReasoningEffort: (effort) => runtimeSettings.requestReasoningEffortFromUi(effort), + }, + }; +} + +function createSessionComposerController( + host: ChatPanelSessionPartsHost, + composerSurface: ChatPanelComposerSurface, + runtimeSettings: ChatPanelRuntimeSettingsActions, + messageStreamScrollBridge: MessageStreamScrollBridge, +): ChatComposerController { + const { environment, stateStore } = host; + return new ChatComposerController({ + app: environment.obsidian.app, + stateStore, + viewId: environment.obsidian.viewId, + sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut, + scrollThreadFromComposerEdges: () => environment.plugin.settingsRef.settings.scrollThreadFromComposerEdges, + canInterrupt: (state) => { + return state.turn.lifecycle.kind !== "idle" && Boolean(state.activeThread.id && activeTurnId(state)); + }, + composerProjection: (state) => chatPanelComposerProjection(composerSurface, state), + currentModelForSuggestions: () => { + const current = stateStore.getState(); + return currentModel(runtimeSnapshotForChatState(current), runtimeConfigOrDefault(current.connection.runtimeConfig)); + }, + threadScrollFromComposer: (action) => { + messageStreamScrollBridge.scrollFromComposer(action); + }, + togglePlan: () => void runtimeSettings.toggleCollaborationMode(), + toggleAutoReview: () => void runtimeSettings.toggleAutoReview(), + toggleFast: () => void runtimeSettings.toggleFastMode(), + onDraftChange: () => { + host.refreshLiveState(); + }, + onHeightChange: () => { + messageStreamScrollBridge.repinMessageStreamToBottomIfPinned(); + }, + }); +} + +function createThreadActionParts( + host: ChatPanelSessionPartsHost, + input: { + operations: ThreadOperations; + ensureConnected: () => Promise; + currentClient: CurrentAppServerClient; + status: ChatPanelSessionStatus; + composerController: ChatComposerController; + resume: ResumeController; + refreshActiveThreads: () => Promise; + }, +): ChatPanelThreadActionParts { + const { operations, ensureConnected, currentClient, status, composerController, resume, refreshActiveThreads } = input; + const { environment, stateStore } = host; + const threadManagementHost: ThreadManagementActionsHost = { + stateStore, + vaultPath: environment.plugin.settingsRef.vaultPath, + operations, + ensureConnected, + currentClient, + addSystemMessage: status.addSystemMessage, + setStatus: status.set, + setComposerText: (text) => { + composerController.setDraft(text, { focus: true }); + }, + openThreadInNewView: (threadId) => environment.plugin.workspace.openThreadInNewView(threadId), + openThreadInCurrentPanel: (threadId) => resume.resumeThread(threadId), + notifyActiveThreadIdentityChanged: () => { + host.notifyActiveThreadIdentityChanged(); + }, + refreshAfterThreadMutation: async () => { + await refreshActiveThreads(); + }, + recordForkedThread: (thread) => { + environment.plugin.threadCatalog.upsertFromAppServer(thread); + }, + }; + const actions = createThreadManagementActions(threadManagementHost); + const toolbarPanels = createToolbarPanelActions({ + stateStore, + threadActions: actions, + }); + const selection = createSelectionActions({ + stateStore, + closeForThreadSelection: () => { + toolbarPanels.closeForThreadSelection(); + }, + focusThreadInOpenView: (threadId) => environment.plugin.workspace.focusThreadInOpenView(threadId), + resumeThread: (threadId) => resume.resumeThread(threadId), + addSystemMessage: status.addSystemMessage, + }); + return { actions, toolbarPanels, selection }; +} + +function createComposerAndTurnActions( + host: ChatPanelSessionPartsHost, + input: { + connection: ConnectionManager; + ensureConnected: () => Promise; + currentClient: CurrentAppServerClient; + status: ChatPanelSessionStatus; + inboundController: ChatInboundController; + threadLifecycle: ChatPanelThreadLifecycle; + threadActions: ChatPanelThreadActions; + selection: ChatPanelSelectionActions; + composerController: ChatComposerController; + runtimeSettings: ChatPanelRuntimeSettingsActions; + serverThreads: ChatServerThreadActions; + serverDiagnostics: ChatServerDiagnosticsActions; + goals: ChatPanelGoalActions; + autoTitle: AutoTitleController; + }, +): ChatPanelComposerAndTurnParts { + const { + connection, + ensureConnected, + currentClient, + status, + inboundController, + threadLifecycle, + threadActions, + selection, + composerController, + runtimeSettings, + serverThreads, + serverDiagnostics, + goals, + autoTitle, + } = input; + const pendingRequests = new PendingRequestController({ + stateStore: host.stateStore, + responder: inboundController, + composerHasFocus: () => composerController.hasFocus(), + refreshLiveState: () => { + host.refreshLiveState(); + }, + }); + const reconnectHost: ChatReconnectActionsHost = { + stateStore: host.stateStore, + invalidateConnectionWork: () => { + host.connectionWork.invalidate(); + }, + invalidateResumeWork: () => { + host.invalidateResumeWork(); + }, + clearDeferredDiagnostics: () => { + host.deferredTasks.clearDiagnostics(); + }, + resetConnection: () => { + connection.resetConnection(); + }, + setStatus: (statusText, phase) => { + status.set(statusText, phase); + }, + ensureConnected, + resumeThread: (threadId) => threadLifecycle.resume.resumeThread(threadId), + addSystemMessage: (text) => { + status.addSystemMessage(text); + }, + }; + const reconnect = () => reconnectPanel(reconnectHost); + const turnActions = createConversationTurnActions( + { + vaultPath: host.environment.plugin.settingsRef.vaultPath, + stateStore: host.stateStore, + client: { + currentClient, + ensureConnected, + }, + status, + runtime: { + connectionDiagnosticDetails: () => host.connectionDiagnosticDetails(), + modelStatusLines: () => host.modelStatusLines(), + effortStatusLines: () => host.effortStatusLines(), + statusSummaryLines: () => host.statusSummaryLines(), + mcpStatusLines: () => serverDiagnostics.mcpStatusLines(), + }, + thread: { + ensureRestoredThreadLoaded: () => + threadLifecycle.restoration.ensureLoaded((threadId) => threadLifecycle.resume.resumeThread(threadId)), + startNewThread: () => host.startNewThread(), + selectThread: (threadId) => selection.selectThread(threadId), + notifyIdentityChanged: () => { + host.notifyActiveThreadIdentityChanged(); + }, + resetTurnPresence: (hadTurns) => { + autoTitle.resetThreadTurnPresence(hadTurns); + }, + }, + composer: { + codexInput: (text) => composerController.codexInput(text), + trimmedDraft: () => composerController.trimmedDraft, + setDraft: (text, options) => { + composerController.setDraft(text, options); + }, + }, + scroll: { + followBottom: () => { + host.messageScrollIntent.followBottom(); + }, + }, + }, + { + threadStarter: serverThreads, + runtimeSettings, + threadActions, + reconnectPanel: reconnect, + goals, + }, + ); + + return { + pendingRequests, + reconnect, + turnActions, + }; +} + +function createSurfacesAndPresenter( + host: ChatPanelSessionPartsHost, + input: { + connection: ConnectionManager; + connectionController: ChatPanelConnectionBundle["connection"]["controller"]; + inboundController: ChatInboundController; + serverThreads: ChatServerThreadActions; + goals: ChatPanelGoalActions; + rename: ThreadRenameEditorControllerInstance; + threadActions: ChatPanelThreadActions; + toolbarPanels: ToolbarPanelActions; + selection: ChatPanelSelectionActions; + reconnect: () => Promise; + history: HistoryController; + pendingRequests: PendingRequestController; + turnActions: ChatPanelConversationTurnActions; + messageStreamScrollBridge: MessageStreamScrollBridge; + }, +): ChatPanelSurfacePresenterParts { + const { + connection, + connectionController, + inboundController, + serverThreads, + goals, + rename, + threadActions, + toolbarPanels, + selection, + reconnect, + history, + pendingRequests, + turnActions, + messageStreamScrollBridge, + } = input; + const { environment, stateStore } = host; + const toolbarActions = createChatPanelToolbarActions( + { + stateStore, + startNewThread: () => host.startNewThread(), + }, + { + connectionController, + reconnectPanel: reconnect, + inboundController, + threadActions, + toolbarPanels, + rename, + selection, + }, + ); + const toolbarSurface: ChatPanelToolbarSurface = { + state: { + connected: () => connection.isConnected(), + nowMs: () => Date.now(), + }, + settings: { + vaultPath: () => environment.plugin.settingsRef.vaultPath, + configuredCommand: () => environment.plugin.settingsRef.settings.codexPath, + archiveExportEnabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled, + }, + }; + const goalSurface = createChatPanelGoalSurface( + { + settings: environment.plugin.settingsRef.settings, + stateStore, + }, + { + connectionController, + inboundController, + threadStarter: serverThreads, + goals, + }, + ); + const messageStreamPresenter = new MessageStreamPresenter({ + obsidian: { + app: environment.obsidian.app, + owner: environment.obsidian.owner, + }, + state: { + store: stateStore, + }, + workspace: { + vaultPath: environment.plugin.settingsRef.vaultPath, + }, + scroll: { + consumeIntent: () => host.messageScrollIntent.consumeIntent(), + registerVirtualizer: messageStreamScrollBridge.registerVirtualizer, + dispose: () => { + messageStreamScrollBridge.dispose(); + }, + }, + history: { + loadOlderTurns: () => void history.loadOlder(), + }, + actions: { + rollbackThread: (threadId) => void threadActions.rollbackThread(threadId), + forkThreadFromTurn: (threadId, turnId, archiveSource) => void threadActions.forkThreadFromTurn(threadId, turnId, archiveSource), + implementPlan: (item: MessageStreamItem) => void turnActions.planImplementation.implement(item), + openTurnDiff: (state) => void environment.plugin.workspace.openTurnDiff(state), + }, + requests: { + pendingSignature: () => { + const state = stateStore.getState(); + return pendingRequestsSignature(state.requests.approvals, state.requests.pendingUserInputs, state.requests.userInputDrafts); + }, + pendingSnapshot: () => pendingRequests.snapshot(), + pendingActions: () => pendingRequests.actions(), + consumePendingAutoFocus: () => pendingRequests.consumeAutoFocus(), + }, + }); + + return { + toolbarActions, + toolbarSurface, + goalSurface, + messageStreamPresenter, + }; +} + +function collaborationModeLabel(stateStore: ChatStateStore): string { + return formatCollaborationModeLabel(stateStore.getState().runtime.selectedCollaborationMode); +} diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index 3860cd76..0175820b 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -1,20 +1,14 @@ -import { Notice } from "obsidian"; - import type { AppServerClient } from "../../../app-server/connection/client"; -import { ConnectionManager } from "../../../app-server/connection/connection-manager"; import type { AppServerObservedQueryResult } from "../../../app-server/query/cache"; import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries"; import { appServerQueryContextRawEquals, type AppServerQueryContext } from "../../../app-server/query/keys"; import type { ModelMetadata } from "../../../domain/catalog/metadata"; import type { Thread } from "../../../domain/threads/model"; -import { getThreadTitle, normalizeExplicitThreadName } from "../../../domain/threads/model"; +import { getThreadTitle } from "../../../domain/threads/model"; import type { SharedServerMetadata } from "../../../domain/server/metadata"; import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; import { shortThreadId } from "../../../utils"; -import { createThreadOperations, type ThreadOperations } from "../../threads/thread-operations"; -import { createThreadTitleService, type ThreadTitleService } from "../../threads/thread-title-service"; -import { PendingRequestController } from "../application/pending-requests/controller"; import type { MessageStreamItem, MessageStreamNoticeSection } from "../domain/message-stream/items"; import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items"; import { createLocalChatItemIdFactory, type LocalChatItemIdFactory } from "../domain/local-id"; @@ -25,64 +19,17 @@ import { } from "../presentation/runtime/status"; import { createChatViewDeferredTasks } from "./lifecycle"; import { ChatResumeWorkTracker, type ChatViewDeferredTasks } from "../application/lifecycle"; -import { - createChatConnectionController, - handleChatConnectionExit, - type ChatConnectionController, -} from "../application/connection/connection-controller"; import { connectionDiagnosticsModel } from "../application/connection/diagnostics-display"; -import { reconnectPanel, type ChatReconnectActionsHost } from "../application/connection/reconnect-actions"; -import { createConversationTurnActions } from "../application/conversation/composition"; -import type { ComposerSubmitActions } from "../application/conversation/composer-submit-actions"; import { openPanelTurnLifecycle, parseRestoredThreadState, type ChatPanelSnapshot } from "../panel/snapshot"; import { collaborationModeLabel as formatCollaborationModeLabel } from "../presentation/runtime/messages"; import { runtimeSnapshotForChatState, type RuntimeSnapshot } from "../application/runtime/snapshot"; -import { createChatRuntimeSettingsActions } from "../application/runtime/settings-actions"; -import { activeTurnId, type ChatConnectionPhase, chatTurnBusy, type ChatAction, type ChatState } from "../application/state/root-reducer"; -import { messageStreamItems } from "../application/state/message-stream"; +import { chatTurnBusy, type ChatAction, type ChatState } from "../application/state/root-reducer"; import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell"; import { createChatStateStore, type ChatStateStore } from "../application/state/store"; -import { createGoalActions, createThreadGoalSyncActions } from "../application/threads/goal-actions"; -import { AutoTitleController } from "../application/threads/auto-title-controller"; -import type { HistoryController } from "../application/threads/history-controller"; -import type { IdentitySync } from "../application/threads/identity-sync"; -import { createThreadLifecycleParts } from "../application/threads/lifecycle-parts"; -import { - activeThreadRenameTitleContext, - ThreadRenameEditorController, - type ThreadRenameEditorController as ThreadRenameEditorControllerInstance, -} from "../application/threads/rename-editor-controller"; -import type { RestorationController } from "../application/threads/restoration-controller"; -import type { ResumeController } from "../application/threads/resume-controller"; -import { createSelectionActions } from "../application/threads/selection-actions"; -import { createThreadManagementActions, type ThreadManagementActionsHost } from "../application/threads/thread-management-actions"; -import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics"; -import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../app-server/actions/metadata"; -import { createChatServerThreadActions, type ChatServerThreadActions } from "../app-server/actions/threads"; -import { ChatInboundController } from "../app-server/inbound/controller"; -import { ChatComposerController } from "../panel/composer-controller"; -import type { ChatPanelComposerSurface } from "../panel/surface/composer-projection"; -import type { ChatPanelGoalSurface } from "../panel/surface/goal-projection"; -import type { ChatPanelToolbarSurface } from "../panel/surface/toolbar-projection"; -import { chatPanelComposerProjection } from "../panel/surface/composer-projection"; -import { MessageStreamPresenter } from "../panel/surface/message-stream-presenter"; -import { - createChatMessageScrollIntentState, - MessageStreamScrollBridge, - type ChatMessageScrollIntentState, -} from "../panel/surface/message-stream-scroll"; -import { createChatPanelGoalSurface } from "../panel/surface/goal-projection"; -import { createChatPanelToolbarActions, createToolbarPanelActions, type ToolbarPanelActions } from "../panel/toolbar-actions"; -import type { ToolbarActions } from "../ui/toolbar"; -import { pendingRequestsSignature } from "../domain/pending-requests/signatures"; -import { threadTitleContextFromMessageStreamItems } from "../application/threads/title-context"; -import { currentModel, runtimeConfigOrDefault } from "../domain/runtime/effective"; +import { createChatMessageScrollIntentState, type ChatMessageScrollIntentState } from "../panel/surface/message-stream-scroll"; import type { ChatSurfaceHandle } from "./surface-handle"; import type { ChatPanelEnvironment } from "./runtime"; - -type CurrentAppServerClient = () => AppServerClient | null; -type RespondRequestId = Parameters[0]; -type RejectRequestId = Parameters[0]; +import { createChatPanelSessionParts, type ChatPanelSessionParts, type ChatPanelSessionStatus } from "./session-parts"; function codexPanelDisplayTitle(activeThreadId: string | null, threads: readonly Thread[], fallbackTitle?: string | null): string { if (!activeThreadId) return "Codex"; @@ -92,113 +39,6 @@ function codexPanelDisplayTitle(activeThreadId: string | null, threads: readonly return title ? `Codex: ${title}` : "Codex"; } -function respondToCurrentServerRequest(currentClient: CurrentAppServerClient, requestId: RespondRequestId, result: unknown): boolean { - try { - const client = currentClient(); - client?.respondToServerRequest(requestId, result); - return Boolean(client); - } catch { - return false; - } -} - -function rejectCurrentServerRequest( - currentClient: CurrentAppServerClient, - requestId: RejectRequestId, - code: number, - message: string, -): boolean { - try { - const client = currentClient(); - client?.rejectServerRequest(requestId, code, message); - return Boolean(client); - } catch { - return false; - } -} - -interface ChatPanelSessionParts { - connection: { - manager: ConnectionManager; - controller: ChatConnectionController; - }; - serverActions: { - threads: ChatServerThreadActions; - metadata: ChatServerMetadataActions; - diagnostics: ChatServerDiagnosticsActions; - }; - thread: { - history: HistoryController; - resume: ResumeController; - restoration: RestorationController; - identity: IdentitySync; - rename: ThreadRenameEditorControllerInstance; - }; - toolbar: { - panels: ToolbarPanelActions; - actions: ToolbarActions; - }; - composer: { - controller: ChatComposerController; - submission: ComposerSubmitActions; - }; - render: { - messageStreamPresenter: MessageStreamPresenter; - }; - surface: { - toolbar: ChatPanelToolbarSurface; - goal: ChatPanelGoalSurface; - composer: ChatPanelComposerSurface; - }; -} - -interface ChatPanelSessionStatus { - set: (statusText: string, phase?: ChatConnectionPhase) => void; - addSystemMessage: (text: string) => void; - addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void; -} - -interface ChatPanelConnectionBundle { - connection: ChatPanelSessionParts["connection"]; - inboundController: ChatInboundController; - serverActions: ChatPanelSessionParts["serverActions"]; -} - -type ChatPanelGoalSyncActions = ReturnType; -type ChatPanelRuntimeSettingsActions = ReturnType; -type ChatPanelGoalActions = ReturnType; -type ChatPanelThreadLifecycle = ReturnType; -type ChatPanelThreadActions = ReturnType; -type ChatPanelSelectionActions = ReturnType; -type ChatPanelConversationTurnActions = ReturnType; - -interface ChatPanelConnectionBundleInput { - connection: ConnectionManager; - currentClient: CurrentAppServerClient; - status: ChatPanelSessionStatus; - goalSync: ChatPanelGoalSyncActions; - autoTitle: AutoTitleController; -} - -interface ChatPanelThreadActionParts { - actions: ChatPanelThreadActions; - toolbarPanels: ToolbarPanelActions; - selection: ChatPanelSelectionActions; -} - -interface ChatPanelComposerAndTurnParts { - pendingRequests: PendingRequestController; - reconnect: () => Promise; - turnActions: ChatPanelConversationTurnActions; -} - -interface ChatPanelSurfacePresenterParts { - toolbarActions: ToolbarActions; - toolbarSurface: ChatPanelToolbarSurface; - goalSurface: ChatPanelGoalSurface; - messageStreamPresenter: MessageStreamPresenter; -} - export class ChatPanelSession implements ChatSurfaceHandle { private readonly stateStore: ChatStateStore = createChatStateStore(); private readonly parts: ChatPanelSessionParts; @@ -539,774 +379,40 @@ export class ChatPanelSession implements ChatSurfaceHandle { } private createSessionParts(): ChatPanelSessionParts { - const status = this.createSessionStatus(); - const connection = this.createConnectionManager(); - const currentClient = () => connection.currentClient(); - const titleService = this.createThreadTitleService(currentClient); - const autoTitle = this.createAutoTitleController(currentClient, titleService); - const goalSync = this.createGoalSyncActions(currentClient, status); - const serverParts = this.createConnectionBundle({ - connection, - currentClient, - goalSync, - autoTitle, - status, - }); - const { - connection: { controller }, - inboundController, - } = serverParts; - const connectionController = controller; - const { threads: serverThreads, diagnostics: serverDiagnostics } = serverParts.serverActions; - const ensureConnected = () => connectionController.ensureConnected(); - const refreshActiveThreads = () => connectionController.refreshActiveThreads(); - const threadOperations = this.createThreadOperations(currentClient, ensureConnected); - const runtimeSettings = this.createRuntimeSettingsActions(currentClient, status); - const goals = this.createGoalActions(currentClient, ensureConnected, status); - const rename = this.createThreadRenameEditor(threadOperations, titleService, ensureConnected, status); - const threadLifecycle = this.createThreadLifecycle(currentClient, ensureConnected, status, goals, autoTitle); - const { history, identity, restoration, resume } = threadLifecycle; - - const composerSurface = this.createComposerSurface(threadLifecycle, runtimeSettings); - const messageStreamScrollBridge = new MessageStreamScrollBridge(); - const composerController = this.createComposerController(composerSurface, runtimeSettings, messageStreamScrollBridge); - const threadActionParts = this.createThreadActionParts({ - operations: threadOperations, - ensureConnected, - currentClient, - status, - composerController, - resume, - refreshActiveThreads, - }); - const composerAndTurn = this.createComposerAndTurnActions({ - connection, - ensureConnected, - currentClient, - status, - inboundController, - threadLifecycle, - threadActions: threadActionParts.actions, - selection: threadActionParts.selection, - composerController, - runtimeSettings, - serverThreads, - serverDiagnostics, - goals, - autoTitle, - }); - const surfaceAndPresenter = this.createSurfacesAndPresenter({ - connection, - connectionController, - inboundController, - serverThreads, - goals, - rename, - threadActions: threadActionParts.actions, - toolbarPanels: threadActionParts.toolbarPanels, - selection: threadActionParts.selection, - reconnect: composerAndTurn.reconnect, - history, - pendingRequests: composerAndTurn.pendingRequests, - turnActions: composerAndTurn.turnActions, - messageStreamScrollBridge, - }); + return createChatPanelSessionParts(this.sessionPartsHost(), this.createSessionStatus()); + } + private sessionPartsHost(): Parameters[0] { return { - connection: { - manager: connection, - controller: connectionController, - }, - serverActions: serverParts.serverActions, - thread: { - history, - resume, - restoration, - identity, - rename, - }, - toolbar: { - panels: threadActionParts.toolbarPanels, - actions: surfaceAndPresenter.toolbarActions, - }, - composer: { - controller: composerController, - submission: composerAndTurn.turnActions.composerSubmit, - }, - render: { - messageStreamPresenter: surfaceAndPresenter.messageStreamPresenter, - }, - surface: { - toolbar: surfaceAndPresenter.toolbarSurface, - goal: surfaceAndPresenter.goalSurface, - composer: composerSurface, - }, - }; - } - - private createConnectionManager(): ConnectionManager { - return new ConnectionManager( - () => this.environment.plugin.settingsRef.settings.codexPath, - this.environment.plugin.settingsRef.vaultPath, - ); - } - - private createThreadTitleService(currentClient: CurrentAppServerClient): ThreadTitleService { - const environment = this.environment; - return createThreadTitleService({ - settings: { - current: () => environment.plugin.settingsRef.settings, - vaultPath: environment.plugin.settingsRef.vaultPath, - }, - currentClient, - visibleContext: (threadId) => activeThreadRenameTitleContext(this.state, threadId), - visibleCompletedTurnContext: (turnId) => - threadTitleContextFromMessageStreamItems(turnId, messageStreamItems(this.state.messageStream)), - }); - } - - private createAutoTitleController(currentClient: CurrentAppServerClient, titleService: ThreadTitleService): AutoTitleController { - return new AutoTitleController({ + environment: this.environment, stateStore: this.stateStore, - completedTurnTitleContext: (turnId, completedSummary) => titleService.completedTurnContext(turnId, completedSummary), - generateTitleFromContext: (context) => titleService.generate(context), - renameGeneratedTitle: async (threadId, title, options) => { - const name = normalizeExplicitThreadName(title); - if (!name) return false; - const client = currentClient(); - if (!client) return false; - - await client.setThreadName(threadId, name); - if (currentClient() !== client) return false; - if (options.shouldPublish()) { - this.environment.plugin.threadCatalog.recordThreadRenamed(threadId, name); - } - return true; + deferredTasks: this.deferredTasks, + resumeWork: this.resumeWork, + connectionWork: this.connectionWork, + messageScrollIntent: this.messageScrollIntent, + getOpened: () => this.opened, + getClosing: () => this.closing, + invalidateResumeWork: () => { + this.invalidateResumeWork(); }, - }); - } - - private createGoalSyncActions(currentClient: CurrentAppServerClient, status: ChatPanelSessionStatus): ChatPanelGoalSyncActions { - return createThreadGoalSyncActions({ - stateStore: this.stateStore, - currentClient, - addSystemMessage: (text) => { - status.addSystemMessage(text); - }, - addGoalEvent: (item) => { - this.dispatch({ type: "message-stream/item-upserted", item }); - }, - refreshLiveState: () => { - this.refreshLiveState(); - }, - }); - } - - private createThreadOperations(currentClient: CurrentAppServerClient, ensureConnected: () => Promise): ThreadOperations { - const environment = this.environment; - return createThreadOperations({ - connection: { - ensureConnected, - currentClient, - }, - settings: { - current: () => environment.plugin.settingsRef.settings, - vaultPath: environment.plugin.settingsRef.vaultPath, - }, - archiveAdapter: environment.obsidian.archiveAdapter, - catalog: environment.plugin.threadCatalog, - notice: (text) => { - new Notice(text); - }, - }); - } - - private createRuntimeSettingsActions( - currentClient: CurrentAppServerClient, - status: ChatPanelSessionStatus, - ): ChatPanelRuntimeSettingsActions { - return createChatRuntimeSettingsActions({ - stateStore: this.stateStore, - currentClient, - runtimeSnapshotForState: runtimeSnapshotForChatState, - collaborationModeLabel: () => this.collaborationModeLabel(), - addSystemMessage: (text) => { - status.addSystemMessage(text); - }, - }); - } - - private createGoalActions( - currentClient: CurrentAppServerClient, - ensureConnected: () => Promise, - status: ChatPanelSessionStatus, - ): ChatPanelGoalActions { - return createGoalActions({ - stateStore: this.stateStore, - currentClient, - ensureConnected, - addSystemMessage: (text) => { - status.addSystemMessage(text); - }, - addGoalEvent: (item) => { - this.dispatch({ type: "message-stream/item-upserted", item }); - }, - refreshLiveState: () => { - this.refreshLiveState(); - }, - }); - } - - private createThreadRenameEditor( - operations: ThreadOperations, - titleService: ThreadTitleService, - ensureConnected: () => Promise, - status: ChatPanelSessionStatus, - ): ThreadRenameEditorControllerInstance { - return new ThreadRenameEditorController({ - stateStore: this.stateStore, - ensureConnected, - addSystemMessage: status.addSystemMessage, - renameThread: (threadId, value, options) => operations.renameThread(threadId, value, options), - generateThreadTitle: (threadId) => titleService.generateTitle(threadId), - }); - } - - private createThreadLifecycle( - currentClient: CurrentAppServerClient, - ensureConnected: () => Promise, - status: ChatPanelSessionStatus, - goals: ChatPanelGoalActions, - autoTitle: AutoTitleController, - ): ChatPanelThreadLifecycle { - return createThreadLifecycleParts({ - settingsRef: this.environment.plugin.settingsRef, - stateStore: this.stateStore, - client: { - currentClient, - ensureConnected, - }, - lifecycle: { - deferredTasks: this.deferredTasks, - resumeWork: this.resumeWork, - getOpened: () => this.opened, - getClosing: () => this.closing, - }, - thread: { - notifyIdentityChanged: () => { - this.notifyActiveThreadIdentityChanged(); - }, - refreshTabHeader: () => { - this.refreshTabHeader(); - }, - }, - status, - liveState: { - refresh: () => { - this.refreshLiveState(); - }, - }, - scroll: { - preservePosition: () => { - this.messageScrollIntent.preservePosition(); - }, - forceBottom: () => { - this.messageScrollIntent.forceBottom(); - }, - }, - goals, - resetThreadTurnPresence: (hadTurns) => { - autoTitle.resetThreadTurnPresence(hadTurns); - }, - }); - } - - private createComposerSurface( - threadLifecycle: ChatPanelThreadLifecycle, - runtimeSettings: ChatPanelRuntimeSettingsActions, - ): ChatPanelComposerSurface { - return { - thread: { - restoredPlaceholder: () => threadLifecycle.restoration.placeholder(), - }, - runtime: { - requestModel: (model) => runtimeSettings.requestModelFromUi(model), - requestReasoningEffort: (effort) => runtimeSettings.requestReasoningEffortFromUi(effort), - }, - }; - } - - private createComposerController( - composerSurface: ChatPanelComposerSurface, - runtimeSettings: ChatPanelRuntimeSettingsActions, - messageStreamScrollBridge: MessageStreamScrollBridge, - ): ChatComposerController { - const environment = this.environment; - const stateStore = this.stateStore; - return new ChatComposerController({ - app: environment.obsidian.app, - stateStore, - viewId: environment.obsidian.viewId, - sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut, - scrollThreadFromComposerEdges: () => environment.plugin.settingsRef.settings.scrollThreadFromComposerEdges, - canInterrupt: (state) => { - return state.turn.lifecycle.kind !== "idle" && Boolean(state.activeThread.id && activeTurnId(state)); - }, - composerProjection: (state) => chatPanelComposerProjection(composerSurface, state), - currentModelForSuggestions: () => { - const current = stateStore.getState(); - return currentModel(runtimeSnapshotForChatState(current), runtimeConfigOrDefault(current.connection.runtimeConfig)); - }, - threadScrollFromComposer: (action) => { - messageStreamScrollBridge.scrollFromComposer(action); - }, - togglePlan: () => void runtimeSettings.toggleCollaborationMode(), - toggleAutoReview: () => void runtimeSettings.toggleAutoReview(), - toggleFast: () => void runtimeSettings.toggleFastMode(), - onDraftChange: () => { - this.refreshLiveState(); - }, - onHeightChange: () => { - messageStreamScrollBridge.repinMessageStreamToBottomIfPinned(); - }, - }); - } - - private createThreadActionParts(input: { - operations: ThreadOperations; - ensureConnected: () => Promise; - currentClient: CurrentAppServerClient; - status: ChatPanelSessionStatus; - composerController: ChatComposerController; - resume: ResumeController; - refreshActiveThreads: () => Promise; - }): ChatPanelThreadActionParts { - const { operations, ensureConnected, currentClient, status, composerController, resume, refreshActiveThreads } = input; - const environment = this.environment; - const threadManagementHost: ThreadManagementActionsHost = { - stateStore: this.stateStore, - vaultPath: environment.plugin.settingsRef.vaultPath, - operations, - ensureConnected, - currentClient, - addSystemMessage: status.addSystemMessage, - setStatus: status.set, - setComposerText: (text) => { - composerController.setDraft(text, { focus: true }); - }, - openThreadInNewView: (threadId) => environment.plugin.workspace.openThreadInNewView(threadId), - openThreadInCurrentPanel: (threadId) => resume.resumeThread(threadId), + loadSharedThreadList: () => this.loadSharedThreadList(), notifyActiveThreadIdentityChanged: () => { this.notifyActiveThreadIdentityChanged(); }, - refreshAfterThreadMutation: async () => { - await refreshActiveThreads(); - }, - recordForkedThread: (thread) => { - environment.plugin.threadCatalog.upsertFromAppServer(thread); - }, - }; - const actions = createThreadManagementActions(threadManagementHost); - const toolbarPanels = createToolbarPanelActions({ - stateStore: this.stateStore, - threadActions: actions, - }); - const selection = createSelectionActions({ - stateStore: this.stateStore, - closeForThreadSelection: () => { - toolbarPanels.closeForThreadSelection(); - }, - focusThreadInOpenView: (threadId) => environment.plugin.workspace.focusThreadInOpenView(threadId), - resumeThread: (threadId) => resume.resumeThread(threadId), - addSystemMessage: status.addSystemMessage, - }); - return { actions, toolbarPanels, selection }; - } - - private createComposerAndTurnActions(input: { - connection: ConnectionManager; - ensureConnected: () => Promise; - currentClient: CurrentAppServerClient; - status: ChatPanelSessionStatus; - inboundController: ChatInboundController; - threadLifecycle: ChatPanelThreadLifecycle; - threadActions: ChatPanelThreadActions; - selection: ChatPanelSelectionActions; - composerController: ChatComposerController; - runtimeSettings: ChatPanelRuntimeSettingsActions; - serverThreads: ChatServerThreadActions; - serverDiagnostics: ChatServerDiagnosticsActions; - goals: ChatPanelGoalActions; - autoTitle: AutoTitleController; - }): ChatPanelComposerAndTurnParts { - const { - connection, - ensureConnected, - currentClient, - status, - inboundController, - threadLifecycle, - threadActions, - selection, - composerController, - runtimeSettings, - serverThreads, - serverDiagnostics, - goals, - autoTitle, - } = input; - const pendingRequests = new PendingRequestController({ - stateStore: this.stateStore, - responder: inboundController, - composerHasFocus: () => composerController.hasFocus(), - refreshLiveState: () => { - this.refreshLiveState(); - }, - }); - const reconnectHost: ChatReconnectActionsHost = { - stateStore: this.stateStore, - invalidateConnectionWork: () => { - this.connectionWork.invalidate(); - }, - invalidateResumeWork: () => { - this.invalidateResumeWork(); - }, - clearDeferredDiagnostics: () => { - this.deferredTasks.clearDiagnostics(); - }, - resetConnection: () => { - connection.resetConnection(); - }, - setStatus: (statusText, phase) => { - status.set(statusText, phase); - }, - ensureConnected, - resumeThread: (threadId) => threadLifecycle.resume.resumeThread(threadId), - addSystemMessage: (text) => { - status.addSystemMessage(text); - }, - }; - const reconnect = () => reconnectPanel(reconnectHost); - const turnActions = createConversationTurnActions( - { - vaultPath: this.environment.plugin.settingsRef.vaultPath, - stateStore: this.stateStore, - client: { - currentClient, - ensureConnected, - }, - status, - runtime: { - connectionDiagnosticDetails: () => this.connectionDiagnosticDetails(), - modelStatusLines: () => this.modelStatusLines(), - effortStatusLines: () => this.effortStatusLines(), - statusSummaryLines: () => this.statusSummaryLines(), - mcpStatusLines: () => serverDiagnostics.mcpStatusLines(), - }, - thread: { - ensureRestoredThreadLoaded: () => - threadLifecycle.restoration.ensureLoaded((threadId) => threadLifecycle.resume.resumeThread(threadId)), - startNewThread: () => this.startNewThread(), - selectThread: (threadId) => selection.selectThread(threadId), - notifyIdentityChanged: () => { - this.notifyActiveThreadIdentityChanged(); - }, - resetTurnPresence: (hadTurns) => { - autoTitle.resetThreadTurnPresence(hadTurns); - }, - }, - composer: { - codexInput: (text) => composerController.codexInput(text), - trimmedDraft: () => composerController.trimmedDraft, - setDraft: (text, options) => { - composerController.setDraft(text, options); - }, - }, - scroll: { - followBottom: () => { - this.messageScrollIntent.followBottom(); - }, - }, - }, - { - threadStarter: serverThreads, - runtimeSettings, - threadActions, - reconnectPanel: reconnect, - goals, - }, - ); - - return { - pendingRequests, - reconnect, - turnActions, - }; - } - - private createSurfacesAndPresenter(input: { - connection: ConnectionManager; - connectionController: ChatConnectionController; - inboundController: ChatInboundController; - serverThreads: ChatServerThreadActions; - goals: ChatPanelGoalActions; - rename: ThreadRenameEditorControllerInstance; - threadActions: ChatPanelThreadActions; - toolbarPanels: ToolbarPanelActions; - selection: ChatPanelSelectionActions; - reconnect: () => Promise; - history: HistoryController; - pendingRequests: PendingRequestController; - turnActions: ChatPanelConversationTurnActions; - messageStreamScrollBridge: MessageStreamScrollBridge; - }): ChatPanelSurfacePresenterParts { - const { - connection, - connectionController, - inboundController, - serverThreads, - goals, - rename, - threadActions, - toolbarPanels, - selection, - reconnect, - history, - pendingRequests, - turnActions, - messageStreamScrollBridge, - } = input; - const environment = this.environment; - const toolbarActions = createChatPanelToolbarActions( - { - stateStore: this.stateStore, - startNewThread: () => this.startNewThread(), - }, - { - connectionController, - reconnectPanel: reconnect, - inboundController, - threadActions, - toolbarPanels, - rename, - selection, - }, - ); - const toolbarSurface: ChatPanelToolbarSurface = { - state: { - connected: () => connection.isConnected(), - nowMs: () => Date.now(), - }, - settings: { - vaultPath: () => environment.plugin.settingsRef.vaultPath, - configuredCommand: () => environment.plugin.settingsRef.settings.codexPath, - archiveExportEnabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled, - }, - }; - const goalSurface = createChatPanelGoalSurface( - { - settings: environment.plugin.settingsRef.settings, - stateStore: this.stateStore, - }, - { - connectionController, - inboundController, - threadStarter: serverThreads, - goals, - }, - ); - const messageStreamPresenter = new MessageStreamPresenter({ - obsidian: { - app: environment.obsidian.app, - owner: environment.obsidian.owner, - }, - state: { - store: this.stateStore, - }, - workspace: { - vaultPath: environment.plugin.settingsRef.vaultPath, - }, - scroll: { - consumeIntent: () => this.messageScrollIntent.consumeIntent(), - registerVirtualizer: messageStreamScrollBridge.registerVirtualizer, - dispose: () => { - messageStreamScrollBridge.dispose(); - }, - }, - history: { - loadOlderTurns: () => void history.loadOlder(), - }, - actions: { - rollbackThread: (threadId) => void threadActions.rollbackThread(threadId), - forkThreadFromTurn: (threadId, turnId, archiveSource) => void threadActions.forkThreadFromTurn(threadId, turnId, archiveSource), - implementPlan: (item: MessageStreamItem) => void turnActions.planImplementation.implement(item), - openTurnDiff: (state) => void environment.plugin.workspace.openTurnDiff(state), - }, - requests: { - pendingSignature: () => - pendingRequestsSignature( - this.state.requests.approvals, - this.state.requests.pendingUserInputs, - this.state.requests.userInputDrafts, - ), - pendingSnapshot: () => pendingRequests.snapshot(), - pendingActions: () => pendingRequests.actions(), - consumePendingAutoFocus: () => pendingRequests.consumeAutoFocus(), - }, - }); - - return { - toolbarActions, - toolbarSurface, - goalSurface, - messageStreamPresenter, - }; - } - - private createConnectionBundle(input: ChatPanelConnectionBundleInput): ChatPanelConnectionBundle { - const { connection, currentClient, status, goalSync, autoTitle } = input; - const environment = this.environment; - const stateStore = this.stateStore; - const serverMetadata = createChatServerMetadataActions({ - stateStore, - vaultPath: environment.plugin.settingsRef.vaultPath, - currentClient, - updateAppServerMetadata: (updater) => environment.plugin.appServerData.updateAppServerMetadata(updater), - appServerMetadataSnapshot: () => environment.plugin.appServerData.appServerMetadataSnapshot(), - refreshAppServerMetadata: (options) => environment.plugin.appServerData.refreshAppServerMetadata(options), - }); - const serverDiagnostics = createChatServerDiagnosticsActions({ - stateStore, - vaultPath: environment.plugin.settingsRef.vaultPath, - currentClient, - updateAppServerMetadata: (updater) => environment.plugin.appServerData.updateAppServerMetadata(updater), - appServerMetadataSnapshot: () => environment.plugin.appServerData.appServerMetadataSnapshot(), - }); - const serverThreads = createChatServerThreadActions({ - stateStore, - vaultPath: environment.plugin.settingsRef.vaultPath, - currentClient, - runtimeSnapshotForState: runtimeSnapshotForChatState, - recordStartedThread: (thread) => { - environment.plugin.threadCatalog.upsertFromAppServer(thread); - }, - syncThreadGoal: (threadId) => { - void goalSync.syncThreadGoal(threadId); - }, - }); - const loadSharedThreadList = () => this.loadSharedThreadList(); - const inboundController = new ChatInboundController(stateStore, { - refreshActiveThreads: () => { - void loadSharedThreadList(); - }, - refreshRateLimits: () => { - void serverMetadata.refreshRateLimits({ preserveExistingOnFailure: true }); - }, - refreshSkills: (forceReload) => void serverMetadata.refreshSkills(forceReload), - applyAppServerMetadataSnapshot: () => { - serverMetadata.applyAppServerMetadataSnapshot(); - }, - maybeNameThread: (threadId, turnId, completedSummary) => { - autoTitle.maybeAutoTitleThread(threadId, turnId, completedSummary); - }, - upsertActiveThread: (thread) => { - environment.plugin.threadCatalog.upsertFromAppServer(thread); - }, - applyThreadArchived: (threadId) => { - environment.plugin.threadCatalog.recordThreadArchived(threadId); - }, - recordActiveThreadDeleted: (threadId) => { - environment.plugin.threadCatalog.recordThreadDeleted(threadId); - }, - applyThreadRenamed: (threadId, name) => { - environment.plugin.threadCatalog.recordThreadRenamed(threadId, name); - }, - recordMcpStartupStatus: (name, mcpStatus, message) => { - serverDiagnostics.recordMcpStartupStatus(name, mcpStatus, message); - }, - respondToServerRequest: (requestId, result) => respondToCurrentServerRequest(currentClient, requestId, result), - rejectServerRequest: (requestId, code, message) => rejectCurrentServerRequest(currentClient, requestId, code, message), - }); - const connectionExitHost = { - stateStore, - connectionWork: this.connectionWork, - invalidateResumeWork: () => { - this.invalidateResumeWork(); - }, - setStatus: status.set, - resetThreadTurnPresence: (hadTurns: boolean) => { - autoTitle.resetThreadTurnPresence(hadTurns); - }, - refreshLiveState: () => { - this.refreshLiveState(); - }, - }; - const connectionController = createChatConnectionController({ - ...connectionExitHost, - connection: { - connect: () => - connection.connect({ - onNotification: (notification) => { - inboundController.handleNotification(notification); - this.deferLiveStateRefresh(); - }, - onServerRequest: (request) => { - inboundController.handleServerRequest(request); - this.deferLiveStateRefresh(); - }, - onLog: (message) => { - inboundController.handleAppServerLog(message); - }, - onExit: () => { - handleChatConnectionExit(connectionExitHost); - }, - }), - currentClient, - isConnected: () => connection.isConnected(), - }, - metadata: { - refreshAppServerMetadata: () => serverMetadata.refreshAppServerMetadata(), - refreshSkills: (forceReload) => serverMetadata.refreshSkills(forceReload), - }, - diagnostics: { - refreshDiagnosticProbes: (options) => serverDiagnostics.refreshDiagnosticProbes(options), - }, - loadSharedThreadList, - scheduleDeferredDiagnostics: () => { - this.deferredTasks.scheduleDiagnostics(() => { - if (connection.isConnected()) { - void serverDiagnostics.refreshDiagnosticProbes({ appServerMetadataSnapshot: true }); - } - }); - }, - clearDeferredDiagnostics: () => { - this.deferredTasks.clearDiagnostics(); - }, refreshTabHeader: () => { this.refreshTabHeader(); }, - setStatus: status.set, - addSystemMessage: status.addSystemMessage, - configuredCommand: () => environment.plugin.settingsRef.settings.codexPath, refreshLiveState: () => { this.refreshLiveState(); }, - notifyConnectionFailed: () => { - new Notice("Codex app-server connection failed."); - }, - }); - - return { - connection: { - manager: connection, - controller: connectionController, - }, - inboundController, - serverActions: { - threads: serverThreads, - metadata: serverMetadata, - diagnostics: serverDiagnostics, + deferLiveStateRefresh: () => { + this.deferLiveStateRefresh(); }, + startNewThread: () => this.startNewThread(), + statusSummaryLines: () => this.statusSummaryLines(), + modelStatusLines: () => this.modelStatusLines(), + effortStatusLines: () => this.effortStatusLines(), + connectionDiagnosticDetails: () => this.connectionDiagnosticDetails(), }; }