mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Extract chat panel session composition parts
This commit is contained in:
parent
901708278b
commit
ca4db5f0cc
3 changed files with 1052 additions and 918 deletions
238
src/features/chat/host/connection-bundle.ts
Normal file
238
src/features/chat/host/connection-bundle.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
790
src/features/chat/host/session-parts.ts
Normal file
790
src/features/chat/host/session-parts.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue