Reorganize chat host session graph

This commit is contained in:
murashit 2026-06-27 10:32:29 +09:00
parent b47a43e763
commit 318ffb4532
18 changed files with 1059 additions and 854 deletions

View file

@ -0,0 +1,90 @@
import { runtimeConfigOrDefault } from "../../../domain/runtime/config";
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
import type { ChatStateStore } from "../application/state/store";
import { resolveRuntimeControls } from "../domain/runtime/resolution";
import { ChatComposerController } from "../panel/composer-controller";
import { type ChatPanelComposerSurface, chatPanelComposerProjection } from "../panel/surface/composer-projection";
import type { ChatMessageScrollController } from "../panel/surface/message-stream-scroll";
import { VaultNoteCandidateProvider } from "../panel/vault-note-candidate-provider";
import type { ChatPanelEnvironment } from "./contracts";
import type { ChatPanelRuntimeSettingsActions } from "./runtime-bundle";
import type { ChatPanelThreadLifecycle } from "./thread-bundle";
interface ChatPanelComposerHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
messageScrollController: ChatMessageScrollController;
}
export interface ChatPanelComposerBundle {
controller: ChatComposerController;
dispose(): void;
}
export function createComposerBundle(
host: ChatPanelComposerHost,
input: {
threadLifecycle: ChatPanelThreadLifecycle;
runtimeSettings: ChatPanelRuntimeSettingsActions;
},
): ChatPanelComposerBundle {
const surface = createSessionComposerSurface(input.threadLifecycle, input.runtimeSettings);
const controller = createSessionComposerController(host, surface, input.runtimeSettings);
return {
controller,
dispose: () => {
controller.dispose();
},
};
}
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: ChatPanelComposerHost,
composerSurface: ChatPanelComposerSurface,
runtimeSettings: ChatPanelRuntimeSettingsActions,
): ChatComposerController {
const { environment, stateStore } = host;
return new ChatComposerController({
noteCandidateProvider: new VaultNoteCandidateProvider(environment.obsidian.app),
sourcePath: () => environment.obsidian.app.workspace.getActiveFile()?.path ?? "",
stateStore,
viewId: environment.obsidian.viewId,
sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut,
scrollThreadFromComposerEdges: () => environment.plugin.settingsRef.settings.scrollThreadFromComposerEdges,
canInterrupt: (state) => {
return state.turnBusy && Boolean(state.activeThreadId && state.activeTurnId);
},
composerProjection: (state) => chatPanelComposerProjection(composerSurface, state),
currentModelForSuggestions: () => {
const current = stateStore.getState();
const config = runtimeConfigOrDefault(current.connection.runtimeConfig);
return resolveRuntimeControls(runtimeSnapshotForChatState(current), config).model.effective;
},
threadScrollFromComposer: (action) => {
host.messageScrollController.scrollFromComposer(action);
},
togglePlan: () => void runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void runtimeSettings.toggleAutoReview(),
toggleFast: () => void runtimeSettings.toggleFastMode(),
onDraftChange: () => {
environment.plugin.workspace.refreshThreadsViewLiveState();
},
onHeightChange: () => undefined,
});
}

View file

@ -20,7 +20,7 @@ import type { ChatConnectionPhase } from "../application/state/root-reducer";
import type { ChatStateStore } from "../application/state/store";
import type { AutoTitleCoordinator } from "../application/threads/auto-title-coordinator";
import type { createThreadGoalSyncActions } from "../application/threads/goal-actions";
import type { ChatPanelEnvironment } from "./environment";
import type { ChatPanelEnvironment } from "./contracts";
export type CurrentAppServerClient = () => AppServerClient | null;

View file

@ -1,5 +1,7 @@
import type { App, Component, EventRef } from "obsidian";
import type { AppServerClient } from "../../../app-server/connection/client";
import type { AppServerQueryContext } from "../../../app-server/query/keys";
import type { ArchiveExportDestination } from "../../../app-server/services/thread-archive-markdown";
import type { ModelMetadata } from "../../../domain/catalog/metadata";
import type { ObservedResultListener } from "../../../domain/observed-result";
@ -7,6 +9,7 @@ import type { SharedServerMetadata } from "../../../domain/server/metadata";
import type { CodexPanelSettings } from "../../../settings/model";
import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../../workspace/thread-catalog";
import type { ChatTurnDiffViewState } from "../domain/turn-diff";
import type { ChatPanelSnapshot } from "../panel/snapshot";
export interface CodexChatHost {
readonly settingsRef: PluginSettingsRef;
@ -58,3 +61,40 @@ export interface ChatPanelEnvironment {
refreshTabHeader: () => void;
};
}
export interface ChatViewLifecycleSurface {
displayTitle(): string;
persistedState(): Record<string, unknown>;
applyViewState(state: unknown): void;
open(): void;
close(): void;
refreshSettings(): void;
}
export interface ChatWorkspacePanelSurface {
openPanelSnapshot(): ChatPanelSnapshot;
openThread(threadId: string): Promise<void>;
focusThread(threadId?: string | null): Promise<void>;
hydrateRestoredThread(): Promise<void>;
focusComposer(): void;
connect(): Promise<void>;
startNewThread(): Promise<void>;
}
export interface ChatSharedThreadSurface {
refreshSharedThreads(): Promise<void>;
applyThreadArchived(threadId: string): void;
applyThreadRenamed(threadId: string, name: string | null): void;
}
export interface ChatPanelClientSurface {
canServeAppServerContext(context: AppServerQueryContext): boolean;
runWithAppServerClient<T>(operation: (client: AppServerClient) => Promise<T>): Promise<T>;
}
export type ChatPanelHandle = ChatViewLifecycleSurface &
ChatWorkspacePanelSurface &
ChatSharedThreadSurface &
ChatPanelClientSurface & {
setComposerText(text: string): void;
};

View file

@ -0,0 +1,136 @@
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
import { connectionDiagnosticSectionsFromState } from "../application/connection/diagnostic-sections";
import { toolInventoryDiagnosticSections } from "../application/connection/tool-inventory-diagnostic-sections";
import { type ChatRuntimeSettingsActions, createChatRuntimeSettingsActions } from "../application/runtime/settings-actions";
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
import type { ChatStateStore } from "../application/state/store";
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
import { collaborationModeLabel as formatCollaborationModeLabel } from "../domain/runtime/labels";
import type { RuntimeSnapshot } from "../domain/runtime/snapshot";
import {
effortStatusLines as buildEffortStatusLines,
modelStatusLines as buildModelStatusLines,
statusSummaryLines as buildStatusSummaryLines,
} from "../presentation/runtime/status";
import type { CurrentAppServerClient } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./contracts";
export type ChatPanelRuntimeSettingsActions = ChatRuntimeSettingsActions;
interface ChatPanelRuntimeStatus {
addSystemMessage: (text: string) => void;
}
interface ChatPanelRuntimeHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
}
export interface ChatPanelRuntimeProjection {
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
modelStatusLines: () => string[];
effortStatusLines: () => string[];
statusSummaryLines: () => string[];
toolInventoryDetails: () => MessageStreamNoticeSection[];
}
interface ChatPanelRuntimeBundle {
settings: ChatPanelRuntimeSettingsActions;
projection: ChatPanelRuntimeProjection;
}
export function createRuntimeBundle(
host: ChatPanelRuntimeHost,
input: {
connection: ConnectionManager;
currentClient: CurrentAppServerClient;
status: ChatPanelRuntimeStatus;
},
): ChatPanelRuntimeBundle {
return {
settings: createSessionRuntimeSettingsActions(host, input.currentClient, input.status),
projection: createSessionRuntimeProjection(host, input.connection),
};
}
function createSessionRuntimeSettingsActions(
host: ChatPanelRuntimeHost,
currentClient: CurrentAppServerClient,
status: ChatPanelRuntimeStatus,
): ChatPanelRuntimeSettingsActions {
return createChatRuntimeSettingsActions({
stateStore: host.stateStore,
currentClient,
runtimeSnapshotForState: runtimeSnapshotForChatState,
collaborationModeLabel: () => collaborationModeLabel(host.stateStore),
addSystemMessage: (text) => {
status.addSystemMessage(text);
},
});
}
function collaborationModeLabel(stateStore: ChatStateStore): string {
return formatCollaborationModeLabel(stateStore.getState().runtime.pending.collaborationMode);
}
function createSessionRuntimeProjection(host: ChatPanelRuntimeHost, connection: ConnectionManager): ChatPanelRuntimeProjection {
return {
connectionDiagnosticDetails: () => connectionDiagnosticDetails(host, connection),
modelStatusLines: () => modelStatusLines(host),
effortStatusLines: () => effortStatusLines(host),
statusSummaryLines: () => statusSummaryLines(host),
toolInventoryDetails: () => toolInventoryDetails(host),
};
}
function statusSummaryLines(host: ChatPanelRuntimeHost): string[] {
const state = host.stateStore.getState();
return buildStatusSummaryLines({
activeThreadId: state.activeThread.id,
snapshot: runtimeSnapshot(host),
nowMs: Date.now(),
});
}
function modelStatusLines(host: ChatPanelRuntimeHost): string[] {
const state = host.stateStore.getState();
return buildModelStatusLines({
runtimeConfig: state.connection.runtimeConfig,
pendingModel: state.runtime.pending.model,
snapshot: runtimeSnapshot(host),
collaborationModeLabel: collaborationModeLabel(host.stateStore),
});
}
function effortStatusLines(host: ChatPanelRuntimeHost): string[] {
const state = host.stateStore.getState();
return buildEffortStatusLines({
runtimeConfig: state.connection.runtimeConfig,
pendingReasoningEffort: state.runtime.pending.reasoningEffort,
snapshot: runtimeSnapshot(host),
});
}
function connectionDiagnosticDetails(host: ChatPanelRuntimeHost, connection: ConnectionManager): MessageStreamNoticeSection[] {
const sections = connectionDiagnosticSectionsFromState({
state: host.stateStore.getState(),
connected: connection.isConnected(),
configuredCommand: host.environment.plugin.settingsRef.settings.codexPath,
});
return sections.map((section) => ({
title: section.title,
auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })),
}));
}
function toolInventoryDetails(host: ChatPanelRuntimeHost): MessageStreamNoticeSection[] {
const sections = toolInventoryDiagnosticSections(host.stateStore.getState().connection.serverDiagnostics);
return sections.map((section) => ({
title: section.title,
auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })),
}));
}
function runtimeSnapshot(host: ChatPanelRuntimeHost): RuntimeSnapshot {
return runtimeSnapshotForChatState(host.stateStore.getState());
}

