Narrow chat controller composition bridges

This commit is contained in:
murashit 2026-06-10 14:19:14 +09:00
parent 0d717dd36b
commit 6007fed826
3 changed files with 123 additions and 115 deletions

View file

@ -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: {

View file

@ -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<ChatInboundController, "addSystemMessage" | "addStructuredSystemMessage"> | null;
};
connection: {
controller: Pick<ChatConnectionController, "ensureConnected" | "refreshThreads" | "refreshSkills"> | null;
};
threadSelection: {
actions: Pick<ThreadSelectionActions, "selectThread"> | null;
};
messageViewport: {
renderer: Pick<ChatMessageRenderer, "forceMessagesToBottom"> | null;
};
composerDraft: {
controller: Pick<ChatComposerController, "setDraft"> | null;
};
}
export interface ChatControllerCompositionActions {
@ -40,17 +39,10 @@ export interface ChatControllerCompositionActions {
scroll: ChatControllerCompositionPorts["scroll"];
thread: ChatControllerCompositionPorts["thread"] & {
selectThread: (threadId: string) => Promise<void>;
resumeThread: (threadId: string) => Promise<void>;
refreshThreads: () => Promise<void>;
refreshSkills: (forceReload?: boolean) => Promise<void>;
publishAppServerMetadataSnapshot: () => void;
resetTurnPresence: (hadTurns: boolean) => void;
restorePlaceholder: (restoredThreadState: Parameters<RestoredThreadController["restore"]>[0]) => void;
clearRestoredLifecycle: () => void;
};
runtime: ChatControllerCompositionPorts["runtime"] & {
mcpStatusLines: () => Promise<string[]>;
};
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<ChatInboundController["addStructuredSystemMessage"]>[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<RestoredThreadController["restore"]>[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<T>(value: T | null, name: string): T {
function requireCompositionBridge<T>(value: T | null, name: string): T {
if (!value) throw new Error(`Chat controller composition did not initialize ${name}.`);
return value;
}

View file

@ -46,10 +46,8 @@ interface ThreadControllerGroupPorts {
};
thread: {
selectThread: (threadId: string) => Promise<void>;
resumeThread: (threadId: string) => Promise<void>;
refreshThreads: () => Promise<void>;
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<T>(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<CodexChatHost, "focusThreadInOpenView">;
state: {