Split chat session controller composition

This commit is contained in:
murashit 2026-06-08 08:42:23 +09:00
parent 8f01d579d0
commit 1ca1a33175
6 changed files with 681 additions and 682 deletions

View file

@ -0,0 +1,14 @@
import type { ChatAppServerMetadataController } from "../app-server/metadata-controller";
import type { ChatAppServerThreadController } from "../app-server/thread-controller";
import type { ChatPanelContext } from "./context";
export function applyCachedSharedAppServerState(
context: ChatPanelContext,
appServerThreads: ChatAppServerThreadController,
appServerMetadata: ChatAppServerMetadataController,
): void {
const threads = context.plugin.cachedThreadList();
if (threads) appServerThreads.applyThreadList(threads);
const metadata = context.plugin.cachedAppServerMetadata();
if (metadata) appServerMetadata.applyAppServerMetadata(metadata);
}

View file

@ -1,39 +1,34 @@
import { Notice } from "obsidian";
import { ConnectionManager } from "../../../app-server/connection-manager";
import { recoverRolloutTokenUsage } from "../../../app-server/rollout-token-usage";
import { currentModel } from "../../../runtime/state";
import { ChatAppServerDiagnosticsController } from "../app-server/diagnostics-controller";
import { ChatAppServerMetadataController } from "../app-server/metadata-controller";
import { ChatAppServerThreadController } from "../app-server/thread-controller";
import { ChatComposerController } from "../composer/controller";
import { ChatInboundController } from "../inbound/controller";
import { activeTurnId } from "../chat-state";
import { ChatThreadGoalController } from "../controllers/thread/thread-goal-controller";
import { ChatRuntimeSettingsController } from "../controllers/runtime/runtime-settings-controller";
import { ChatThreadActionController } from "../controllers/thread/thread-actions-controller";
import { ThreadHistoryController } from "../controllers/thread/thread-history-controller";
import { ThreadRenameController } from "../controllers/thread/thread-rename-controller";
import { ToolbarPanelController } from "./toolbar-controller";
import { createAppServerWarmupActions, type AppServerWarmupActions } from "../controllers/connection/app-server-warmup-controller";
import { ChatConnectionController } from "../controllers/connection/connection-controller";
import { ChatReconnectController } from "../controllers/connection/reconnect-controller";
import { PendingRequestController } from "../controllers/requests/pending-request-controller";
import type { ChatAppServerDiagnosticsController } from "../app-server/diagnostics-controller";
import type { ChatAppServerMetadataController } from "../app-server/metadata-controller";
import type { ChatAppServerThreadController } from "../app-server/thread-controller";
import type { ChatComposerController } from "../composer/controller";
import type { ChatInboundController } from "../inbound/controller";
import type { ChatThreadGoalController } from "../controllers/thread/thread-goal-controller";
import type { ChatRuntimeSettingsController } from "../controllers/runtime/runtime-settings-controller";
import type { ChatThreadActionController } from "../controllers/thread/thread-actions-controller";
import type { ThreadHistoryController } from "../controllers/thread/thread-history-controller";
import type { ThreadRenameController } from "../controllers/thread/thread-rename-controller";
import type { ToolbarPanelController } from "./toolbar-controller";
import type { AppServerWarmupActions } from "../controllers/connection/app-server-warmup-controller";
import type { ChatConnectionController } from "../controllers/connection/connection-controller";
import type { ChatReconnectController } from "../controllers/connection/reconnect-controller";
import type { PendingRequestController } from "../controllers/requests/pending-request-controller";
import { ServerRequestResponder } from "../controllers/requests/server-request-responder";
import { createChatShellRenderPort } from "./shell-render";
import { ComposerSubmissionController } from "../controllers/submission/composer-submission-controller";
import { PlanImplementationController } from "../controllers/submission/plan-implementation-controller";
import { SlashCommandController } from "../controllers/submission/slash-command-controller";
import { TurnSubmissionController } from "../controllers/submission/turn-submission-controller";
import { RestoredThreadController } from "../controllers/thread/restored-thread-controller";
import { ThreadIdentityController } from "../controllers/thread/thread-identity-controller";
import { ThreadResumeController } from "../controllers/thread/thread-resume-controller";
import { createThreadSelectionActions, type ThreadSelectionActions } from "../controllers/thread/thread-selection-controller";
import { ChatViewOpenCloseController } from "../controllers/view/view-open-close-controller";
import { ChatViewRenderController } from "../controllers/view/view-render-controller";
import { createChatViewStateActions, type ChatViewStateActions } from "../controllers/view/view-state-controller";
import { ChatMessageRenderer } from "../ui/message-stream";
import type { ComposerSubmissionController } from "../controllers/submission/composer-submission-controller";
import type { RestoredThreadController } from "../controllers/thread/restored-thread-controller";
import type { ThreadIdentityController } from "../controllers/thread/thread-identity-controller";
import type { ThreadResumeController } from "../controllers/thread/thread-resume-controller";
import type { ThreadSelectionActions } from "../controllers/thread/thread-selection-controller";
import type { ChatViewOpenCloseController } from "../controllers/view/view-open-close-controller";
import type { ChatViewRenderController } from "../controllers/view/view-render-controller";
import type { ChatViewStateActions } from "../controllers/view/view-state-controller";
import type { ChatMessageRenderer } from "../ui/message-stream";
import type { ChatPanelContext } from "./context";
import { createChatAppServerControllers, createChatConnectionControllers, createChatInboundController } from "./session-controllers";
import { createThreadControllerGroup } from "./thread-controllers";
import { createTurnControllerGroup } from "./turn-controllers";
import { createConnectionLifecycleControllerGroup, createViewRenderControllerGroup } from "./ui-controllers";
export interface ChatViewControllers {
connection: {
@ -81,12 +76,9 @@ export interface ChatViewControllers {
};
}
type ControllerContext = ReturnType<typeof createControllerContext>;
export function createChatViewControllers(ports: ChatPanelContext): ChatViewControllers {
const context = createControllerContext(ports);
const connection = new ConnectionManager(() => context.plugin.settings.codexPath, context.plugin.vaultPath);
const { renderController } = createViewRenderControllerGroup(context, { connection });
const connection = new ConnectionManager(() => ports.plugin.settings.codexPath, ports.plugin.vaultPath);
const { renderController } = createViewRenderControllerGroup(ports, { connection });
const {
history,
threadActions,
@ -100,23 +92,23 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
threadResume,
threadIdentity,
threadRename,
} = createThreadToolbarControllerGroup(context, {
} = createThreadControllerGroup(ports, {
connection,
});
const { appServerThreads, appServerMetadata, appServerDiagnostics } = createAppServerControllerGroup(context, {
const { appServerThreads, appServerMetadata, appServerDiagnostics } = createChatAppServerControllers(ports, {
connection,
goals,
});
const serverRequestResponder = new ServerRequestResponder({
currentClient: context.currentClient,
currentClient: ports.client.getClient,
});
const controller = createInboundController(context, {
const controller = createChatInboundController(ports, {
appServerMetadata,
appServerDiagnostics,
threadRename,
serverRequestResponder,
});
const { connectionController } = createConnectionControllerGroup(context, {
const { connectionController } = createChatConnectionControllers(ports, {
connection,
appServerMetadata,
appServerDiagnostics,
@ -125,24 +117,24 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
connection.setHandlers({
onNotification: (notification) => {
controller.handleNotification(notification);
context.liveState.refresh();
context.render.schedule();
ports.liveState.refresh();
ports.render.schedule();
},
onServerRequest: (request) => {
controller.handleServerRequest(request);
context.liveState.refresh();
context.render.now();
ports.liveState.refresh();
ports.render.now();
},
onLog: (message) => {
controller.handleAppServerLog(message);
context.render.now();
ports.render.now();
},
onExit: () => {
connectionController.handleExit();
},
});
const { pendingRequests, messageRenderer, composerController, composerSubmission } = createSubmissionControllerGroup(context, {
const { pendingRequests, messageRenderer, composerController, composerSubmission } = createTurnControllerGroup(ports, {
controller,
appServerThreads,
runtimeSettings,
@ -152,7 +144,7 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
goals,
history,
});
const { appServerWarmup, openCloseController } = createConnectionLifecycleControllerGroup(context, {
const { appServerWarmup, openCloseController } = createConnectionLifecycleControllerGroup(ports, {
connection,
composerController,
messageRenderer,
@ -206,635 +198,3 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
},
};
}
function createControllerContext(ports: ChatPanelContext) {
const { obsidian, plugin, state, client, lifecycle, render, runtime, thread, liveState, scroll, status, composer } = ports;
const { app, owner, viewId } = obsidian;
const { stateStore } = state;
return {
ports,
obsidian,
plugin,
state,
client,
lifecycle,
render,
runtime,
thread,
liveState,
scroll,
status,
composer,
app,
owner,
viewId,
stateStore,
currentClient: client.getClient,
};
}
function createViewRenderControllerGroup(
context: ControllerContext,
refs: {
connection: ConnectionManager;
},
) {
const { plugin, render, lifecycle, stateStore } = context;
const { deferredTasks } = lifecycle;
return {
renderController: new ChatViewRenderController({
shell: createChatShellRenderPort(stateStore, {
connected: () => refs.connection.isConnected(),
showToolbar: () => plugin.settings.showToolbar,
pendingRequestsSignature: render.pendingRequestsSignature,
activeComposerThreadName: render.activeComposerThreadName,
}),
panelRoot: render.panelRoot,
renderToolbar: render.renderToolbar,
renderGoal: render.renderGoal,
renderMessages: render.renderMessages,
renderComposer: render.renderComposer,
clearScheduledRender: () => {
deferredTasks.clearRender();
},
}),
};
}
function createSubmissionControllerGroup(
context: ControllerContext,
refs: {
controller: ChatInboundController;
appServerThreads: ChatAppServerThreadController;
runtimeSettings: ChatRuntimeSettingsController;
threadActions: ChatThreadActionController;
threadRename: ThreadRenameController;
reconnectActions: ChatReconnectController;
goals: ChatThreadGoalController;
history: ThreadHistoryController;
},
) {
const {
app,
owner,
plugin,
state,
stateStore,
viewId,
render,
runtime,
thread,
liveState,
scroll,
status,
lifecycle,
currentClient,
client,
} = context;
const { messageScrollIntent } = lifecycle;
const composerController = new ChatComposerController({
app,
stateStore,
viewId,
sendShortcut: () => plugin.settings.sendShortcut,
scrollThreadFromComposerEdges: () => plugin.settings.scrollThreadFromComposerEdges,
canInterrupt: () =>
state.getState().turn.lifecycle.kind !== "idle" && Boolean(state.getState().activeThread.id && activeTurnId(state.getState())),
composerPlaceholder: render.composerPlaceholder,
composerMeta: render.composerMetaViewModel,
currentModelForSuggestions: () => currentModel(runtime.runtimeSnapshot()),
togglePlan: () => void refs.runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
toggleFast: () => void refs.runtimeSettings.toggleFastMode(),
renderIfDetached: render.now,
onDraftChange: liveState.refresh,
onComposerResize: () => {
scroll.correctAfterLayoutChange();
},
});
const pendingRequests = new PendingRequestController({
stateStore,
controller: refs.controller,
composerHasFocus: () => composerController.hasFocus(),
refreshLiveState: liveState.refresh,
render: render.now,
});
const turnSubmission = new TurnSubmissionController({
stateStore,
connection: {
vaultPath: plugin.vaultPath,
currentClient,
},
restoredThread: {
ensureRestoredThreadLoaded: thread.ensureRestoredThreadLoaded,
},
thread: {
startThread: (preview) => refs.appServerThreads.startThread(preview),
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
resetThreadTurnPresence: thread.resetTurnPresence,
},
runtime: {
applyPendingThreadSettings: () => refs.runtimeSettings.applyPendingThreadSettings(),
},
composer: {
codexInput: (text) => composerController.codexInput(text),
setDraft: (text, options) => {
composerController.setDraft(text, options);
},
},
view: {
forceMessagesToBottom: scroll.forceBottom,
render: render.now,
scheduleRender: render.schedule,
},
status: {
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
},
});
const slashCommands = new SlashCommandController({
stateStore,
currentClient,
codexInput: (text) => composerController.codexInput(text),
threads: {
startNewThread: thread.startNewThread,
startThreadForGoal: async (objective) => {
const response = await refs.appServerThreads.startThread(objective, { syncGoal: false });
return response?.thread.id ?? null;
},
resumeThread: thread.selectThread,
forkThread: (threadId) => refs.threadActions.forkThread(threadId),
rollbackThread: (threadId) => refs.threadActions.rollbackThread(threadId),
compactThread: (threadId) => refs.threadActions.compactThread(threadId),
archiveThread: (threadId) => refs.threadActions.archiveThread(threadId),
renameThread: (threadId, name) => refs.threadRename.rename(threadId, name),
reconnect: () => refs.reconnectActions.reconnectPanel(),
},
runtime: {
toggleFastMode: () => refs.runtimeSettings.toggleFastMode(),
toggleCollaborationMode: () => refs.runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
setRequestedModel: (model) => refs.runtimeSettings.setRequestedModel(model),
setRequestedReasoningEffort: (effort) => refs.runtimeSettings.setRequestedReasoningEffort(effort),
},
goals: {
activeGoal: () => refs.goals.activeGoal(),
setObjective: (threadId, objective, tokenBudget) => refs.goals.setObjective(threadId, objective, tokenBudget),
setStatus: (threadId, status) => refs.goals.setStatus(threadId, status),
clear: (threadId) => refs.goals.clear(threadId),
},
status: {
addSystemMessage: status.addSystemMessage,
addStructuredSystemMessage: status.addStructuredSystemMessage,
setStatus: status.set,
statusSummaryLines: runtime.statusSummaryLines,
connectionDiagnosticDetails: runtime.connectionDiagnosticDetails,
mcpStatusLines: runtime.mcpStatusLines,
modelStatusLines: runtime.modelStatusLines,
effortStatusLines: runtime.effortStatusLines,
},
});
const planImplementation = new PlanImplementationController({
stateStore,
connection: {
currentClient,
ensureConnected: client.ensureConnected,
},
submission: {
sendTurnText: (text) => turnSubmission.sendTurnText(text),
},
});
const messageRenderer = new ChatMessageRenderer({
obsidian: {
app,
owner,
},
state: {
store: stateStore,
},
workspace: {
vaultPath: plugin.vaultPath,
},
scroll: {
consumeIntent: () => messageScrollIntent.consumeIntent(),
},
history: {
loadOlderTurns: () => void refs.history.loadOlder(),
},
actions: {
rollbackThread: (threadId) => void refs.threadActions.rollbackThread(threadId),
forkThreadFromTurn: (threadId, turnId, archiveSource) => void refs.threadActions.forkThreadFromTurn(threadId, turnId, archiveSource),
implementPlan: (item) => void planImplementation.implement(item),
openTurnDiff: (state) => void plugin.openTurnDiff(state),
},
requests: {
pendingSignature: render.pendingRequestsSignature,
renderPending: () => pendingRequests.renderNode(),
},
});
const composerSubmission = new ComposerSubmissionController({
stateStore,
composer: composerController,
slashCommands,
turnSubmission,
connection: {
currentClient,
ensureConnected: client.ensureConnected,
},
status: {
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
},
});
composerController.setActionHandlers({
submit: () => void composerSubmission.submit(),
threadScrollFromComposer: (action) => {
messageRenderer.scrollFromComposer(action);
},
});
return {
pendingRequests,
messageRenderer,
composerController,
composerSubmission,
};
}
function createConnectionLifecycleControllerGroup(
context: ControllerContext,
refs: {
connection: ConnectionManager;
composerController: ChatComposerController;
messageRenderer: ChatMessageRenderer;
appServerThreads: ChatAppServerThreadController;
appServerMetadata: ChatAppServerMetadataController;
},
) {
const { ports, obsidian, lifecycle, render, liveState, scroll, client } = context;
const { deferredTasks } = lifecycle;
return {
appServerWarmup: createAppServerWarmupActions({
deferredTasks,
opened: lifecycle.getOpened,
closing: lifecycle.getClosing,
connected: () => refs.connection.isConnected(),
ensureConnected: client.ensureConnected,
}),
openCloseController: new ChatViewOpenCloseController({
setOpened: lifecycle.setOpened,
setClosing: lifecycle.setClosing,
registerEvent: obsidian.registerEvent,
registerComposerNoteIndexInvalidation: (register) => {
refs.composerController.registerNoteIndexInvalidation(register);
},
registerPointerDown: obsidian.registerPointerDown,
registerActiveLeafChange: obsidian.registerActiveLeafChange,
isOwnLeaf: obsidian.isOwnLeaf,
scrollMessagesToBottomOnFocus: scroll.bottomOnFocus,
applyCachedSharedAppServerState: () => {
applyCachedSharedAppServerState(ports, refs.appServerThreads, refs.appServerMetadata);
},
render: render.now,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
scheduleDeferredRestoredThreadHydration: lifecycle.scheduleDeferredRestoredThreadHydration,
closeToolbarPanelOnOutsidePointer: render.closeToolbarPanelOnOutsidePointer,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredTasks: () => {
deferredTasks.clearAll();
},
panelRoot: render.panelRoot,
disposeMessages: () => {
refs.messageRenderer.dispose();
},
disposeComposer: () => {
refs.composerController.dispose();
},
disconnect: () => {
refs.connection.disconnect();
},
clearClient: client.clear,
refreshLiveState: liveState.refresh,
deferRefreshLiveState: liveState.deferRefresh,
}),
};
}
function createAppServerControllerGroup(
context: ControllerContext,
refs: {
connection: ConnectionManager;
goals: ChatThreadGoalController;
},
) {
const { plugin, runtime, scroll, stateStore } = context;
const appServerBaseHost = {
stateStore,
vaultPath: plugin.vaultPath,
currentClient: () => refs.connection.currentClient(),
};
const appServerMetadata = new ChatAppServerMetadataController({
...appServerBaseHost,
publishAppServerMetadata: (metadata) => {
plugin.publishAppServerMetadata(metadata);
},
});
const appServerDiagnostics = new ChatAppServerDiagnosticsController({
...appServerBaseHost,
publishAppServerMetadata: (metadata) => {
plugin.publishAppServerMetadata(metadata);
},
appServerMetadataSnapshot: () => appServerMetadata.appServerMetadataSnapshot(),
});
const appServerThreads = new ChatAppServerThreadController({
...appServerBaseHost,
runtimeSnapshot: runtime.runtimeSnapshot,
forceMessagesToBottom: scroll.forceBottom,
publishThreadList: (threads) => {
plugin.applyThreadListSnapshot(threads);
},
syncThreadGoal: (threadId) => {
void refs.goals.syncThreadGoal(threadId);
},
});
return { appServerThreads, appServerMetadata, appServerDiagnostics };
}
function createInboundController(
context: ControllerContext,
refs: {
appServerMetadata: ChatAppServerMetadataController;
appServerDiagnostics: ChatAppServerDiagnosticsController;
threadRename: ThreadRenameController;
serverRequestResponder: ServerRequestResponder;
},
) {
const { plugin, thread, render, stateStore } = context;
return new ChatInboundController(stateStore, {
refreshThreads: () => {
void thread.refreshThreads();
},
refreshRateLimits: () => {
void refs.appServerMetadata.refreshPublishedRateLimits();
},
refreshSkills: (forceReload) => void thread.refreshSkills(forceReload),
publishAppServerMetadata: thread.publishAppServerMetadataSnapshot,
maybeNameThread: (threadId, turn) => {
refs.threadRename.maybeAutoNameThread(threadId, turn);
},
notifyThreadArchived: plugin.notifyThreadArchived.bind(plugin),
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),
recordMcpStartupStatus: (name, status, message) => {
refs.appServerDiagnostics.recordMcpStartupStatus(name, status, message);
render.schedule();
},
respondToServerRequest: (requestId, result) => refs.serverRequestResponder.respond(requestId, result),
rejectServerRequest: (requestId, code, message) => refs.serverRequestResponder.reject(requestId, code, message),
});
}
function createConnectionControllerGroup(
context: ControllerContext,
refs: {
connection: ConnectionManager;
appServerMetadata: ChatAppServerMetadataController;
appServerDiagnostics: ChatAppServerDiagnosticsController;
},
) {
const { plugin, client, thread, status, liveState, render, lifecycle, stateStore } = context;
const { connectionWork } = lifecycle;
return {
connectionController: new ChatConnectionController({
stateStore,
connection: refs.connection,
connectionWork,
metadata: {
refreshPublishedAppServerMetadata: () => refs.appServerMetadata.refreshPublishedAppServerMetadata(),
refreshPublishedSkills: (forceReload) => refs.appServerMetadata.refreshPublishedSkills(forceReload),
},
diagnostics: {
refreshPublishedCapabilityDiagnostics: () => refs.appServerDiagnostics.refreshPublishedCapabilityDiagnostics(),
},
setClient: client.setClient,
invalidateResumeWork: lifecycle.invalidateResumeWork,
loadSharedThreadList: thread.loadSharedThreadList,
scheduleDeferredDiagnostics: lifecycle.scheduleDeferredDiagnostics,
clearDeferredDiagnostics: lifecycle.clearDeferredDiagnostics,
refreshTabHeader: thread.refreshTabHeader,
resetThreadTurnPresence: thread.resetTurnPresence,
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
configuredCommand: () => plugin.settings.codexPath,
refreshLiveState: liveState.refresh,
render: render.now,
scheduleRender: render.schedule,
notifyConnectionFailed: () => {
new Notice("Codex app-server connection failed.");
},
}),
};
}
function createThreadToolbarControllerGroup(
context: ControllerContext,
refs: {
connection: ConnectionManager;
},
) {
const {
obsidian,
plugin,
state,
runtime,
thread,
status,
liveState,
scroll,
render,
client,
composer,
lifecycle,
stateStore,
currentClient,
} = context;
const { deferredTasks, resumeWork } = lifecycle;
const history = new ThreadHistoryController({
stateStore,
currentClient,
render: render.now,
addSystemMessage: status.addSystemMessage,
forceMessagesToBottom: scroll.forceBottom,
keepCurrentScrollPosition: scroll.preservePosition,
setThreadTurnPresence: thread.resetTurnPresence,
});
const threadActions = new ChatThreadActionController({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
archiveAdapter: obsidian.archiveAdapter,
ensureConnected: client.ensureConnected,
currentClient,
history,
addSystemMessage: status.addSystemMessage,
setStatus: status.set,
setComposerText: composer.setText,
openThreadInNewView: (threadId) => plugin.openThreadInNewView(threadId),
openThreadInCurrentPanel: thread.selectThread,
notifyThreadArchived: plugin.notifyThreadArchived.bind(plugin),
notifyThreadRenamed: (threadId, name) => {
plugin.notifyThreadRenamed(threadId, name);
},
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
refreshThreads: thread.refreshThreads,
refreshSharedThreadListFromOpenSurface: () => {
plugin.refreshSharedThreadListFromOpenSurface();
},
});
const toolbarPanels = new ToolbarPanelController({
stateStore,
threadActions,
scheduleRender: render.schedule,
});
const threadSelection = createThreadSelectionActions({
stateStore,
closeForThreadSelection: () => {
toolbarPanels.closeForThreadSelection();
},
focusThreadInOpenView: (threadId) => plugin.focusThreadInOpenView(threadId),
resumeThread: thread.resumeThread,
addSystemMessage: status.addSystemMessage,
});
const reconnectActions = new ChatReconnectController({
stateStore,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredDiagnostics: lifecycle.clearDeferredDiagnostics,
reconnect: () => {
refs.connection.reconnect();
},
clearClient: client.clear,
setStatus: status.set,
render: render.now,
ensureConnected: client.ensureConnected,
resumeThread: thread.resumeThread,
addSystemMessage: status.addSystemMessage,
});
const runtimeSettings = new ChatRuntimeSettingsController({
stateStore,
currentClient,
runtimeSnapshot: runtime.runtimeSnapshot,
collaborationModeLabel: runtime.collaborationModeLabel,
addSystemMessage: status.addSystemMessage,
});
const goals = new ChatThreadGoalController({
stateStore,
currentClient,
ensureConnected: client.ensureConnected,
addSystemMessage: status.addSystemMessage,
addGoalEvent: (item) => {
stateStore.dispatch({ type: "transcript/item-upserted", item });
},
render: render.now,
refreshLiveState: liveState.refresh,
});
const restoredThread = new RestoredThreadController({
deferredTasks,
opened: lifecycle.getOpened,
resumeThread: thread.resumeThread,
invalidateResumeWork: lifecycle.invalidateResumeWork,
stateStore,
systemItem: state.systemItem,
setStatus: status.set,
refreshTabHeader: thread.refreshTabHeader,
});
const viewStateController = createChatViewStateActions({
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearRestoredThreadLifecycle: thread.clearRestoredLifecycle,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
restoreThreadPlaceholder: thread.restorePlaceholder,
});
const threadResume = new ThreadResumeController({
stateStore,
vaultPath: plugin.vaultPath,
resumeWork,
history,
restoredThread,
currentClient,
ensureConnected: client.ensureConnected,
closing: lifecycle.getClosing,
systemItem: state.systemItem,
resetThreadTurnPresence: thread.resetTurnPresence,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
addSystemMessage: status.addSystemMessage,
forceMessagesToBottom: scroll.forceBottom,
render: render.now,
refreshLiveState: liveState.refresh,
syncThreadGoal: (threadId) => goals.syncThreadGoal(threadId),
recoverTokenUsageFromRollout: (path) =>
recoverRolloutTokenUsage(path, async (filePath, options) => {
const response = await currentClient()?.readFile(filePath, options);
return response?.dataBase64 ?? "";
}),
});
const threadIdentity = new ThreadIdentityController({
stateStore,
restoredThread,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
resetThreadTurnPresence: thread.resetTurnPresence,
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,
toolbarPanels,
threadSelection,
reconnectActions,
runtimeSettings,
goals,
restoredThread,
viewStateController,
threadResume,
threadIdentity,
threadRename,
};
}
function applyCachedSharedAppServerState(
ports: ChatPanelContext,
appServerThreads: ChatAppServerThreadController,
appServerMetadata: ChatAppServerMetadataController,
): void {
const threads = ports.plugin.cachedThreadList();
if (threads) appServerThreads.applyThreadList(threads);
const metadata = ports.plugin.cachedAppServerMetadata();
if (metadata) appServerMetadata.applyAppServerMetadata(metadata);
}

View file

@ -0,0 +1,131 @@
import { Notice } from "obsidian";
import type { ConnectionManager } from "../../../app-server/connection-manager";
import { ChatAppServerDiagnosticsController } from "../app-server/diagnostics-controller";
import { ChatAppServerMetadataController } from "../app-server/metadata-controller";
import { ChatAppServerThreadController } from "../app-server/thread-controller";
import { ChatConnectionController } from "../controllers/connection/connection-controller";
import type { ServerRequestResponder } from "../controllers/requests/server-request-responder";
import type { ChatThreadGoalController } from "../controllers/thread/thread-goal-controller";
import type { ThreadRenameController } from "../controllers/thread/thread-rename-controller";
import { ChatInboundController } from "../inbound/controller";
import type { ChatPanelContext } from "./context";
export function createChatAppServerControllers(
context: ChatPanelContext,
refs: {
connection: ConnectionManager;
goals: ChatThreadGoalController;
},
) {
const { plugin, runtime, scroll } = context;
const stateStore = context.state.stateStore;
const appServerBaseHost = {
stateStore,
vaultPath: plugin.vaultPath,
currentClient: () => refs.connection.currentClient(),
};
const appServerMetadata = new ChatAppServerMetadataController({
...appServerBaseHost,
publishAppServerMetadata: (metadata) => {
plugin.publishAppServerMetadata(metadata);
},
});
const appServerDiagnostics = new ChatAppServerDiagnosticsController({
...appServerBaseHost,
publishAppServerMetadata: (metadata) => {
plugin.publishAppServerMetadata(metadata);
},
appServerMetadataSnapshot: () => appServerMetadata.appServerMetadataSnapshot(),
});
const appServerThreads = new ChatAppServerThreadController({
...appServerBaseHost,
runtimeSnapshot: runtime.runtimeSnapshot,
forceMessagesToBottom: scroll.forceBottom,
publishThreadList: (threads) => {
plugin.applyThreadListSnapshot(threads);
},
syncThreadGoal: (threadId) => {
void refs.goals.syncThreadGoal(threadId);
},
});
return { appServerThreads, appServerMetadata, appServerDiagnostics };
}
export function createChatInboundController(
context: ChatPanelContext,
refs: {
appServerMetadata: ChatAppServerMetadataController;
appServerDiagnostics: ChatAppServerDiagnosticsController;
threadRename: ThreadRenameController;
serverRequestResponder: ServerRequestResponder;
},
): ChatInboundController {
const { plugin, thread, render } = context;
return new ChatInboundController(context.state.stateStore, {
refreshThreads: () => {
void thread.refreshThreads();
},
refreshRateLimits: () => {
void refs.appServerMetadata.refreshPublishedRateLimits();
},
refreshSkills: (forceReload) => void thread.refreshSkills(forceReload),
publishAppServerMetadata: thread.publishAppServerMetadataSnapshot,
maybeNameThread: (threadId, turn) => {
refs.threadRename.maybeAutoNameThread(threadId, turn);
},
notifyThreadArchived: plugin.notifyThreadArchived.bind(plugin),
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),
recordMcpStartupStatus: (name, status, message) => {
refs.appServerDiagnostics.recordMcpStartupStatus(name, status, message);
render.schedule();
},
respondToServerRequest: (requestId, result) => refs.serverRequestResponder.respond(requestId, result),
rejectServerRequest: (requestId, code, message) => refs.serverRequestResponder.reject(requestId, code, message),
});
}
export function createChatConnectionControllers(
context: ChatPanelContext,
refs: {
connection: ConnectionManager;
appServerMetadata: ChatAppServerMetadataController;
appServerDiagnostics: ChatAppServerDiagnosticsController;
},
) {
const { plugin, client, thread, status, liveState, render, lifecycle } = context;
const { connectionWork } = lifecycle;
return {
connectionController: new ChatConnectionController({
stateStore: context.state.stateStore,
connection: refs.connection,
connectionWork,
metadata: {
refreshPublishedAppServerMetadata: () => refs.appServerMetadata.refreshPublishedAppServerMetadata(),
refreshPublishedSkills: (forceReload) => refs.appServerMetadata.refreshPublishedSkills(forceReload),
},
diagnostics: {
refreshPublishedCapabilityDiagnostics: () => refs.appServerDiagnostics.refreshPublishedCapabilityDiagnostics(),
},
setClient: client.setClient,
invalidateResumeWork: lifecycle.invalidateResumeWork,
loadSharedThreadList: thread.loadSharedThreadList,
scheduleDeferredDiagnostics: lifecycle.scheduleDeferredDiagnostics,
clearDeferredDiagnostics: lifecycle.clearDeferredDiagnostics,
refreshTabHeader: thread.refreshTabHeader,
resetThreadTurnPresence: thread.resetTurnPresence,
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
configuredCommand: () => plugin.settings.codexPath,
refreshLiveState: liveState.refresh,
render: render.now,
scheduleRender: render.schedule,
notifyConnectionFailed: () => {
new Notice("Codex app-server connection failed.");
},
}),
};
}

View file

@ -0,0 +1,185 @@
import type { ConnectionManager } from "../../../app-server/connection-manager";
import { recoverRolloutTokenUsage } from "../../../app-server/rollout-token-usage";
import { ChatRuntimeSettingsController } from "../controllers/runtime/runtime-settings-controller";
import { ChatThreadActionController } from "../controllers/thread/thread-actions-controller";
import { ChatThreadGoalController } from "../controllers/thread/thread-goal-controller";
import { ThreadHistoryController } from "../controllers/thread/thread-history-controller";
import { ThreadIdentityController } from "../controllers/thread/thread-identity-controller";
import { ThreadRenameController } from "../controllers/thread/thread-rename-controller";
import { ThreadResumeController } from "../controllers/thread/thread-resume-controller";
import { createThreadSelectionActions } from "../controllers/thread/thread-selection-controller";
import { RestoredThreadController } from "../controllers/thread/restored-thread-controller";
import { ChatReconnectController } from "../controllers/connection/reconnect-controller";
import { createChatViewStateActions } from "../controllers/view/view-state-controller";
import { ToolbarPanelController } from "./toolbar-controller";
import type { ChatPanelContext } from "./context";
export function createThreadControllerGroup(
context: ChatPanelContext,
refs: {
connection: ConnectionManager;
},
) {
const { obsidian, plugin, state, runtime, thread, status, liveState, scroll, render, client, composer, lifecycle } = context;
const stateStore = state.stateStore;
const currentClient = client.getClient;
const { deferredTasks, resumeWork } = lifecycle;
const history = new ThreadHistoryController({
stateStore,
currentClient,
render: render.now,
addSystemMessage: status.addSystemMessage,
forceMessagesToBottom: scroll.forceBottom,
keepCurrentScrollPosition: scroll.preservePosition,
setThreadTurnPresence: thread.resetTurnPresence,
});
const threadActions = new ChatThreadActionController({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
archiveAdapter: obsidian.archiveAdapter,
ensureConnected: client.ensureConnected,
currentClient,
history,
addSystemMessage: status.addSystemMessage,
setStatus: status.set,
setComposerText: composer.setText,
openThreadInNewView: (threadId) => plugin.openThreadInNewView(threadId),
openThreadInCurrentPanel: thread.selectThread,
notifyThreadArchived: plugin.notifyThreadArchived.bind(plugin),
notifyThreadRenamed: (threadId, name) => {
plugin.notifyThreadRenamed(threadId, name);
},
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
refreshThreads: thread.refreshThreads,
refreshSharedThreadListFromOpenSurface: () => {
plugin.refreshSharedThreadListFromOpenSurface();
},
});
const toolbarPanels = new ToolbarPanelController({
stateStore,
threadActions,
scheduleRender: render.schedule,
});
const threadSelection = createThreadSelectionActions({
stateStore,
closeForThreadSelection: () => {
toolbarPanels.closeForThreadSelection();
},
focusThreadInOpenView: (threadId) => plugin.focusThreadInOpenView(threadId),
resumeThread: thread.resumeThread,
addSystemMessage: status.addSystemMessage,
});
const reconnectActions = new ChatReconnectController({
stateStore,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredDiagnostics: lifecycle.clearDeferredDiagnostics,
reconnect: () => {
refs.connection.reconnect();
},
clearClient: client.clear,
setStatus: status.set,
render: render.now,
ensureConnected: client.ensureConnected,
resumeThread: thread.resumeThread,
addSystemMessage: status.addSystemMessage,
});
const runtimeSettings = new ChatRuntimeSettingsController({
stateStore,
currentClient,
runtimeSnapshot: runtime.runtimeSnapshot,
collaborationModeLabel: runtime.collaborationModeLabel,
addSystemMessage: status.addSystemMessage,
});
const goals = new ChatThreadGoalController({
stateStore,
currentClient,
ensureConnected: client.ensureConnected,
addSystemMessage: status.addSystemMessage,
addGoalEvent: (item) => {
stateStore.dispatch({ type: "transcript/item-upserted", item });
},
render: render.now,
refreshLiveState: liveState.refresh,
});
const restoredThread = new RestoredThreadController({
deferredTasks,
opened: lifecycle.getOpened,
resumeThread: thread.resumeThread,
invalidateResumeWork: lifecycle.invalidateResumeWork,
stateStore,
systemItem: state.systemItem,
setStatus: status.set,
refreshTabHeader: thread.refreshTabHeader,
});
const viewStateController = createChatViewStateActions({
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearRestoredThreadLifecycle: thread.clearRestoredLifecycle,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
restoreThreadPlaceholder: thread.restorePlaceholder,
});
const threadResume = new ThreadResumeController({
stateStore,
vaultPath: plugin.vaultPath,
resumeWork,
history,
restoredThread,
currentClient,
ensureConnected: client.ensureConnected,
closing: lifecycle.getClosing,
systemItem: state.systemItem,
resetThreadTurnPresence: thread.resetTurnPresence,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
addSystemMessage: status.addSystemMessage,
forceMessagesToBottom: scroll.forceBottom,
render: render.now,
refreshLiveState: liveState.refresh,
syncThreadGoal: (threadId) => goals.syncThreadGoal(threadId),
recoverTokenUsageFromRollout: (path) =>
recoverRolloutTokenUsage(path, async (filePath, options) => {
const response = await currentClient()?.readFile(filePath, options);
return response?.dataBase64 ?? "";
}),
});
const threadIdentity = new ThreadIdentityController({
stateStore,
restoredThread,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
resetThreadTurnPresence: thread.resetTurnPresence,
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,
toolbarPanels,
threadSelection,
reconnectActions,
runtimeSettings,
goals,
restoredThread,
viewStateController,
threadResume,
threadIdentity,
threadRename,
};
}

View file

@ -0,0 +1,208 @@
import type { ChatAppServerThreadController } from "../app-server/thread-controller";
import { ChatComposerController } from "../composer/controller";
import { activeTurnId } from "../chat-state";
import type { ChatReconnectController } from "../controllers/connection/reconnect-controller";
import { PendingRequestController } from "../controllers/requests/pending-request-controller";
import type { ChatRuntimeSettingsController } from "../controllers/runtime/runtime-settings-controller";
import { ComposerSubmissionController } from "../controllers/submission/composer-submission-controller";
import { PlanImplementationController } from "../controllers/submission/plan-implementation-controller";
import { SlashCommandController } from "../controllers/submission/slash-command-controller";
import { TurnSubmissionController } from "../controllers/submission/turn-submission-controller";
import type { ChatThreadActionController } from "../controllers/thread/thread-actions-controller";
import type { ChatThreadGoalController } from "../controllers/thread/thread-goal-controller";
import type { ThreadHistoryController } from "../controllers/thread/thread-history-controller";
import type { ThreadRenameController } from "../controllers/thread/thread-rename-controller";
import type { ChatInboundController } from "../inbound/controller";
import { currentModel } from "../../../runtime/state";
import { ChatMessageRenderer } from "../ui/message-stream";
import type { ChatPanelContext } from "./context";
export function createTurnControllerGroup(
context: ChatPanelContext,
refs: {
controller: ChatInboundController;
appServerThreads: ChatAppServerThreadController;
runtimeSettings: ChatRuntimeSettingsController;
threadActions: ChatThreadActionController;
threadRename: ThreadRenameController;
reconnectActions: ChatReconnectController;
goals: ChatThreadGoalController;
history: ThreadHistoryController;
},
) {
const { plugin, state, render, runtime, thread, liveState, scroll, status, lifecycle, client } = context;
const { app, owner, viewId } = context.obsidian;
const stateStore = state.stateStore;
const currentClient = client.getClient;
const { messageScrollIntent } = lifecycle;
const composerController = new ChatComposerController({
app,
stateStore,
viewId,
sendShortcut: () => plugin.settings.sendShortcut,
scrollThreadFromComposerEdges: () => plugin.settings.scrollThreadFromComposerEdges,
canInterrupt: () =>
state.getState().turn.lifecycle.kind !== "idle" && Boolean(state.getState().activeThread.id && activeTurnId(state.getState())),
composerPlaceholder: render.composerPlaceholder,
composerMeta: render.composerMetaViewModel,
currentModelForSuggestions: () => currentModel(runtime.runtimeSnapshot()),
togglePlan: () => void refs.runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
toggleFast: () => void refs.runtimeSettings.toggleFastMode(),
renderIfDetached: render.now,
onDraftChange: liveState.refresh,
onComposerResize: () => {
scroll.correctAfterLayoutChange();
},
});
const pendingRequests = new PendingRequestController({
stateStore,
controller: refs.controller,
composerHasFocus: () => composerController.hasFocus(),
refreshLiveState: liveState.refresh,
render: render.now,
});
const turnSubmission = new TurnSubmissionController({
stateStore,
connection: {
vaultPath: plugin.vaultPath,
currentClient,
},
restoredThread: {
ensureRestoredThreadLoaded: thread.ensureRestoredThreadLoaded,
},
thread: {
startThread: (preview) => refs.appServerThreads.startThread(preview),
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
resetThreadTurnPresence: thread.resetTurnPresence,
},
runtime: {
applyPendingThreadSettings: () => refs.runtimeSettings.applyPendingThreadSettings(),
},
composer: {
codexInput: (text) => composerController.codexInput(text),
setDraft: (text, options) => {
composerController.setDraft(text, options);
},
},
view: {
forceMessagesToBottom: scroll.forceBottom,
render: render.now,
scheduleRender: render.schedule,
},
status: {
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
},
});
const slashCommands = new SlashCommandController({
stateStore,
currentClient,
codexInput: (text) => composerController.codexInput(text),
threads: {
startNewThread: thread.startNewThread,
startThreadForGoal: async (objective) => {
const response = await refs.appServerThreads.startThread(objective, { syncGoal: false });
return response?.thread.id ?? null;
},
resumeThread: thread.selectThread,
forkThread: (threadId) => refs.threadActions.forkThread(threadId),
rollbackThread: (threadId) => refs.threadActions.rollbackThread(threadId),
compactThread: (threadId) => refs.threadActions.compactThread(threadId),
archiveThread: (threadId) => refs.threadActions.archiveThread(threadId),
renameThread: (threadId, name) => refs.threadRename.rename(threadId, name),
reconnect: () => refs.reconnectActions.reconnectPanel(),
},
runtime: {
toggleFastMode: () => refs.runtimeSettings.toggleFastMode(),
toggleCollaborationMode: () => refs.runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
setRequestedModel: (model) => refs.runtimeSettings.setRequestedModel(model),
setRequestedReasoningEffort: (effort) => refs.runtimeSettings.setRequestedReasoningEffort(effort),
},
goals: {
activeGoal: () => refs.goals.activeGoal(),
setObjective: (threadId, objective, tokenBudget) => refs.goals.setObjective(threadId, objective, tokenBudget),
setStatus: (threadId, goalStatus) => refs.goals.setStatus(threadId, goalStatus),
clear: (threadId) => refs.goals.clear(threadId),
},
status: {
addSystemMessage: status.addSystemMessage,
addStructuredSystemMessage: status.addStructuredSystemMessage,
setStatus: status.set,
statusSummaryLines: runtime.statusSummaryLines,
connectionDiagnosticDetails: runtime.connectionDiagnosticDetails,
mcpStatusLines: runtime.mcpStatusLines,
modelStatusLines: runtime.modelStatusLines,
effortStatusLines: runtime.effortStatusLines,
},
});
const planImplementation = new PlanImplementationController({
stateStore,
connection: {
currentClient,
ensureConnected: client.ensureConnected,
},
submission: {
sendTurnText: (text) => turnSubmission.sendTurnText(text),
},
});
const messageRenderer = new ChatMessageRenderer({
obsidian: {
app,
owner,
},
state: {
store: stateStore,
},
workspace: {
vaultPath: plugin.vaultPath,
},
scroll: {
consumeIntent: () => messageScrollIntent.consumeIntent(),
},
history: {
loadOlderTurns: () => void refs.history.loadOlder(),
},
actions: {
rollbackThread: (threadId) => void refs.threadActions.rollbackThread(threadId),
forkThreadFromTurn: (threadId, turnId, archiveSource) => void refs.threadActions.forkThreadFromTurn(threadId, turnId, archiveSource),
implementPlan: (item) => void planImplementation.implement(item),
openTurnDiff: (state) => void plugin.openTurnDiff(state),
},
requests: {
pendingSignature: render.pendingRequestsSignature,
renderPending: () => pendingRequests.renderNode(),
},
});
const composerSubmission = new ComposerSubmissionController({
stateStore,
composer: composerController,
slashCommands,
turnSubmission,
connection: {
currentClient,
ensureConnected: client.ensureConnected,
},
status: {
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
},
});
composerController.setActionHandlers({
submit: () => void composerSubmission.submit(),
threadScrollFromComposer: (action) => {
messageRenderer.scrollFromComposer(action);
},
});
return {
pendingRequests,
messageRenderer,
composerController,
composerSubmission,
};
}

View file

@ -0,0 +1,101 @@
import type { ConnectionManager } from "../../../app-server/connection-manager";
import type { ChatAppServerMetadataController } from "../app-server/metadata-controller";
import type { ChatAppServerThreadController } from "../app-server/thread-controller";
import type { ChatComposerController } from "../composer/controller";
import { createAppServerWarmupActions } from "../controllers/connection/app-server-warmup-controller";
import { ChatViewOpenCloseController } from "../controllers/view/view-open-close-controller";
import { ChatViewRenderController } from "../controllers/view/view-render-controller";
import type { ChatMessageRenderer } from "../ui/message-stream";
import { applyCachedSharedAppServerState } from "./cached-app-server-state";
import type { ChatPanelContext } from "./context";
import { createChatShellRenderPort } from "./shell-render";
export function createViewRenderControllerGroup(
context: ChatPanelContext,
refs: {
connection: ConnectionManager;
},
) {
const { plugin, render, lifecycle } = context;
const { deferredTasks } = lifecycle;
return {
renderController: new ChatViewRenderController({
shell: createChatShellRenderPort(context.state.stateStore, {
connected: () => refs.connection.isConnected(),
showToolbar: () => plugin.settings.showToolbar,
pendingRequestsSignature: render.pendingRequestsSignature,
activeComposerThreadName: render.activeComposerThreadName,
}),
panelRoot: render.panelRoot,
renderToolbar: render.renderToolbar,
renderGoal: render.renderGoal,
renderMessages: render.renderMessages,
renderComposer: render.renderComposer,
clearScheduledRender: () => {
deferredTasks.clearRender();
},
}),
};
}
export function createConnectionLifecycleControllerGroup(
context: ChatPanelContext,
refs: {
connection: ConnectionManager;
composerController: ChatComposerController;
messageRenderer: ChatMessageRenderer;
appServerThreads: ChatAppServerThreadController;
appServerMetadata: ChatAppServerMetadataController;
},
) {
const { obsidian, lifecycle, render, liveState, scroll, client } = context;
const { deferredTasks } = lifecycle;
return {
appServerWarmup: createAppServerWarmupActions({
deferredTasks,
opened: lifecycle.getOpened,
closing: lifecycle.getClosing,
connected: () => refs.connection.isConnected(),
ensureConnected: client.ensureConnected,
}),
openCloseController: new ChatViewOpenCloseController({
setOpened: lifecycle.setOpened,
setClosing: lifecycle.setClosing,
registerEvent: obsidian.registerEvent,
registerComposerNoteIndexInvalidation: (register) => {
refs.composerController.registerNoteIndexInvalidation(register);
},
registerPointerDown: obsidian.registerPointerDown,
registerActiveLeafChange: obsidian.registerActiveLeafChange,
isOwnLeaf: obsidian.isOwnLeaf,
scrollMessagesToBottomOnFocus: scroll.bottomOnFocus,
applyCachedSharedAppServerState: () => {
applyCachedSharedAppServerState(context, refs.appServerThreads, refs.appServerMetadata);
},
render: render.now,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
scheduleDeferredRestoredThreadHydration: lifecycle.scheduleDeferredRestoredThreadHydration,
closeToolbarPanelOnOutsidePointer: render.closeToolbarPanelOnOutsidePointer,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredTasks: () => {
deferredTasks.clearAll();
},
panelRoot: render.panelRoot,
disposeMessages: () => {
refs.messageRenderer.dispose();
},
disposeComposer: () => {
refs.composerController.dispose();
},
disconnect: () => {
refs.connection.disconnect();
},
clearClient: client.clear,
refreshLiveState: liveState.refresh,
deferRefreshLiveState: liveState.deferRefresh,
}),
};
}