View file

@ -1,104 +1,40 @@
import { Notice } from "obsidian";
import type { AppServerClientAccess } from "../../../app-server/connection/client-access";
import { ConnectionManager } from "../../../app-server/connection/connection-manager";
import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries";
import { runtimeConfigOrDefault } from "../../../domain/runtime/config";
import { normalizeExplicitThreadName } from "../../../domain/threads/model";
import { createLocalIdSource, type LocalIdSource } from "../../../shared/id/local-id";
import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work";
import { createThreadOperations, type ThreadOperations } from "../../threads/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../../threads/thread-title-service";
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 { ChatInboundHandler } from "../app-server/inbound/handler";
import { connectionDiagnosticSectionsFromState } from "../application/connection/diagnostic-sections";
import { type ChatReconnectActionsHost, reconnectPanel } from "../application/connection/reconnect-actions";
import { toolInventoryDiagnosticSections } from "../application/connection/tool-inventory-diagnostic-sections";
import type { ComposerSubmitActions } from "../application/conversation/composer-submit-actions";
import {
type ConversationTurnActions as ChatPanelConversationTurnActions,
createConversationTurnActions,
} from "../application/conversation/composition";
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../application/lifecycle";
import { createPendingRequestActions, type PendingRequestActions } from "../application/pending-requests/pending-request-actions";
import { createChatRuntimeSettingsActions } from "../application/runtime/settings-actions";
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
import { messageStreamItems } from "../application/state/message-stream";
import type { ChatAction, ChatConnectionPhase } from "../application/state/root-reducer";
import type { ChatStateStore } from "../application/state/store";
import type { ActiveThreadIdentitySync } from "../application/threads/active-thread-identity-sync";
import { type AutoTitleCoordinator, createAutoTitleCoordinator } from "../application/threads/auto-title-coordinator";
import { createGoalActions, createThreadGoalSyncActions } from "../application/threads/goal-actions";
import { HistoryController } from "../application/threads/history-controller";
import { createThreadLifecycleParts } from "../application/threads/lifecycle-parts";
import {
activeThreadRenameTitleContext,
createThreadRenameEditorActions,
type ThreadRenameEditorActions,
} from "../application/threads/rename-editor-actions";
import type { RestorationController } from "../application/threads/restoration-controller";
import type { ResumeActions } from "../application/threads/resume-actions";
import { createThreadManagementActions, type ThreadManagementActionsHost } from "../application/threads/thread-management-actions";
import { createThreadNavigationActions } from "../application/threads/thread-navigation-actions";
import { threadTitleContextFromMessageStreamItems } from "../application/threads/title-context";
import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items";
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
import { collaborationModeLabel as formatCollaborationModeLabel } from "../domain/runtime/labels";
import { resolveRuntimeControls } from "../domain/runtime/resolution";
import type { RuntimeSnapshot } from "../domain/runtime/snapshot";
import { ChatComposerController } from "../panel/composer-controller";
import { type ChatPanelComposerSurface, chatPanelComposerProjection } from "../panel/surface/composer-projection";
import type { ChatPanelGoalSurface } from "../panel/surface/goal-projection";
import type { MessageStreamPresenter } from "../panel/surface/message-stream-presenter";
import type { ChatComposerController } from "../panel/composer-controller";
import type { ChatMessageScrollController } from "../panel/surface/message-stream-scroll";
import type { ChatPanelToolbarSurface } from "../panel/surface/toolbar-projection";
import { createToolbarPanelActions, type ToolbarPanelActions } from "../panel/toolbar-actions";
import { VaultNoteCandidateProvider } from "../panel/vault-note-candidate-provider";
import {
effortStatusLines as buildEffortStatusLines,
modelStatusLines as buildModelStatusLines,
statusSummaryLines as buildStatusSummaryLines,
} from "../presentation/runtime/status";
import type { ToolbarActions } from "../ui/toolbar";
import { type ChatPanelConnectionBundle, type CurrentAppServerClient, createConnectionBundle } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./environment";
import { createChatPanelSurfaces } from "./panel-surfaces";
import { createComposerBundle } from "./composer-bundle";
import { type ChatPanelConnectionBundle, createConnectionBundle } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./contracts";
import { createRuntimeBundle } from "./runtime-bundle";
import { type ChatPanelSharedStateBinding, createChatPanelSharedStateBinding } from "./shared-state-binding";
import { type ChatPanelShellBundle, createShellBundle } from "./shell-bundle";
import { createThreadActionBundle, createThreadFoundation, createThreadLifecycleBundle } from "./thread-bundle";
import { createTurnBundle } from "./turn-bundle";
export interface ChatPanelSessionGraph {
connection: {
manager: ConnectionManager;
controller: ChatPanelConnectionBundle["connection"]["controller"];
};
serverActions: {
threads: ChatServerThreadActions;
metadata: ChatServerMetadataActions;
diagnostics: ChatServerDiagnosticsActions;
};
thread: {
history: HistoryController;
resume: ResumeActions;
restoration: RestorationController;
identity: ActiveThreadIdentitySync;
rename: ThreadRenameEditorActions;
};
toolbar: {
panels: ToolbarPanelActions;
actions: ToolbarActions;
};
composer: {
controller: ChatComposerController;
submission: ComposerSubmitActions;
};
render: {
messageStreamPresenter: MessageStreamPresenter;
};
surface: {
toolbar: ChatPanelToolbarSurface;
goal: ChatPanelGoalSurface;
composer: ChatPanelComposerSurface;
};
shell: ChatPanelShellBundle;
actions: {
invalidateThreadWork(): void;
refreshSharedThreads(): Promise<void>;
@ -129,98 +65,59 @@ interface ChatPanelSessionGraphHost {
viewWindow: () => Window;
}
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 ChatPanelThreadNavigationActions = ReturnType<typeof createThreadNavigationActions>;
interface ChatPanelThreadActionParts {
actions: ChatPanelThreadActions;
toolbarPanelActions: ToolbarPanelActions;
navigation: ChatPanelThreadNavigationActions;
}
interface ChatPanelComposerAndTurnParts {
pendingRequests: PendingRequestActions;
reconnect: () => Promise<void>;
turnActions: ChatPanelConversationTurnActions;
}
interface ChatPanelRuntimeProjection {
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
modelStatusLines: () => string[];
effortStatusLines: () => string[];
statusSummaryLines: () => string[];
toolInventoryDetails: () => MessageStreamNoticeSection[];
}
interface ChatPanelComposerAndTurnInput {
connection: ConnectionManager;
localItemIds: LocalIdSource;
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
currentClient: CurrentAppServerClient;
status: ChatPanelSessionStatus;
inboundHandler: ChatInboundHandler;
threadLifecycle: ChatPanelThreadLifecycle;
threadActions: ChatPanelThreadActions;
navigation: ChatPanelThreadNavigationActions;
composerController: ChatComposerController;
runtimeSettings: ChatPanelRuntimeSettingsActions;
serverThreads: ChatServerThreadActions;
goals: ChatPanelGoalActions;
autoTitleCoordinator: AutoTitleCoordinator;
invalidateThreadWork: () => void;
runtimeProjection: ChatPanelRuntimeProjection;
}
export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): ChatPanelSessionGraph {
const { environment, stateStore } = host;
const localItemIds = createLocalIdSource();
const connection = createConnectionManager(environment);
const currentClient = () => connection.currentClient();
const status = createSessionStatus(stateStore, localItemIds);
const titleService = createSessionThreadTitleService(host, currentClient);
const autoTitleCoordinator = createSessionAutoTitleCoordinator(host, currentClient, titleService);
const history = createSessionHistoryController(host, currentClient, status, autoTitleCoordinator);
const invalidateThreadWork = () => {
host.resumeWork.invalidate();
history.invalidate();
const refreshTabHeader = () => {
host.environment.view.refreshTabHeader();
};
const goalSync = createSessionGoalSyncActions(host, currentClient, localItemIds, status);
const refreshLiveState = () => {
host.environment.plugin.workspace.refreshThreadsViewLiveState();
};
const deferLiveStateRefresh = () => {
host.viewWindow().setTimeout(refreshLiveState, 0);
};
const notifyActiveThreadIdentityChanged = () => {
refreshTabHeader();
host.environment.obsidian.requestWorkspaceLayoutSave();
refreshLiveState();
};
const threadFoundation = createThreadFoundation(host, {
currentClient,
localItemIds,
status,
refreshLiveState,
});
const connectionBundle = createConnectionBundle(
{
environment,
stateStore,
connectionWork: host.connectionWork,
deferredTasks: host.deferredTasks,
invalidateThreadWork,
deferLiveStateRefresh: () => {
deferLiveStateRefresh(host);
},
refreshTabHeader: () => {
refreshTabHeader(host);
},
refreshLiveState: () => {
refreshLiveState(host);
invalidateThreadWork: () => {
threadFoundation.invalidateThreadWork();
},
deferLiveStateRefresh,
refreshTabHeader,
refreshLiveState,
},
{
connection,
currentClient,
localItemIds,
goalSync,
autoTitleCoordinator,
goalSync: threadFoundation.goalSync,
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
status,
},
);
const {
connection: { controller },
connection: { controller: connectionController },
inboundHandler,
} = connectionBundle;
const connectionController = controller;
const { threads: serverThreads } = connectionBundle.serverActions;
const ensureConnected = () => connectionController.ensureConnected();
const connectedClient = async () => {
@ -228,35 +125,38 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
return currentClient();
};
const refreshActiveThreads = () => connectionController.refreshActiveThreads();
const threadOperations = createSessionThreadOperations(environment, currentClient);
const runtimeSettings = createSessionRuntimeSettingsActions(host, currentClient, status);
const goals = createSessionGoalActions(host, currentClient, localItemIds, connectedClient, status, serverThreads);
const rename = createSessionThreadRenameEditorActions(stateStore, threadOperations, titleService, ensureConnected, status);
const threadLifecycle = createSessionThreadLifecycle(
host,
const runtime = createRuntimeBundle(host, {
connection,
currentClient,
status,
});
const threadLifecycle = createThreadLifecycleBundle(host, {
currentClient,
localItemIds,
connectedClient,
ensureConnected,
status,
goals,
autoTitleCoordinator,
history,
invalidateThreadWork,
);
const { identity, restoration, resume } = threadLifecycle;
const composerSurface = createSessionComposerSurface(threadLifecycle, runtimeSettings);
const composerController = createSessionComposerController(host, composerSurface, runtimeSettings);
const threadActionParts = createThreadActionParts(host, {
operations: threadOperations,
connectedClient,
currentClient,
status,
composerController,
identity,
resume,
refreshActiveThreads,
serverThreads,
foundation: threadFoundation,
refreshTabHeader,
refreshLiveState,
notifyActiveThreadIdentityChanged,
});
const composerAndTurn = createComposerAndTurnActions(host, {
const composer = createComposerBundle(host, {
threadLifecycle: threadLifecycle.lifecycle,
runtimeSettings: runtime.settings,
});
const threadActions = createThreadActionBundle(host, {
currentClient,
connectedClient,
status,
composerController: composer.controller,
foundation: threadFoundation,
lifecycle: threadLifecycle,
refreshActiveThreads,
notifyActiveThreadIdentityChanged,
});
const turn = createTurnBundle(host, {
connection,
localItemIds,
ensureConnected,
@ -264,29 +164,34 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
currentClient,
status,
inboundHandler,
threadLifecycle,
threadActions: threadActionParts.actions,
navigation: threadActionParts.navigation,
composerController,
runtimeSettings,
threadLifecycle: threadLifecycle.lifecycle,
threadActions: threadActions.actions,
navigation: threadActions.navigation,
composerController: composer.controller,
runtimeSettings: runtime.settings,
serverThreads,
goals,
autoTitleCoordinator,
invalidateThreadWork,
runtimeProjection: createSessionRuntimeProjection(host, connection),
goals: threadLifecycle.goals,
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
invalidateThreadWork: () => {
threadFoundation.invalidateThreadWork();
},
runtimeProjection: runtime.projection,
refreshLiveState,
notifyActiveThreadIdentityChanged,
});
const surfaces = createChatPanelSurfaces(host, {
const shell = createShellBundle(host, {
connection,
connectionController,
goals,
rename,
threadActions: threadActionParts.actions,
toolbarPanelActions: threadActionParts.toolbarPanelActions,
navigation: threadActionParts.navigation,
reconnect: composerAndTurn.reconnect,
history,
pendingRequests: composerAndTurn.pendingRequests,
turnActions: composerAndTurn.turnActions,
goals: threadLifecycle.goals,
rename: threadLifecycle.rename,
threadActions: threadActions.actions,
toolbarPanelActions: threadActions.toolbarPanelActions,
navigation: threadActions.navigation,
reconnect: turn.reconnect,
history: threadFoundation.history,
pendingRequests: turn.pendingRequests,
turn,
composer,
});
const refreshSharedThreads = async (): Promise<void> => {
try {
@ -296,18 +201,12 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
throw error;
}
};
const dispose = (): void => {
surfaces.messageStreamPresenter.dispose();
composerController.dispose();
};
const sharedState = createChatPanelSharedStateBinding({
stateStore: host.stateStore,
threadCatalog: host.environment.plugin.threadCatalog,
appServerQueries: host.environment.plugin.appServerQueries,
stateStore,
threadCatalog: environment.plugin.threadCatalog,
appServerQueries: environment.plugin.appServerQueries,
serverActions: connectionBundle.serverActions,
refreshTabHeader: () => {
refreshTabHeader(host);
},
refreshTabHeader,
});
return {
@ -315,44 +214,30 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
manager: connection,
controller: connectionController,
},
serverActions: connectionBundle.serverActions,
thread: {
history,
resume,
restoration,
identity,
rename,
},
toolbar: {
panels: threadActionParts.toolbarPanelActions,
actions: surfaces.toolbarActions,
resume: threadLifecycle.resume,
restoration: threadLifecycle.restoration,
identity: threadLifecycle.identity,
},
composer: {
controller: composerController,
submission: composerAndTurn.turnActions.composerSubmit,
},
render: {
messageStreamPresenter: surfaces.messageStreamPresenter,
},
surface: {
toolbar: surfaces.toolbarSurface,
goal: surfaces.goalSurface,
composer: composerSurface,
controller: composer.controller,
},
shell,
actions: {
invalidateThreadWork,
invalidateThreadWork: () => {
threadFoundation.invalidateThreadWork();
},
refreshSharedThreads,
startNewThread: () => threadActionParts.navigation.startNewThread(),
dispose,
startNewThread: () => threadActions.navigation.startNewThread(),
dispose: () => {
shell.dispose();
composer.dispose();
},
},
runtime: {
sharedState,
refreshLiveState: () => {
refreshLiveState(host);
},
deferLiveStateRefresh: () => {
deferLiveStateRefresh(host);
},
refreshLiveState,
deferLiveStateRefresh,
},
};
}
@ -361,462 +246,6 @@ function createConnectionManager(environment: ChatPanelEnvironment): ConnectionM
return new ConnectionManager(() => environment.plugin.settingsRef.settings.codexPath, environment.plugin.settingsRef.vaultPath);
}
function createSessionThreadTitleService(host: ChatPanelSessionGraphHost, currentClient: CurrentAppServerClient): ThreadTitleService {
const { environment, stateStore } = host;
return createThreadTitleService({
codexPath: () => environment.plugin.settingsRef.settings.codexPath,
vaultPath: environment.plugin.settingsRef.vaultPath,
threadNamingModel: () => environment.plugin.settingsRef.settings.threadNamingModel,
threadNamingEffort: () => environment.plugin.settingsRef.settings.threadNamingEffort,
clientAccess: createCurrentClientAccess(currentClient),
visibleContext: (threadId) => activeThreadRenameTitleContext(stateStore.getState(), threadId),
visibleCompletedTurnContext: (turnId) =>
threadTitleContextFromMessageStreamItems(turnId, messageStreamItems(stateStore.getState().messageStream)),
});
}
function createSessionAutoTitleCoordinator(
host: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
titleService: ThreadTitleService,
): AutoTitleCoordinator {
return createAutoTitleCoordinator({
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.apply({ type: "thread-renamed", threadId, name });
}
return true;
},
});
}
function createSessionHistoryController(
host: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
status: ChatPanelSessionStatus,
autoTitleCoordinator: AutoTitleCoordinator,
): HistoryController {
return new HistoryController({
stateStore: host.stateStore,
currentClient,
addSystemMessage: status.addSystemMessage,
showLatestPageAtBottom: () => {
host.messageScrollController.showLatest();
},
setThreadTurnPresence: (hadTurns) => {
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
});
}
function createSessionGoalSyncActions(
host: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
localItemIds: LocalIdSource,
status: ChatPanelSessionStatus,
): ChatPanelGoalSyncActions {
return createThreadGoalSyncActions({
stateStore: host.stateStore,
currentClient,
localItemIds,
addSystemMessage: (text) => {
status.addSystemMessage(text);
},
addGoalEvent: (item) => {
host.stateStore.dispatch({ type: "message-stream/item-upserted", item });
},
refreshLiveState: () => {
refreshLiveState(host);
},
});
}
function createSessionThreadOperations(environment: ChatPanelEnvironment, currentClient: CurrentAppServerClient): ThreadOperations {
return createThreadOperations({
clientAccess: createCurrentClientAccess(currentClient),
archiveExport: {
settings: () => ({
archiveExportFolderTemplate: environment.plugin.settingsRef.settings.archiveExportFolderTemplate,
archiveExportFilenameTemplate: environment.plugin.settingsRef.settings.archiveExportFilenameTemplate,
archiveExportTags: environment.plugin.settingsRef.settings.archiveExportTags,
}),
enabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled,
vaultPath: environment.plugin.settingsRef.vaultPath,
vaultConfigDir: environment.obsidian.app.vault.configDir,
},
archiveDestination: environment.obsidian.archiveDestination,
catalog: environment.plugin.threadCatalog,
notice: (text) => {
new Notice(text);
},
});
}
function createCurrentClientAccess(currentClient: CurrentAppServerClient): AppServerClientAccess {
return {
withClient: async (operation) => {
const client = currentClient();
if (!client) throw new Error("Codex app-server is not connected.");
const result = await operation(client);
if (currentClient() !== client) {
throw new Error("Codex app-server connection changed while running the operation.");
}
return result;
},
};
}
function createSessionRuntimeSettingsActions(
host: ChatPanelSessionGraphHost,
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: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
localItemIds: LocalIdSource,
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>,
status: ChatPanelSessionStatus,
serverThreads: ChatServerThreadActions,
): ChatPanelGoalActions {
return createGoalActions({
stateStore: host.stateStore,
currentClient,
localItemIds,
connectedClient,
startThread: (preview, options) => serverThreads.startThread(preview, options),
addSystemMessage: (text) => {
status.addSystemMessage(text);
},
addGoalEvent: (item) => {
host.stateStore.dispatch({ type: "message-stream/item-upserted", item });
},
refreshLiveState: () => {
refreshLiveState(host);
},
});
}
function createSessionThreadRenameEditorActions(
stateStore: ChatStateStore,
operations: ThreadOperations,
titleService: ThreadTitleService,
ensureConnected: () => Promise<void>,
status: ChatPanelSessionStatus,
): ThreadRenameEditorActions {
return createThreadRenameEditorActions({
stateStore,
ensureConnected,
addSystemMessage: status.addSystemMessage,
renameThread: (threadId, value, options) => operations.renameThread(threadId, value, options),
generateThreadTitle: (threadId) => titleService.generateTitle(threadId),
});
}
function createSessionThreadLifecycle(
host: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
ensureConnected: () => Promise<void>,
status: ChatPanelSessionStatus,
goals: ChatPanelGoalActions,
autoTitleCoordinator: AutoTitleCoordinator,
history: HistoryController,
invalidateThreadWork: () => void,
): ChatPanelThreadLifecycle {
return createThreadLifecycleParts({
settingsRef: host.environment.plugin.settingsRef,
stateStore: host.stateStore,
client: {
currentClient,
ensureConnected,
},
lifecycle: {
resumeWork: host.resumeWork,
history,
invalidateThreadWork,
getClosing: host.getClosing,
},
thread: {
notifyIdentityChanged: () => {
notifyActiveThreadIdentityChanged(host);
},
refreshTabHeader: () => {
refreshTabHeader(host);
},
},
status,
liveState: {
refresh: () => {
refreshLiveState(host);
},
},
goals,
resetThreadTurnPresence: (hadTurns) => {
autoTitleCoordinator.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: ChatPanelSessionGraphHost,
composerSurface: ChatPanelComposerSurface,
runtimeSettings: ChatPanelRuntimeSettingsActions,
): ChatComposerController {
const { environment, stateStore } = host;
return new ChatComposerController({
noteCandidateProvider: new VaultNoteCandidateProvider(environment.obsidian.app),
sourcePath: () => environment.obsidian.app.workspace.getActiveFile()?.path ?? "",
stateStore,
viewId: environment.obsidian.viewId,
sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut,
scrollThreadFromComposerEdges: () => environment.plugin.settingsRef.settings.scrollThreadFromComposerEdges,
canInterrupt: (state) => {
return state.turnBusy && Boolean(state.activeThreadId && state.activeTurnId);
},
composerProjection: (state) => chatPanelComposerProjection(composerSurface, state),
currentModelForSuggestions: () => {
const current = stateStore.getState();
const config = runtimeConfigOrDefault(current.connection.runtimeConfig);
return resolveRuntimeControls(runtimeSnapshotForChatState(current), config).model.effective;
},
threadScrollFromComposer: (action) => {
host.messageScrollController.scrollFromComposer(action);
},
togglePlan: () => void runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void runtimeSettings.toggleAutoReview(),
toggleFast: () => void runtimeSettings.toggleFastMode(),
onDraftChange: () => {
refreshLiveState(host);
},
onHeightChange: () => undefined,
});
}
function createThreadActionParts(
host: ChatPanelSessionGraphHost,
input: {
operations: ThreadOperations;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
currentClient: CurrentAppServerClient;
status: ChatPanelSessionStatus;
composerController: ChatComposerController;
identity: ActiveThreadIdentitySync;
resume: ResumeActions;
refreshActiveThreads: () => Promise<void>;
},
): ChatPanelThreadActionParts {
const { operations, connectedClient, currentClient, status, composerController, identity, resume, refreshActiveThreads } = input;
const { environment, stateStore } = host;
const threadManagementHost: ThreadManagementActionsHost = {
stateStore,
vaultPath: environment.plugin.settingsRef.vaultPath,
operations,
connectedClient,
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: () => {
notifyActiveThreadIdentityChanged(host);
},
refreshAfterThreadMutation: async () => {
await refreshActiveThreads();
},
applyThreadCatalogEvent: (event) => {
environment.plugin.threadCatalog.apply(event);
},
};
const actions = createThreadManagementActions(threadManagementHost);
const toolbarPanelActions = createToolbarPanelActions({
stateStore,
threadActions: actions,
});
const navigation = createThreadNavigationActions({
stateStore,
identity,
closeForThreadSelection: () => {
toolbarPanelActions.closeForThreadSelection();
},
focusThreadInOpenView: (threadId) => environment.plugin.workspace.focusThreadInOpenView(threadId),
resumeThread: (threadId) => resume.resumeThread(threadId),
addSystemMessage: status.addSystemMessage,
focusComposer: () => {
composerController.focus();
},
});
return { actions, toolbarPanelActions, navigation };
}
function createComposerAndTurnActions(
host: ChatPanelSessionGraphHost,
input: ChatPanelComposerAndTurnInput,
): ChatPanelComposerAndTurnParts {
const {
connection,
localItemIds,
ensureConnected,
connectedClient,
currentClient,
status,
inboundHandler,
threadLifecycle,
threadActions,
navigation,
composerController,
runtimeSettings,
serverThreads,
goals,
autoTitleCoordinator,
invalidateThreadWork,
runtimeProjection,
} = input;
const pendingRequests = createPendingRequestActions({
stateStore: host.stateStore,
responder: inboundHandler,
composerHasFocus: () => composerController.hasFocus(),
focusComposer: () => {
composerController.focus();
},
refreshLiveState: () => {
refreshLiveState(host);
},
});
const reconnectHost: ChatReconnectActionsHost = {
stateStore: host.stateStore,
invalidateConnectionWork: () => {
host.connectionWork.invalidate();
},
invalidateThreadWork: () => {
invalidateThreadWork();
},
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,
localItemIds,
client: {
currentClient,
connectedClient,
},
status,
runtime: {
connectionDiagnosticDetails: runtimeProjection.connectionDiagnosticDetails,
modelStatusLines: runtimeProjection.modelStatusLines,
effortStatusLines: runtimeProjection.effortStatusLines,
statusSummaryLines: runtimeProjection.statusSummaryLines,
toolInventoryDetails: runtimeProjection.toolInventoryDetails,
},
thread: {
ensureRestoredThreadLoaded: () =>
threadLifecycle.restoration.ensureLoaded((threadId) => threadLifecycle.resume.resumeThread(threadId)),
startNewThread: () => navigation.startNewThread(),
selectThread: (threadId) => navigation.selectThread(threadId),
notifyIdentityChanged: () => {
notifyActiveThreadIdentityChanged(host);
},
resetTurnPresence: (hadTurns) => {
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
},
composer: {
codexInput: (text) => composerController.codexInput(text),
trimmedDraft: () => composerController.trimmedDraft,
setDraft: (text, options) => {
composerController.setDraft(text, options);
},
},
scroll: {
showLatest: () => {
host.messageScrollController.showLatest();
},
},
},
{
threadStarter: serverThreads,
runtimeSettings,
threadActions,
reconnectPanel: reconnect,
goals,
},
);
return {
pendingRequests,
reconnect,
turnActions,
};
}
function collaborationModeLabel(stateStore: ChatStateStore): string {
return formatCollaborationModeLabel(stateStore.getState().runtime.pending.collaborationMode);
}
function createSessionRuntimeProjection(host: ChatPanelSessionGraphHost, connection: ConnectionManager): ChatPanelRuntimeProjection {
return {
connectionDiagnosticDetails: () => connectionDiagnosticDetails(host, connection),
modelStatusLines: () => modelStatusLines(host),
effortStatusLines: () => effortStatusLines(host),
statusSummaryLines: () => statusSummaryLines(host),
toolInventoryDetails: () => toolInventoryDetails(host),
};
}
function createSessionStatus(stateStore: ChatStateStore, localItemIds: LocalIdSource): ChatPanelSessionStatus {
return {
set: (statusText, phase) => {
@ -837,75 +266,3 @@ function createSessionStatus(stateStore: ChatStateStore, localItemIds: LocalIdSo
function dispatch(stateStore: ChatStateStore, action: ChatAction): void {
stateStore.dispatch(action);
}
function notifyActiveThreadIdentityChanged(host: ChatPanelSessionGraphHost): void {
refreshTabHeader(host);
host.environment.obsidian.requestWorkspaceLayoutSave();
refreshLiveState(host);
}
function refreshTabHeader(host: ChatPanelSessionGraphHost): void {
host.environment.view.refreshTabHeader();
}
function refreshLiveState(host: ChatPanelSessionGraphHost): void {
host.environment.plugin.workspace.refreshThreadsViewLiveState();
}
function deferLiveStateRefresh(host: ChatPanelSessionGraphHost): void {
host.viewWindow().setTimeout(() => {
refreshLiveState(host);
}, 0);
}
function statusSummaryLines(host: ChatPanelSessionGraphHost): string[] {
const state = host.stateStore.getState();
return buildStatusSummaryLines({
activeThreadId: state.activeThread.id,
snapshot: runtimeSnapshot(host),
nowMs: Date.now(),
});
}
function modelStatusLines(host: ChatPanelSessionGraphHost): string[] {
const state = host.stateStore.getState();
return buildModelStatusLines({
runtimeConfig: state.connection.runtimeConfig,
pendingModel: state.runtime.pending.model,
snapshot: runtimeSnapshot(host),
collaborationModeLabel: collaborationModeLabel(host.stateStore),
});
}
function effortStatusLines(host: ChatPanelSessionGraphHost): string[] {
const state = host.stateStore.getState();
return buildEffortStatusLines({
runtimeConfig: state.connection.runtimeConfig,
pendingReasoningEffort: state.runtime.pending.reasoningEffort,
snapshot: runtimeSnapshot(host),
});
}
function connectionDiagnosticDetails(host: ChatPanelSessionGraphHost, connection: ConnectionManager): MessageStreamNoticeSection[] {
const sections = connectionDiagnosticSectionsFromState({
state: host.stateStore.getState(),
connected: connection.isConnected(),
configuredCommand: host.environment.plugin.settingsRef.settings.codexPath,
});
return sections.map((section) => ({
title: section.title,
auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })),
}));
}
function toolInventoryDetails(host: ChatPanelSessionGraphHost): MessageStreamNoticeSection[] {
const sections = toolInventoryDiagnosticSections(host.stateStore.getState().connection.serverDiagnostics);
return sections.map((section) => ({
title: section.title,
auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })),
}));
}
function runtimeSnapshot(host: ChatPanelSessionGraphHost): RuntimeSnapshot {
return runtimeSnapshotForChatState(host.stateStore.getState());
}

View file

@ -9,12 +9,11 @@ import { type ChatStateStore, createChatStateStore } from "../application/state/
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom";
import { type ChatPanelSnapshot, openPanelTurnLifecycle, parseRestoredThreadState } from "../panel/snapshot";
import { type ChatMessageScrollController, createChatMessageScrollController } from "../panel/surface/message-stream-scroll";
import { createChatViewDeferredTasks } from "./deferred-tasks";
import type { ChatPanelEnvironment } from "./environment";
import type { ChatPanelEnvironment, ChatPanelHandle } from "./contracts";
import { createChatViewDeferredTasks } from "./deferred-work";
import { type ChatPanelSessionGraph, createChatPanelSessionGraph } from "./session-graph";
import type { ChatSurfaceHandle } from "./surface-handle";
export class ChatPanelSession implements ChatSurfaceHandle {
export class ChatPanelSession implements ChatPanelHandle {
private readonly stateStore: ChatStateStore = createChatStateStore();
private readonly graph: ChatPanelSessionGraph;
@ -191,20 +190,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
renderChatPanelShell(root, {
stateStore: this.stateStore,
showToolbar: this.environment.plugin.settingsRef.settings.showToolbar,
parts: {
toolbar: {
surface: this.graph.surface.toolbar,
actions: this.graph.toolbar.actions,
},
goal: this.graph.surface.goal,
messageStream: this.graph.render.messageStreamPresenter,
composer: {
presenter: this.graph.composer.controller,
actions: {
submit: () => void this.graph.composer.submission.submit(),
},
},
},
parts: this.graph.shell.parts,
});
}
@ -230,12 +216,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
}
private closeToolbarPanelOnOutsidePointer(event: PointerEvent): void {
this.graph.toolbar.panels.closeOnOutsidePointer({
target: event.target,
viewWindow: this.environment.view.viewWindow() as (Window & { Element: typeof Element }) | null,
contains: (element) => this.environment.view.containsElement(element),
renameEditing: this.graph.thread.rename.isEditing(),
});
this.graph.shell.closeToolbarPanelOnOutsidePointer(event);
}
private activeThreadTitle(): string | null {

View file

@ -1,33 +1,27 @@
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
import type { ConversationTurnActions } from "../application/conversation/composition";
import type { PendingRequestActions } from "../application/pending-requests/pending-request-actions";
import type { ChatStateStore } from "../application/state/store";
import type { createGoalActions } from "../application/threads/goal-actions";
import type { HistoryController } from "../application/threads/history-controller";
import type { ThreadRenameEditorActions } from "../application/threads/rename-editor-actions";
import type { createThreadManagementActions } from "../application/threads/thread-management-actions";
import type { createThreadNavigationActions } from "../application/threads/thread-navigation-actions";
import type { ChatPanelShellParts } from "../panel/shell.dom";
import type { ChatPanelGoalSurface } from "../panel/surface/goal-projection";
import { MessageStreamPresenter } from "../panel/surface/message-stream-presenter";
import type { ChatMessageScrollController } from "../panel/surface/message-stream-scroll";
import type { ChatPanelToolbarSurface } from "../panel/surface/toolbar-projection";
import { createToolbarUiActions, type ToolbarPanelActions } from "../panel/toolbar-actions";
import type { ToolbarActions } from "../ui/toolbar";
import type { ChatPanelComposerBundle } from "./composer-bundle";
import type { ChatPanelConnectionBundle } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./environment";
import type { ChatPanelEnvironment } from "./contracts";
import type { ChatPanelGoalActions, ChatPanelThreadActions, ChatPanelThreadNavigationActions } from "./thread-bundle";
import type { ChatPanelTurnBundle } from "./turn-bundle";
type ChatPanelGoalActions = ReturnType<typeof createGoalActions>;
type ChatPanelThreadActions = ReturnType<typeof createThreadManagementActions>;
type ChatPanelThreadNavigationActions = ReturnType<typeof createThreadNavigationActions>;
type ChatPanelConversationTurnActions = ConversationTurnActions;
export interface ChatPanelSurfacesHost {
interface ChatPanelShellBundleHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
messageScrollController: ChatMessageScrollController;
}
export interface ChatPanelSurfacesInput {
interface ChatPanelShellBundleInput {
connection: ConnectionManager;
connectionController: ChatPanelConnectionBundle["connection"]["controller"];
goals: ChatPanelGoalActions;
@ -38,17 +32,17 @@ export interface ChatPanelSurfacesInput {
reconnect: () => Promise<void>;
history: HistoryController;
pendingRequests: PendingRequestActions;
turnActions: ChatPanelConversationTurnActions;
turn: ChatPanelTurnBundle;
composer: ChatPanelComposerBundle;
}
export interface ChatPanelSurfaces {
toolbarActions: ToolbarActions;
toolbarSurface: ChatPanelToolbarSurface;
goalSurface: ChatPanelGoalSurface;
messageStreamPresenter: MessageStreamPresenter;
export interface ChatPanelShellBundle {
parts: ChatPanelShellParts;
closeToolbarPanelOnOutsidePointer(event: PointerEvent): void;
dispose(): void;
}
export function createChatPanelSurfaces(host: ChatPanelSurfacesHost, input: ChatPanelSurfacesInput): ChatPanelSurfaces {
export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPanelShellBundleInput): ChatPanelShellBundle {
const {
connection,
connectionController,
@ -60,7 +54,8 @@ export function createChatPanelSurfaces(host: ChatPanelSurfacesHost, input: Chat
reconnect,
history,
pendingRequests,
turnActions,
turn,
composer,
} = input;
const { environment, stateStore } = host;
const toolbarActions = createToolbarUiActions({
@ -110,7 +105,7 @@ export function createChatPanelSurfaces(host: ChatPanelSurfacesHost, input: Chat
actions: {
rollbackThread: (threadId) => void threadActions.rollbackThread(threadId),
forkThreadFromTurn: (threadId, turnId, archiveSource) => void threadActions.forkThreadFromTurn(threadId, turnId, archiveSource),
implementPlan: (itemId) => void turnActions.planImplementation.implement(itemId),
implementPlan: (itemId) => void turn.turnActions.planImplementation.implement(itemId),
openTurnDiff: (state) => void environment.plugin.workspace.openTurnDiff(state),
},
requests: {
@ -118,11 +113,33 @@ export function createChatPanelSurfaces(host: ChatPanelSurfacesHost, input: Chat
consumePendingAutoFocus: () => pendingRequests.consumeAutoFocus(),
},
});
const parts: ChatPanelShellParts = {
toolbar: {
surface: toolbarSurface,
actions: toolbarActions,
},
goal: goalSurface,
messageStream: messageStreamPresenter,
composer: {
presenter: composer.controller,
actions: {
submit: () => void turn.turnActions.composerSubmit.submit(),
},
},
};
return {
toolbarActions,
toolbarSurface,
goalSurface,
messageStreamPresenter,
parts,
closeToolbarPanelOnOutsidePointer: (event) => {
toolbarPanelActions.closeOnOutsidePointer({
target: event.target,
viewWindow: environment.view.viewWindow() as (Window & { Element: typeof Element }) | null,
contains: (element) => environment.view.containsElement(element),
renameEditing: rename.isEditing(),
});
},
dispose: () => {
messageStreamPresenter.dispose();
},
};
}

View file

@ -1,40 +0,0 @@
import type { AppServerClient } from "../../../app-server/connection/client";
import type { AppServerQueryContext } from "../../../app-server/query/keys";
import type { ChatPanelSnapshot } from "../panel/snapshot";
export interface ChatViewLifecycleSurface {
displayTitle(): string;
persistedState(): Record<string, unknown>;
applyViewState(state: unknown): void;
open(): void;
close(): void;
refreshSettings(): void;
}
export interface ChatWorkspacePanelSurface {
openPanelSnapshot(): ChatPanelSnapshot;
openThread(threadId: string): Promise<void>;
focusThread(threadId?: string | null): Promise<void>;
hydrateRestoredThread(): Promise<void>;
focusComposer(): void;
connect(): Promise<void>;
startNewThread(): Promise<void>;
}
export interface ChatSharedThreadSurface {
refreshSharedThreads(): Promise<void>;
applyThreadArchived(threadId: string): void;
applyThreadRenamed(threadId: string, name: string | null): void;
}
export interface ChatPanelClientSurface {
canServeAppServerContext(context: AppServerQueryContext): boolean;
runWithAppServerClient<T>(operation: (client: AppServerClient) => Promise<T>): Promise<T>;
}
export type ChatSurfaceHandle = ChatViewLifecycleSurface &
ChatWorkspacePanelSurface &
ChatSharedThreadSurface &
ChatPanelClientSurface & {
setComposerText(text: string): void;
};

View file

@ -0,0 +1,442 @@
import { Notice } from "obsidian";
import type { AppServerClientAccess } from "../../../app-server/connection/client-access";
import { normalizeExplicitThreadName } from "../../../domain/threads/model";
import type { LocalIdSource } from "../../../shared/id/local-id";
import { createThreadOperations, type ThreadOperations } from "../../threads/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../../threads/thread-title-service";
import type { ChatServerThreadActions } from "../app-server/actions/threads";
import type { ChatResumeWorkTracker } from "../application/lifecycle";
import { messageStreamItems } from "../application/state/message-stream";
import type { ChatStateStore } from "../application/state/store";
import type { ActiveThreadIdentitySync } from "../application/threads/active-thread-identity-sync";
import { type AutoTitleCoordinator, createAutoTitleCoordinator } from "../application/threads/auto-title-coordinator";
import { createGoalActions, createThreadGoalSyncActions } from "../application/threads/goal-actions";
import { HistoryController } from "../application/threads/history-controller";
import { createThreadLifecycleParts } from "../application/threads/lifecycle-parts";
import {
activeThreadRenameTitleContext,
createThreadRenameEditorActions,
type ThreadRenameEditorActions,
} from "../application/threads/rename-editor-actions";
import type { RestorationController } from "../application/threads/restoration-controller";
import type { ResumeActions } from "../application/threads/resume-actions";
import { createThreadManagementActions, type ThreadManagementActionsHost } from "../application/threads/thread-management-actions";
import { createThreadNavigationActions } from "../application/threads/thread-navigation-actions";
import { threadTitleContextFromMessageStreamItems } from "../application/threads/title-context";
import type { ChatComposerController } from "../panel/composer-controller";
import { createToolbarPanelActions, type ToolbarPanelActions } from "../panel/toolbar-actions";
import type { CurrentAppServerClient } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./contracts";
type ChatPanelGoalSyncActions = ReturnType<typeof createThreadGoalSyncActions>;
export type ChatPanelGoalActions = ReturnType<typeof createGoalActions>;
export type ChatPanelThreadLifecycle = ReturnType<typeof createThreadLifecycleParts>;
export type ChatPanelThreadActions = ReturnType<typeof createThreadManagementActions>;
export type ChatPanelThreadNavigationActions = ReturnType<typeof createThreadNavigationActions>;
interface ChatPanelThreadStatus {
set: (statusText: string) => void;
addSystemMessage: (text: string) => void;
}
interface ChatPanelThreadHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
resumeWork: ChatResumeWorkTracker;
messageScrollController: {
showLatest(): void;
};
getClosing: () => boolean;
}
interface ChatPanelThreadFoundationInput {
currentClient: CurrentAppServerClient;
localItemIds: LocalIdSource;
status: ChatPanelThreadStatus;
refreshLiveState: () => void;
}
interface ChatPanelThreadFoundation {
titleService: ThreadTitleService;
autoTitleCoordinator: AutoTitleCoordinator;
history: HistoryController;
goalSync: ChatPanelGoalSyncActions;
threadOperations: ThreadOperations;
invalidateThreadWork(): void;
}
interface ChatPanelThreadLifecycleInput {
currentClient: CurrentAppServerClient;
localItemIds: LocalIdSource;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
ensureConnected: () => Promise<void>;
status: ChatPanelThreadStatus;
serverThreads: ChatServerThreadActions;
foundation: ChatPanelThreadFoundation;
refreshTabHeader: () => void;
refreshLiveState: () => void;
notifyActiveThreadIdentityChanged: () => void;
}
interface ChatPanelThreadLifecycleBundle {
goals: ChatPanelGoalActions;
rename: ThreadRenameEditorActions;
lifecycle: ChatPanelThreadLifecycle;
identity: ActiveThreadIdentitySync;
restoration: RestorationController;
resume: ResumeActions;
}
interface ChatPanelThreadActionInput {
currentClient: CurrentAppServerClient;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
status: ChatPanelThreadStatus;
composerController: ChatComposerController;
foundation: ChatPanelThreadFoundation;
lifecycle: ChatPanelThreadLifecycleBundle;
refreshActiveThreads: () => Promise<void>;
notifyActiveThreadIdentityChanged: () => void;
}
interface ChatPanelThreadActionBundle {
actions: ChatPanelThreadActions;
toolbarPanelActions: ToolbarPanelActions;
navigation: ChatPanelThreadNavigationActions;
}
export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPanelThreadFoundationInput): ChatPanelThreadFoundation {
const { currentClient, localItemIds, status, refreshLiveState } = input;
const titleService = createSessionThreadTitleService(host, currentClient);
const autoTitleCoordinator = createSessionAutoTitleCoordinator(host, currentClient, titleService);
const history = createSessionHistoryController(host, currentClient, status, autoTitleCoordinator);
const invalidateThreadWork = () => {
host.resumeWork.invalidate();
history.invalidate();
};
const goalSync = createSessionGoalSyncActions(host, currentClient, localItemIds, status, refreshLiveState);
const threadOperations = createSessionThreadOperations(host.environment, currentClient);
return {
titleService,
autoTitleCoordinator,
history,
goalSync,
threadOperations,
invalidateThreadWork,
};
}
export function createThreadLifecycleBundle(
host: ChatPanelThreadHost,
input: ChatPanelThreadLifecycleInput,
): ChatPanelThreadLifecycleBundle {
const {
currentClient,
localItemIds,
connectedClient,
ensureConnected,
status,
serverThreads,
foundation,
refreshTabHeader,
refreshLiveState,
notifyActiveThreadIdentityChanged,
} = input;
const goals = createSessionGoalActions(host, currentClient, localItemIds, connectedClient, status, serverThreads, refreshLiveState);
const rename = createSessionThreadRenameEditorActions(
host.stateStore,
foundation.threadOperations,
foundation.titleService,
ensureConnected,
status,
);
const lifecycle = createSessionThreadLifecycle(host, {
currentClient,
ensureConnected,
status,
goals,
autoTitleCoordinator: foundation.autoTitleCoordinator,
history: foundation.history,
invalidateThreadWork: () => {
foundation.invalidateThreadWork();
},
refreshTabHeader,
refreshLiveState,
notifyActiveThreadIdentityChanged,
});
const { identity, restoration, resume } = lifecycle;
return {
goals,
rename,
lifecycle,
identity,
restoration,
resume,
};
}
export function createThreadActionBundle(host: ChatPanelThreadHost, input: ChatPanelThreadActionInput): ChatPanelThreadActionBundle {
const {
currentClient,
connectedClient,
status,
composerController,
foundation,
lifecycle,
refreshActiveThreads,
notifyActiveThreadIdentityChanged,
} = input;
const { environment, stateStore } = host;
const threadManagementHost: ThreadManagementActionsHost = {
stateStore,
vaultPath: environment.plugin.settingsRef.vaultPath,
operations: foundation.threadOperations,
connectedClient,
currentClient,
addSystemMessage: status.addSystemMessage,
setStatus: status.set,
setComposerText: (text) => {
composerController.setDraft(text, { focus: true });
},
openThreadInNewView: (threadId) => environment.plugin.workspace.openThreadInNewView(threadId),
openThreadInCurrentPanel: (threadId) => lifecycle.resume.resumeThread(threadId),
notifyActiveThreadIdentityChanged,
refreshAfterThreadMutation: async () => {
await refreshActiveThreads();
},
applyThreadCatalogEvent: (event) => {
environment.plugin.threadCatalog.apply(event);
},
};
const actions = createThreadManagementActions(threadManagementHost);
const toolbarPanelActions = createToolbarPanelActions({
stateStore,
threadActions: actions,
});
const navigation = createThreadNavigationActions({
stateStore,
identity: lifecycle.identity,
closeForThreadSelection: () => {
toolbarPanelActions.closeForThreadSelection();
},
focusThreadInOpenView: (threadId) => environment.plugin.workspace.focusThreadInOpenView(threadId),
resumeThread: (threadId) => lifecycle.resume.resumeThread(threadId),
addSystemMessage: status.addSystemMessage,
focusComposer: () => {
composerController.focus();
},
});
return { actions, toolbarPanelActions, navigation };
}
function createSessionThreadTitleService(host: ChatPanelThreadHost, currentClient: CurrentAppServerClient): ThreadTitleService {
const { environment, stateStore } = host;
return createThreadTitleService({
codexPath: () => environment.plugin.settingsRef.settings.codexPath,
vaultPath: environment.plugin.settingsRef.vaultPath,
threadNamingModel: () => environment.plugin.settingsRef.settings.threadNamingModel,
threadNamingEffort: () => environment.plugin.settingsRef.settings.threadNamingEffort,
clientAccess: createCurrentClientAccess(currentClient),
visibleContext: (threadId) => activeThreadRenameTitleContext(stateStore.getState(), threadId),
visibleCompletedTurnContext: (turnId) =>
threadTitleContextFromMessageStreamItems(turnId, messageStreamItems(stateStore.getState().messageStream)),
});
}
function createSessionAutoTitleCoordinator(
host: ChatPanelThreadHost,
currentClient: CurrentAppServerClient,
titleService: ThreadTitleService,
): AutoTitleCoordinator {
return createAutoTitleCoordinator({
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.apply({ type: "thread-renamed", threadId, name });
}
return true;
},
});
}
function createSessionHistoryController(
host: ChatPanelThreadHost,
currentClient: CurrentAppServerClient,
status: ChatPanelThreadStatus,
autoTitleCoordinator: AutoTitleCoordinator,
): HistoryController {
return new HistoryController({
stateStore: host.stateStore,
currentClient,
addSystemMessage: status.addSystemMessage,
showLatestPageAtBottom: () => {
host.messageScrollController.showLatest();
},
setThreadTurnPresence: (hadTurns) => {
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
});
}
function createSessionGoalSyncActions(
host: ChatPanelThreadHost,
currentClient: CurrentAppServerClient,
localItemIds: LocalIdSource,
status: ChatPanelThreadStatus,
refreshLiveState: () => void,
): ChatPanelGoalSyncActions {
return createThreadGoalSyncActions({
stateStore: host.stateStore,
currentClient,
localItemIds,
addSystemMessage: (text) => {
status.addSystemMessage(text);
},
addGoalEvent: (item) => {
host.stateStore.dispatch({ type: "message-stream/item-upserted", item });
},
refreshLiveState,
});
}
function createSessionThreadOperations(environment: ChatPanelEnvironment, currentClient: CurrentAppServerClient): ThreadOperations {
return createThreadOperations({
clientAccess: createCurrentClientAccess(currentClient),
archiveExport: {
settings: () => ({
archiveExportFolderTemplate: environment.plugin.settingsRef.settings.archiveExportFolderTemplate,
archiveExportFilenameTemplate: environment.plugin.settingsRef.settings.archiveExportFilenameTemplate,
archiveExportTags: environment.plugin.settingsRef.settings.archiveExportTags,
}),
enabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled,
vaultPath: environment.plugin.settingsRef.vaultPath,
vaultConfigDir: environment.obsidian.app.vault.configDir,
},
archiveDestination: environment.obsidian.archiveDestination,
catalog: environment.plugin.threadCatalog,
notice: (text) => {
new Notice(text);
},
});
}
function createCurrentClientAccess(currentClient: CurrentAppServerClient): AppServerClientAccess {
return {
withClient: async (operation) => {
const client = currentClient();
if (!client) throw new Error("Codex app-server is not connected.");
const result = await operation(client);
if (currentClient() !== client) {
throw new Error("Codex app-server connection changed while running the operation.");
}
return result;
},
};
}
function createSessionGoalActions(
host: ChatPanelThreadHost,
currentClient: CurrentAppServerClient,
localItemIds: LocalIdSource,
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>,
status: ChatPanelThreadStatus,
serverThreads: ChatServerThreadActions,
refreshLiveState: () => void,
): ChatPanelGoalActions {
return createGoalActions({
stateStore: host.stateStore,
currentClient,
localItemIds,
connectedClient,
startThread: (preview, options) => serverThreads.startThread(preview, options),
addSystemMessage: (text) => {
status.addSystemMessage(text);
},
addGoalEvent: (item) => {
host.stateStore.dispatch({ type: "message-stream/item-upserted", item });
},
refreshLiveState,
});
}
function createSessionThreadRenameEditorActions(
stateStore: ChatStateStore,
operations: ThreadOperations,
titleService: ThreadTitleService,
ensureConnected: () => Promise<void>,
status: ChatPanelThreadStatus,
): ThreadRenameEditorActions {
return createThreadRenameEditorActions({
stateStore,
ensureConnected,
addSystemMessage: status.addSystemMessage,
renameThread: (threadId, value, options) => operations.renameThread(threadId, value, options),
generateThreadTitle: (threadId) => titleService.generateTitle(threadId),
});
}
function createSessionThreadLifecycle(
host: ChatPanelThreadHost,
input: {
currentClient: CurrentAppServerClient;
ensureConnected: () => Promise<void>;
status: ChatPanelThreadStatus;
goals: ChatPanelGoalActions;
autoTitleCoordinator: AutoTitleCoordinator;
history: HistoryController;
invalidateThreadWork: () => void;
refreshTabHeader: () => void;
refreshLiveState: () => void;
notifyActiveThreadIdentityChanged: () => void;
},
): ChatPanelThreadLifecycle {
const {
currentClient,
ensureConnected,
status,
goals,
autoTitleCoordinator,
history,
invalidateThreadWork,
refreshTabHeader,
refreshLiveState,
notifyActiveThreadIdentityChanged,
} = input;
return createThreadLifecycleParts({
settingsRef: host.environment.plugin.settingsRef,
stateStore: host.stateStore,
client: {
currentClient,
ensureConnected,
},
lifecycle: {
resumeWork: host.resumeWork,
history,
invalidateThreadWork,
getClosing: host.getClosing,
},
thread: {
notifyIdentityChanged: notifyActiveThreadIdentityChanged,
refreshTabHeader,
},
status,
liveState: {
refresh: refreshLiveState,
},
goals,
resetThreadTurnPresence: (hadTurns) => {
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
});
}

View file

@ -0,0 +1,181 @@
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
import type { LocalIdSource } from "../../../shared/id/local-id";
import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work";
import type { ChatServerThreadActions } from "../app-server/actions/threads";
import type { ChatInboundHandler } from "../app-server/inbound/handler";
import { type ChatReconnectActionsHost, reconnectPanel } from "../application/connection/reconnect-actions";
import {
type ConversationTurnActions as ChatPanelConversationTurnActions,
createConversationTurnActions,
} from "../application/conversation/composition";
import type { ChatViewDeferredTasks } from "../application/lifecycle";
import { createPendingRequestActions, type PendingRequestActions } from "../application/pending-requests/pending-request-actions";
import type { ChatConnectionPhase } from "../application/state/root-reducer";
import type { ChatStateStore } from "../application/state/store";
import type { AutoTitleCoordinator } from "../application/threads/auto-title-coordinator";
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
import type { ChatComposerController } from "../panel/composer-controller";
import type { CurrentAppServerClient } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./contracts";
import type { ChatPanelRuntimeProjection, ChatPanelRuntimeSettingsActions } from "./runtime-bundle";
import type {
ChatPanelGoalActions,
ChatPanelThreadActions,
ChatPanelThreadLifecycle,
ChatPanelThreadNavigationActions,
} from "./thread-bundle";
interface ChatPanelTurnStatus {
set: (statusText: string, phase?: ChatConnectionPhase) => void;
addSystemMessage: (text: string) => void;
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
}
interface ChatPanelTurnHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
deferredTasks: ChatViewDeferredTasks;
connectionWork: ConnectionWorkTracker;
messageScrollController: {
showLatest(): void;
};
}
export interface ChatPanelTurnBundle {
pendingRequests: PendingRequestActions;
reconnect: () => Promise<void>;
turnActions: ChatPanelConversationTurnActions;
}
interface ChatPanelTurnInput {
connection: ConnectionManager;
localItemIds: LocalIdSource;
ensureConnected: () => Promise<void>;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
currentClient: CurrentAppServerClient;
status: ChatPanelTurnStatus;
inboundHandler: ChatInboundHandler;
threadLifecycle: ChatPanelThreadLifecycle;
threadActions: ChatPanelThreadActions;
navigation: ChatPanelThreadNavigationActions;
composerController: ChatComposerController;
runtimeSettings: ChatPanelRuntimeSettingsActions;
serverThreads: ChatServerThreadActions;
goals: ChatPanelGoalActions;
autoTitleCoordinator: AutoTitleCoordinator;
invalidateThreadWork: () => void;
runtimeProjection: ChatPanelRuntimeProjection;
refreshLiveState: () => void;
notifyActiveThreadIdentityChanged: () => void;
}
export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnInput): ChatPanelTurnBundle {
const {
connection,
localItemIds,
ensureConnected,
connectedClient,
currentClient,
status,
inboundHandler,
threadLifecycle,
threadActions,
navigation,
composerController,
runtimeSettings,
serverThreads,
goals,
autoTitleCoordinator,
invalidateThreadWork,
runtimeProjection,
refreshLiveState,
notifyActiveThreadIdentityChanged,
} = input;
const pendingRequests = createPendingRequestActions({
stateStore: host.stateStore,
responder: inboundHandler,
composerHasFocus: () => composerController.hasFocus(),
focusComposer: () => {
composerController.focus();
},
refreshLiveState,
});
const reconnectHost: ChatReconnectActionsHost = {
stateStore: host.stateStore,
invalidateConnectionWork: () => {
host.connectionWork.invalidate();
},
invalidateThreadWork: () => {
invalidateThreadWork();
},
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,
localItemIds,
client: {
currentClient,
connectedClient,
},
status,
runtime: {
connectionDiagnosticDetails: runtimeProjection.connectionDiagnosticDetails,
modelStatusLines: runtimeProjection.modelStatusLines,
effortStatusLines: runtimeProjection.effortStatusLines,
statusSummaryLines: runtimeProjection.statusSummaryLines,
toolInventoryDetails: runtimeProjection.toolInventoryDetails,
},
thread: {
ensureRestoredThreadLoaded: () =>
threadLifecycle.restoration.ensureLoaded((threadId) => threadLifecycle.resume.resumeThread(threadId)),
startNewThread: () => navigation.startNewThread(),
selectThread: (threadId) => navigation.selectThread(threadId),
notifyIdentityChanged: notifyActiveThreadIdentityChanged,
resetTurnPresence: (hadTurns) => {
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
},
composer: {
codexInput: (text) => composerController.codexInput(text),
trimmedDraft: () => composerController.trimmedDraft,
setDraft: (text, options) => {
composerController.setDraft(text, options);
},
},
scroll: {
showLatest: () => {
host.messageScrollController.showLatest();
},
},
},
{
threadStarter: serverThreads,
runtimeSettings,
threadActions,
reconnectPanel: reconnect,
goals,
},
);
return {
pendingRequests,
reconnect,
turnActions,
};
}

View file

@ -3,12 +3,11 @@ import { ItemView, type ViewStateResult, type WorkspaceLeaf } from "obsidian";
import { VIEW_TYPE_CODEX_PANEL } from "../../../constants";
import { createLocalIdSource } from "../../../shared/id/local-id";
import { createObsidianArchiveExportDestination } from "../../../shared/obsidian/archive-export-destination";
import type { CodexChatHost } from "./environment";
import type { ChatPanelHandle, CodexChatHost } from "./contracts";
import { ChatPanelSession } from "./session";
import type { ChatSurfaceHandle } from "./surface-handle";
export class CodexChatView extends ItemView {
readonly surface: ChatSurfaceHandle;
readonly surface: ChatPanelHandle;
private readonly viewId = createLocalIdSource().next("codex-panel");
constructor(leaf: WorkspaceLeaf, plugin: CodexChatHost) {

View file

@ -8,13 +8,14 @@ import { AppServerSharedQueries } from "./app-server/query/shared-queries";
import { VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants";
import type { ChatTurnDiffViewState } from "./features/chat/domain/turn-diff";
import { persistedChatTurnDiffViewState } from "./features/chat/domain/turn-diff";
import type { CodexChatHost, PluginSettingsRef } from "./features/chat/host/environment";
import type {
ChatPanelClientSurface,
ChatSharedThreadSurface,
ChatViewLifecycleSurface,
ChatWorkspacePanelSurface,
} from "./features/chat/host/surface-handle";
CodexChatHost,
PluginSettingsRef,
} from "./features/chat/host/contracts";
import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view.obsidian";
import { openThreadPicker, type ThreadPickerHost } from "./features/thread-picker/modal.obsidian";
import type { ThreadsViewHost, ThreadsViewSettingsAccess } from "./features/threads-view/session";

View file

@ -2,7 +2,7 @@ import type { App, WorkspaceLeaf } from "obsidian";
import { VIEW_TYPE_CODEX_PANEL } from "../constants";
import { hasPendingRequests, pendingRequestCounts } from "../domain/pending-requests/aggregate";
import type { ChatWorkspacePanelSurface } from "../features/chat/host/surface-handle";
import type { ChatWorkspacePanelSurface } from "../features/chat/host/contracts";
import { CodexChatView } from "../features/chat/host/view";
import type { ChatPanelSnapshot } from "../features/chat/panel/snapshot";

View file

@ -2,7 +2,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ChatResumeWorkTracker, transitionRestoredThreadLifecycle } from "../../../../src/features/chat/application/lifecycle";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-tasks";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-work";
import { ConnectionWorkTracker } from "../../../../src/shared/lifecycle/connection-work";
describe("createChatViewDeferredTasks", () => {

View file

@ -7,8 +7,9 @@ import type { ModelMetadata } from "../../../../src/domain/catalog/metadata";
import type { Thread } from "../../../../src/domain/threads/model";
import { ChatResumeWorkTracker } from "../../../../src/features/chat/application/lifecycle";
import { type ChatStateStore, createChatStateStore } from "../../../../src/features/chat/application/state/store";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-tasks";
import type { ChatPanelEnvironment } from "../../../../src/features/chat/host/environment";
import { HistoryController } from "../../../../src/features/chat/application/threads/history-controller";
import type { ChatPanelEnvironment } from "../../../../src/features/chat/host/contracts";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-work";
import { createChatPanelSessionGraph } from "../../../../src/features/chat/host/session-graph";
import { ChatComposerController } from "../../../../src/features/chat/panel/composer-controller";
import { MessageStreamPresenter } from "../../../../src/features/chat/panel/surface/message-stream-presenter";
@ -34,7 +35,7 @@ describe("createChatPanelSessionGraph actions", () => {
it("invalidates thread work through the graph action", () => {
const { graph, resumeWork } = sessionGraphFixture();
const resume = resumeWork.begin("thread-1");
const invalidateHistory = vi.spyOn(graph.thread.history, "invalidate");
const invalidateHistory = vi.spyOn(HistoryController.prototype, "invalidate");
graph.actions.invalidateThreadWork();

View file

@ -10,7 +10,7 @@ import { emptyRuntimeConfigSnapshot } from "../../../../src/domain/runtime/confi
import { createServerDiagnostics } from "../../../../src/domain/server/diagnostics";
import type { SharedServerMetadata } from "../../../../src/domain/server/metadata";
import type { Thread } from "../../../../src/domain/threads/model";
import type { CodexChatHost } from "../../../../src/features/chat/host/environment";
import type { CodexChatHost } from "../../../../src/features/chat/host/contracts";
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
import type { ThreadCatalogEvent } from "../../../../src/workspace/thread-catalog";
import { notices } from "../../../mocks/obsidian";

View file

@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_THREADS } from "../src/constants";
import type { Thread } from "../src/domain/threads/model";
import type { CodexChatHost } from "../src/features/chat/host/environment";
import type { CodexChatHost } from "../src/features/chat/host/contracts";
import type { CodexChatView } from "../src/features/chat/host/view";
import type CodexPanelPlugin from "../src/main";
import { DEFAULT_SETTINGS } from "../src/settings/model";