mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Reorganize chat host session graph
This commit is contained in:
parent
b47a43e763
commit
318ffb4532
18 changed files with 1059 additions and 854 deletions
90
src/features/chat/host/composer-bundle.ts
Normal file
90
src/features/chat/host/composer-bundle.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
136
src/features/chat/host/runtime-bundle.ts
Normal file
136
src/features/chat/host/runtime-bundle.ts
Normal 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());
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
442
src/features/chat/host/thread-bundle.ts
Normal file
442
src/features/chat/host/thread-bundle.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
181
src/features/chat/host/turn-bundle.ts
Normal file
181
src/features/chat/host/turn-bundle.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue