Extract chat panel session composition parts

This commit is contained in:
murashit 2026-06-17 10:41:38 +09:00
parent 901708278b
commit ca4db5f0cc
3 changed files with 1052 additions and 918 deletions

View file

@ -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<AppServerClient["respondToServerRequest"]>[0];
type RejectRequestId = Parameters<AppServerClient["rejectServerRequest"]>[0];
type ChatPanelGoalSyncActions = ReturnType<typeof createThreadGoalSyncActions>;
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<void>;
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,
},
};
}

View file

@ -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<void>;
notifyActiveThreadIdentityChanged: () => void;
refreshTabHeader: () => void;
refreshLiveState: () => void;
deferLiveStateRefresh: () => void;
startNewThread: () => Promise<void>;
statusSummaryLines: () => string[];
modelStatusLines: () => string[];
effortStatusLines: () => string[];
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
}
type ChatPanelGoalSyncActions = ReturnType<typeof createThreadGoalSyncActions>;
type ChatPanelRuntimeSettingsActions = ReturnType<typeof createChatRuntimeSettingsActions>;
type ChatPanelGoalActions = ReturnType<typeof createGoalActions>;
type ChatPanelThreadLifecycle = ReturnType<typeof createThreadLifecycleParts>;
type ChatPanelThreadActions = ReturnType<typeof createThreadManagementActions>;
type ChatPanelSelectionActions = ReturnType<typeof createSelectionActions>;
interface ChatPanelThreadActionParts {
actions: ChatPanelThreadActions;
toolbarPanels: ToolbarPanelActions;
selection: ChatPanelSelectionActions;
}
interface ChatPanelComposerAndTurnParts {
pendingRequests: PendingRequestController;
reconnect: () => Promise<void>;
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<void>,
): 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<void>,
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<void>,
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<void>,
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<void>;
currentClient: CurrentAppServerClient;
status: ChatPanelSessionStatus;
composerController: ChatComposerController;
resume: ResumeController;
refreshActiveThreads: () => Promise<void>;
},
): 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<void>;
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<void>;
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);
}

View file

@ -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<AppServerClient["respondToServerRequest"]>[0];
type RejectRequestId = Parameters<AppServerClient["rejectServerRequest"]>[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<typeof createThreadGoalSyncActions>;
type ChatPanelRuntimeSettingsActions = ReturnType<typeof createChatRuntimeSettingsActions>;
type ChatPanelGoalActions = ReturnType<typeof createGoalActions>;
type ChatPanelThreadLifecycle = ReturnType<typeof createThreadLifecycleParts>;
type ChatPanelThreadActions = ReturnType<typeof createThreadManagementActions>;
type ChatPanelSelectionActions = ReturnType<typeof createSelectionActions>;
type ChatPanelConversationTurnActions = ReturnType<typeof createConversationTurnActions>;
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<void>;
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<typeof createChatPanelSessionParts>[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<void>): 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<void>,
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<void>,
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<void>,
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<void>;
currentClient: CurrentAppServerClient;
status: ChatPanelSessionStatus;
composerController: ChatComposerController;
resume: ResumeController;
refreshActiveThreads: () => Promise<void>;
}): 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<void>;
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<void>;
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(),
};
}