From 6007fed8267b815a2be9721462a20d5fccced93e Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 10 Jun 2026 14:19:14 +0900 Subject: [PATCH] Narrow chat controller composition bridges --- src/features/chat/panel/composition.ts | 98 ++++++++++++-------- src/features/chat/panel/controller-wiring.ts | 95 ++++++++----------- src/features/chat/threads/composition.ts | 45 +++++---- 3 files changed, 123 insertions(+), 115 deletions(-) diff --git a/src/features/chat/panel/composition.ts b/src/features/chat/panel/composition.ts index b50c3ed7..9e7e1abd 100644 --- a/src/features/chat/panel/composition.ts +++ b/src/features/chat/panel/composition.ts @@ -22,7 +22,7 @@ import type { ThreadSelectionActions } from "../threads/thread-selection-control import type { ChatViewRenderController, ChatViewSlotRenderers } from "./view-render-controller"; import type { ChatMessageRenderer } from "../ui/message-stream"; import type { ChatControllerCompositionPorts } from "./controller-ports"; -import { createChatControllerCompositionActions, requireCompositionRef, type ChatControllerCompositionRefs } from "./controller-wiring"; +import { createChatControllerCompositionActions, type ChatControllerCompositionBridges } from "./controller-wiring"; import { createChatServerActionControllers, createChatConnectionControllers, @@ -84,20 +84,14 @@ export interface ChatViewControllers { export function createChatViewControllers(ports: ChatControllerCompositionPorts): ChatViewControllers { const connection = new ConnectionManager(() => ports.plugin.settings.codexPath, ports.plugin.vaultPath); const { renderController } = createViewRenderControllerGroup(ports, { connection }); - const refs: ChatControllerCompositionRefs = { - renderController, - controller: null, - connectionController: null, - threadSelection: null, - threadRename: null, - threadResume: null, - restoredThread: null, - serverMetadata: null, - serverDiagnostics: null, - messageRenderer: null, - composerController: null, + const bridges: ChatControllerCompositionBridges = { + systemMessages: { controller: null }, + connection: { controller: null }, + threadSelection: { actions: null }, + messageViewport: { renderer: null }, + composerDraft: { controller: null }, }; - const actions = createChatControllerCompositionActions(ports, refs); + const actions = createChatControllerCompositionActions(ports, { renderController, bridges }); const runtimeSettings = createChatRuntimeSettingsActions({ stateStore: ports.state.stateStore, currentClient: ports.client.getClient, @@ -120,10 +114,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) }, ); const { history, threadActions, goals, threadIdentity } = threadControllers; - refs.restoredThread = threadControllers.restoredThread; - refs.threadResume = threadControllers.threadResume; - refs.threadRename = threadControllers.threadRename; - const threadRename = requireCompositionRef(refs.threadRename, "thread rename controller"); + const { restoredThread, threadResume, threadRename } = threadControllers; const lifecycleActions = { ...ports.lifecycle, invalidateResumeWork: threadControllers.invalidateResumeWork, @@ -133,22 +124,32 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) ...ports, lifecycle: lifecycleActions, render: actions.render, - thread: actions.thread, + thread: { + restorePlaceholder: (restoredThreadState) => { + restoredThread.restore(restoredThreadState); + }, + clearRestoredLifecycle: () => { + restoredThread.clear(); + }, + }, }, { threadActions, }, ); - refs.threadSelection = createThreadSelectionControllerGroup( + const threadSelection = createThreadSelectionControllerGroup( { ...ports, - thread: actions.thread, + thread: { + resumeThread: (threadId) => threadResume.resumeThread(threadId), + }, status: actions.status, }, { toolbarPanels, }, ).threadSelection; + bridges.threadSelection.actions = threadSelection; const { reconnectActions } = createChatReconnectControllerGroup( { ...ports, @@ -156,7 +157,9 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) lifecycle: lifecycleActions, render: actions.render, status: actions.status, - thread: actions.thread, + thread: { + resumeThread: (threadId) => threadResume.resumeThread(threadId), + }, }, { connection, @@ -167,16 +170,20 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) goals, }); const { serverThreads, serverMetadata, serverDiagnostics } = serverActionControllers; - refs.serverMetadata = serverMetadata; - refs.serverDiagnostics = serverDiagnostics; const serverRequestHost = { currentClient: ports.client.getClient, }; - refs.controller = createChatInboundController( + const inboundController = createChatInboundController( { ...ports, render: actions.render, - thread: actions.thread, + thread: { + refreshThreads: actions.thread.refreshThreads, + refreshSkills: actions.thread.refreshSkills, + publishAppServerMetadataSnapshot: () => { + serverMetadata.publishAppServerMetadataSnapshot(); + }, + }, }, { serverMetadata, @@ -186,12 +193,19 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) rejectServerRequest: (requestId, code, message) => rejectServerRequest(serverRequestHost, requestId, code, message), }, ); - refs.connectionController = createChatConnectionControllers( + bridges.systemMessages.controller = inboundController; + const connectionController = createChatConnectionControllers( { ...ports, client: actions.client, lifecycle: lifecycleActions, - thread: actions.thread, + thread: { + loadSharedThreadList: ports.thread.loadSharedThreadList, + refreshTabHeader: ports.thread.refreshTabHeader, + resetTurnPresence: (hadTurns) => { + threadRename.resetThreadTurnPresence(hadTurns); + }, + }, status: actions.status, liveState: ports.liveState, render: actions.render, @@ -202,8 +216,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) serverDiagnostics, }, ).connectionController; - const inboundController = requireCompositionRef(refs.controller, "inbound controller"); - const connectionController = requireCompositionRef(refs.connectionController, "connection controller"); + bridges.connection.controller = connectionController; connection.setHandlers({ onNotification: (notification) => { @@ -230,8 +243,19 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) ...ports, client: actions.client, render: actions.render, - runtime: actions.runtime, - thread: actions.thread, + runtime: { + ...actions.runtime, + mcpStatusLines: () => serverDiagnostics.mcpStatusLines(), + }, + thread: { + ensureRestoredThreadLoaded: ports.thread.ensureRestoredThreadLoaded, + startNewThread: ports.thread.startNewThread, + selectThread: actions.thread.selectThread, + notifyIdentityChanged: ports.thread.notifyIdentityChanged, + resetTurnPresence: (hadTurns) => { + threadRename.resetThreadTurnPresence(hadTurns); + }, + }, status: actions.status, scroll: actions.scroll, }, @@ -247,10 +271,9 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) }, ); const { pendingRequests, composerSubmission } = conversationControllers; - refs.messageRenderer = conversationControllers.messageRenderer; - refs.composerController = conversationControllers.composerController; - const messageRenderer = requireCompositionRef(refs.messageRenderer, "message renderer"); - const composerController = requireCompositionRef(refs.composerController, "composer controller"); + const { messageRenderer, composerController } = conversationControllers; + bridges.messageViewport.renderer = messageRenderer; + bridges.composerDraft.controller = composerController; const { scheduleAppServerWarmup, openView, closeView } = createConnectionLifecycleControllerGroup( { ...ports, @@ -266,9 +289,6 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts) serverMetadata, }, ); - const threadResume = requireCompositionRef(refs.threadResume, "thread resume controller"); - const restoredThread = requireCompositionRef(refs.restoredThread, "restored thread controller"); - const threadSelection = requireCompositionRef(refs.threadSelection, "thread selection controller"); return { connection: { diff --git a/src/features/chat/panel/controller-wiring.ts b/src/features/chat/panel/controller-wiring.ts index 4dd29c6b..24e88dfc 100644 --- a/src/features/chat/panel/controller-wiring.ts +++ b/src/features/chat/panel/controller-wiring.ts @@ -1,28 +1,27 @@ -import type { ChatServerDiagnosticsActions } from "../server-actions/diagnostics-actions"; -import type { ChatServerMetadataActions } from "../server-actions/metadata-actions"; import type { ChatComposerController } from "../composer/controller"; import type { ChatInboundController } from "../inbound/controller"; -import type { RestoredThreadController } from "../threads/restored-thread-controller"; import type { ChatConnectionController } from "../session/connection-controller"; -import type { ThreadRenameController } from "../threads/thread-rename-controller"; -import type { ThreadResumeController } from "../threads/thread-resume-controller"; import type { ThreadSelectionActions } from "../threads/thread-selection-controller"; import type { ChatMessageRenderer } from "../ui/message-stream"; import type { ChatControllerCompositionPorts } from "./controller-ports"; import type { ChatViewRenderController } from "./view-render-controller"; -export interface ChatControllerCompositionRefs { - renderController: ChatViewRenderController; - controller: ChatInboundController | null; - connectionController: ChatConnectionController | null; - threadSelection: ThreadSelectionActions | null; - threadRename: ThreadRenameController | null; - threadResume: ThreadResumeController | null; - restoredThread: RestoredThreadController | null; - serverMetadata: ChatServerMetadataActions | null; - serverDiagnostics: ChatServerDiagnosticsActions | null; - messageRenderer: ChatMessageRenderer | null; - composerController: ChatComposerController | null; +export interface ChatControllerCompositionBridges { + systemMessages: { + controller: Pick | null; + }; + connection: { + controller: Pick | null; + }; + threadSelection: { + actions: Pick | null; + }; + messageViewport: { + renderer: Pick | null; + }; + composerDraft: { + controller: Pick | null; + }; } export interface ChatControllerCompositionActions { @@ -40,17 +39,10 @@ export interface ChatControllerCompositionActions { scroll: ChatControllerCompositionPorts["scroll"]; thread: ChatControllerCompositionPorts["thread"] & { selectThread: (threadId: string) => Promise; - resumeThread: (threadId: string) => Promise; refreshThreads: () => Promise; refreshSkills: (forceReload?: boolean) => Promise; - publishAppServerMetadataSnapshot: () => void; - resetTurnPresence: (hadTurns: boolean) => void; - restorePlaceholder: (restoredThreadState: Parameters[0]) => void; - clearRestoredLifecycle: () => void; - }; - runtime: ChatControllerCompositionPorts["runtime"] & { - mcpStatusLines: () => Promise; }; + runtime: ChatControllerCompositionPorts["runtime"]; composer: { setText: (text: string) => void; }; @@ -58,57 +50,47 @@ export interface ChatControllerCompositionActions { export function createChatControllerCompositionActions( ports: ChatControllerCompositionPorts, - refs: ChatControllerCompositionRefs, + deps: { + renderController: ChatViewRenderController; + bridges: ChatControllerCompositionBridges; + }, ): ChatControllerCompositionActions { + const { bridges, renderController } = deps; const render = { ...ports.render, now: () => { - refs.renderController.render(); + renderController.render(); }, shellSlots: () => { - refs.renderController.renderShellSlots(); + renderController.renderShellSlots(); }, }; const status = { ...ports.status, addSystemMessage: (text: string) => { - requireCompositionRef(refs.controller, "inbound controller").addSystemMessage(text); + requireCompositionBridge(bridges.systemMessages.controller, "system message bridge").addSystemMessage(text); render.now(); }, addStructuredSystemMessage: (text: string, details: Parameters[1]) => { - requireCompositionRef(refs.controller, "inbound controller").addStructuredSystemMessage(text, details); + requireCompositionBridge(bridges.systemMessages.controller, "system message bridge").addStructuredSystemMessage(text, details); render.now(); }, }; const threadNavigation = { - selectThread: (threadId: string) => requireCompositionRef(refs.threadSelection, "thread selection controller").selectThread(threadId), - resumeThread: (threadId: string) => requireCompositionRef(refs.threadResume, "thread resume controller").resumeThread(threadId), + selectThread: (threadId: string) => + requireCompositionBridge(bridges.threadSelection.actions, "thread selection bridge").selectThread(threadId), }; const threadRefresh = { - refreshThreads: () => requireCompositionRef(refs.connectionController, "connection controller").refreshThreads(), + refreshThreads: () => requireCompositionBridge(bridges.connection.controller, "connection bridge").refreshThreads(), refreshSkills: (forceReload?: boolean) => - requireCompositionRef(refs.connectionController, "connection controller").refreshSkills(forceReload), - publishAppServerMetadataSnapshot: () => { - requireCompositionRef(refs.serverMetadata, "server metadata actions").publishAppServerMetadataSnapshot(); - }, - }; - const threadLifecycle = { - resetTurnPresence: (hadTurns: boolean) => { - requireCompositionRef(refs.threadRename, "thread rename controller").resetThreadTurnPresence(hadTurns); - }, - restorePlaceholder: (restoredThreadState: Parameters[0]) => { - requireCompositionRef(refs.restoredThread, "restored thread controller").restore(restoredThreadState); - }, - clearRestoredLifecycle: () => { - requireCompositionRef(refs.restoredThread, "restored thread controller").clear(); - }, + requireCompositionBridge(bridges.connection.controller, "connection bridge").refreshSkills(forceReload), }; return { client: { ...ports.client, - ensureConnected: () => requireCompositionRef(refs.connectionController, "connection controller").ensureConnected(), + ensureConnected: () => requireCompositionBridge(bridges.connection.controller, "connection bridge").ensureConnected(), }, render, status, @@ -116,28 +98,27 @@ export function createChatControllerCompositionActions( ...ports.scroll, forceBottom: () => { ports.scroll.forceBottom(); - requireCompositionRef(refs.messageRenderer, "message renderer").forceMessagesToBottom(); + requireCompositionBridge(bridges.messageViewport.renderer, "message viewport bridge").forceMessagesToBottom(); }, }, thread: { ...ports.thread, ...threadNavigation, ...threadRefresh, - ...threadLifecycle, - }, - runtime: { - ...ports.runtime, - mcpStatusLines: () => requireCompositionRef(refs.serverDiagnostics, "server diagnostics actions").mcpStatusLines(), }, + runtime: ports.runtime, composer: { setText: (text) => { - requireCompositionRef(refs.composerController, "composer controller").setDraft(text, { focus: true, renderIfDetached: true }); + requireCompositionBridge(bridges.composerDraft.controller, "composer draft bridge").setDraft(text, { + focus: true, + renderIfDetached: true, + }); }, }, }; } -export function requireCompositionRef(value: T | null, name: string): T { +function requireCompositionBridge(value: T | null, name: string): T { if (!value) throw new Error(`Chat controller composition did not initialize ${name}.`); return value; } diff --git a/src/features/chat/threads/composition.ts b/src/features/chat/threads/composition.ts index c7519e37..7bd2dd11 100644 --- a/src/features/chat/threads/composition.ts +++ b/src/features/chat/threads/composition.ts @@ -46,10 +46,8 @@ interface ThreadControllerGroupPorts { }; thread: { selectThread: (threadId: string) => Promise; - resumeThread: (threadId: string) => Promise; refreshThreads: () => Promise; notifyIdentityChanged: () => void; - resetTurnPresence: (hadTurns: boolean) => void; refreshTabHeader: () => void; }; status: { @@ -83,13 +81,27 @@ export function createThreadControllerGroup( const currentClient = client.getClient; const { deferredTasks, resumeWork } = lifecycle; + const threadRename = new ThreadRenameController({ + stateStore, + vaultPath: plugin.vaultPath, + settings: () => plugin.settings, + ensureConnected: client.ensureConnected, + currentClient: () => refs.connection.currentClient(), + refreshThreads: thread.refreshThreads, + render: render.shellSlots, + addSystemMessage: status.addSystemMessage, + notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin), + }); + const resetThreadTurnPresence = (hadTurns: boolean) => { + threadRename.resetThreadTurnPresence(hadTurns); + }; const history = new ThreadHistoryController({ stateStore, currentClient, render: render.now, addSystemMessage: status.addSystemMessage, keepCurrentScrollPosition: scroll.preservePosition, - setThreadTurnPresence: thread.resetTurnPresence, + setThreadTurnPresence: resetThreadTurnPresence, }); const invalidateResumeWork = () => { resumeWork.invalidate(); @@ -129,17 +141,18 @@ export function createThreadControllerGroup( render: render.now, refreshLiveState: liveState.refresh, }); + let threadResume: ThreadResumeController | null = null; const restoredThread = new RestoredThreadController({ deferredTasks, opened: lifecycle.getOpened, - resumeThread: thread.resumeThread, + resumeThread: (threadId) => requireThreadController(threadResume, "thread resume controller").resumeThread(threadId), invalidateResumeWork, stateStore, systemItem: state.systemItem, setStatus: status.set, refreshTabHeader: thread.refreshTabHeader, }); - const threadResume = new ThreadResumeController({ + threadResume = new ThreadResumeController({ stateStore, vaultPath: plugin.vaultPath, resumeWork, @@ -149,7 +162,7 @@ export function createThreadControllerGroup( ensureConnected: client.ensureConnected, closing: lifecycle.getClosing, systemItem: state.systemItem, - resetThreadTurnPresence: thread.resetTurnPresence, + resetThreadTurnPresence, clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration, notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged, addSystemMessage: status.addSystemMessage, @@ -168,36 +181,30 @@ export function createThreadControllerGroup( restoredThread, invalidateResumeWork, clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration, - resetThreadTurnPresence: thread.resetTurnPresence, + resetThreadTurnPresence, notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged, refreshTabHeader: thread.refreshTabHeader, refreshLiveState: liveState.refresh, render: render.now, }); - const threadRename = new ThreadRenameController({ - stateStore, - vaultPath: plugin.vaultPath, - settings: () => plugin.settings, - ensureConnected: client.ensureConnected, - currentClient: () => refs.connection.currentClient(), - refreshThreads: thread.refreshThreads, - render: render.shellSlots, - addSystemMessage: status.addSystemMessage, - notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin), - }); return { history, threadActions, goals, restoredThread, - threadResume, + threadResume: requireThreadController(threadResume, "thread resume controller"), threadIdentity, threadRename, invalidateResumeWork, }; } +function requireThreadController(controller: T | null, name: string): T { + if (!controller) throw new Error(`Chat thread controller composition did not initialize ${name}.`); + return controller; +} + interface ThreadSelectionControllerGroupPorts { plugin: Pick; state: {