Organize chat host wiring modules

This commit is contained in:
murashit 2026-06-30 09:14:34 +09:00
parent 2cb3844011
commit 2b543f5904
25 changed files with 273 additions and 227 deletions

View file

@ -63,10 +63,6 @@
},
// App-server and generated protocol boundaries.
{
"path": "./scripts/grit/import-boundaries/no-app-server-root-module-imports.grit",
"includes": ["**/src/**/*.ts", "**/src/**/*.tsx"]
},
{
"path": "./scripts/grit/import-boundaries/no-app-server-subfolder-root-imports.grit",
"includes": ["**/src/app-server/**/*.ts"]

View file

@ -1,16 +0,0 @@
language js
private pattern js_module_reference() {
or {
JsImport(),
JsImportCallExpression()
}
}
js_module_reference() as $stmt where {
$stmt <: contains `$source` where {
$source <: r"^[\"'](?:(?:\./)?app-server|(?:\.\./)+app-server|src/app-server)/[^/\"']+[\"']$",
not { $filename <: r".*/src/features/chat/[^/]+/.*", $source <: r"^[\"']\.\./app-server/[^/\"']+[\"']$" }
},
register_diagnostic(span=$stmt, message="Import app-server boundary modules from responsibility subfolders, not src/app-server root modules.", severity="error")
}

View file

@ -1,17 +1,17 @@
import { Notice } from "obsidian";
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 { createVaultComposerAttachmentHandler } from "./composer-attachments.obsidian";
import type { ChatPanelEnvironment } from "./contracts";
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 type { ChatPanelEnvironment } from "../contracts";
import { createVaultComposerAttachmentHandler } from "../obsidian/composer-attachments.obsidian";
import { VaultComposerContextReferenceProvider } from "../obsidian/vault-composer-context-reference-provider.obsidian";
import { VaultNoteCandidateProvider } from "../obsidian/vault-note-candidate-provider.obsidian";
import type { ChatPanelRuntimeSettingsActions } from "./runtime-bundle";
import { VaultComposerContextReferenceProvider } from "./vault-composer-context-reference-provider.obsidian";
import { VaultNoteCandidateProvider } from "./vault-note-candidate-provider.obsidian";
interface ChatPanelComposerHost {
environment: ChatPanelEnvironment;

View file

@ -1,26 +1,26 @@
import { Notice } from "obsidian";
import type { AppServerClient } from "../../../app-server/connection/client";
import { type ConnectionManager, StaleConnectionError } from "../../../app-server/connection/connection-manager";
import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries";
import type { LocalIdSource } from "../../../shared/id/local-id";
import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work";
import { type ChatServerDiagnosticsActions, createChatServerDiagnosticsActions } from "../app-server/actions/diagnostics";
import { type ChatServerMetadataActions, createChatServerMetadataActions } from "../app-server/actions/metadata";
import { type ChatServerThreadActions, createChatServerThreadActions } from "../app-server/actions/threads";
import { type ChatInboundHandler, createChatInboundHandler } from "../app-server/inbound/handler";
import type { AppServerClient } from "../../../../app-server/connection/client";
import { type ConnectionManager, StaleConnectionError } from "../../../../app-server/connection/connection-manager";
import { isStaleAppServerSharedQueryContextError } from "../../../../app-server/query/shared-queries";
import type { LocalIdSource } from "../../../../shared/id/local-id";
import type { ConnectionWorkTracker } from "../../../../shared/lifecycle/connection-work";
import { type ChatServerDiagnosticsActions, createChatServerDiagnosticsActions } from "../../app-server/actions/diagnostics";
import { type ChatServerMetadataActions, createChatServerMetadataActions } from "../../app-server/actions/metadata";
import { type ChatServerThreadActions, createChatServerThreadActions } from "../../app-server/actions/threads";
import { type ChatInboundHandler, createChatInboundHandler } from "../../app-server/inbound/handler";
import {
type ChatConnectionController,
createChatConnectionController,
handleChatConnectionExit,
} from "../application/connection/connection-controller";
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
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 "./contracts";
import type { ChatViewDeferredTasks } from "./deferred-work";
} from "../../application/connection/connection-controller";
import { runtimeSnapshotForChatState } from "../../application/runtime/snapshot";
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 "../contracts";
import type { ChatViewDeferredTasks } from "../session/deferred-work";
type CurrentAppServerClient = () => AppServerClient | null;

View file

@ -0,0 +1,53 @@
import type { ConnectionManager } from "../../../../app-server/connection/connection-manager";
import type { ConnectionWorkTracker } from "../../../../shared/lifecycle/connection-work";
import { type ChatReconnectActionsHost, reconnectPanel } from "../../application/connection/reconnect-actions";
import type { ChatConnectionPhase } from "../../application/state/root-reducer";
import type { ChatStateStore } from "../../application/state/store";
import type { ChatViewDeferredTasks } from "../session/deferred-work";
interface ChatPanelReconnectStatus {
set: (statusText: string, phase?: ChatConnectionPhase) => void;
addSystemMessage: (text: string) => void;
}
interface ChatPanelReconnectHost {
stateStore: ChatStateStore;
connectionWork: ConnectionWorkTracker;
deferredTasks: ChatViewDeferredTasks;
}
interface ChatPanelReconnectInput {
connection: ConnectionManager;
ensureConnected: () => Promise<void>;
invalidateThreadWork: () => void;
resumeThread: (threadId: string) => Promise<void>;
status: ChatPanelReconnectStatus;
}
export function createReconnectAction(host: ChatPanelReconnectHost, input: ChatPanelReconnectInput): () => Promise<void> {
const reconnectHost: ChatReconnectActionsHost = {
stateStore: host.stateStore,
invalidateConnectionWork: () => {
host.connectionWork.invalidate();
},
invalidateThreadWork: () => {
input.invalidateThreadWork();
},
clearDeferredDiagnostics: () => {
host.deferredTasks.clearDiagnostics();
},
resetConnection: () => {
input.connection.resetConnection();
},
setStatus: (statusText, phase) => {
input.status.set(statusText, phase);
},
ensureConnected: input.ensureConnected,
resumeThread: input.resumeThread,
addSystemMessage: (text) => {
input.status.addSystemMessage(text);
},
};
return () => reconnectPanel(reconnectHost);
}

View file

@ -1,11 +1,11 @@
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
import type { ChatAppServerGateway } from "../app-server/session-gateway";
import { type ChatRuntimeSettingsActions, createChatRuntimeSettingsActions } from "../application/runtime/settings-actions";
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
import type { ChatStateStore } from "../application/state/store";
import { collaborationModeLabel as formatCollaborationModeLabel } from "../domain/runtime/labels";
import { type ChatPanelRuntimeProjection, createChatPanelRuntimeProjection } from "../panel/runtime-status-projection";
import type { ChatPanelEnvironment } from "./contracts";
import type { ConnectionManager } from "../../../../app-server/connection/connection-manager";
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
import { type ChatRuntimeSettingsActions, createChatRuntimeSettingsActions } from "../../application/runtime/settings-actions";
import { runtimeSnapshotForChatState } from "../../application/runtime/snapshot";
import type { ChatStateStore } from "../../application/state/store";
import { collaborationModeLabel as formatCollaborationModeLabel } from "../../domain/runtime/labels";
import { type ChatPanelRuntimeProjection, createChatPanelRuntimeProjection } from "../../panel/runtime-status-projection";
import type { ChatPanelEnvironment } from "../contracts";
export type ChatPanelRuntimeSettingsActions = ChatRuntimeSettingsActions;

View file

@ -1,18 +1,18 @@
import type { ConnectionManager } from "../../../app-server/connection/connection-manager";
import type { PendingRequestActions } from "../application/pending-requests/pending-request-actions";
import type { ChatStateStore } from "../application/state/store";
import type { HistoryController } from "../application/threads/history-controller";
import type { ThreadRenameEditorActions } from "../application/threads/rename-editor-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 { toolbarOutsidePointerHit } from "../panel/toolbar-hit-test.dom";
import type { ConnectionManager } from "../../../../app-server/connection/connection-manager";
import type { PendingRequestActions } from "../../application/pending-requests/pending-request-actions";
import type { ChatStateStore } from "../../application/state/store";
import type { HistoryController } from "../../application/threads/history-controller";
import type { ThreadRenameEditorActions } from "../../application/threads/rename-editor-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 { toolbarOutsidePointerHit } from "../../panel/toolbar-hit-test.dom";
import type { ChatPanelEnvironment } from "../contracts";
import type { ChatPanelComposerBundle } from "./composer-bundle";
import type { ChatPanelConnectionBundle } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./contracts";
import type { ChatPanelGoalActions, ChatPanelThreadActions, ChatPanelThreadNavigationActions } from "./thread-bundle";
import type { ChatPanelTurnBundle } from "./turn-bundle";

View file

@ -1,33 +1,33 @@
import { Notice } from "obsidian";
import { recoverRolloutTokenUsage } from "../../../app-server/services/rollout-token-usage";
import { normalizeExplicitThreadName } from "../../../domain/threads/model";
import type { LocalIdSource } from "../../../shared/id/local-id";
import { createThreadOperations, type ThreadOperations } from "../../threads/workflows/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../../threads/workflows/thread-title-service";
import type { ChatServerThreadActions } from "../app-server/actions/threads";
import type { ChatAppServerGateway } from "../app-server/session-gateway";
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 { recoverRolloutTokenUsage } from "../../../../app-server/services/rollout-token-usage";
import { normalizeExplicitThreadName } from "../../../../domain/threads/model";
import type { LocalIdSource } from "../../../../shared/id/local-id";
import { createThreadOperations, type ThreadOperations } from "../../../threads/workflows/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../../../threads/workflows/thread-title-service";
import type { ChatServerThreadActions } from "../../app-server/actions/threads";
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
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 type { ChatResumeWorkTracker } from "../application/threads/resume-work";
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 { ChatPanelEnvironment } from "./contracts";
} from "../../application/threads/rename-editor-actions";
import type { RestorationController } from "../../application/threads/restoration-controller";
import type { ResumeActions } from "../../application/threads/resume-actions";
import type { ChatResumeWorkTracker } from "../../application/threads/resume-work";
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 { ChatPanelEnvironment } from "../contracts";
type ChatPanelGoalSyncActions = ReturnType<typeof createThreadGoalSyncActions>;
export type ChatPanelGoalActions = ReturnType<typeof createGoalActions>;

View file

@ -1,23 +1,17 @@
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 { ChatAppServerGateway } from "../app-server/session-gateway";
import { type ChatReconnectActionsHost, reconnectPanel } from "../application/connection/reconnect-actions";
import type { LocalIdSource } from "../../../../shared/id/local-id";
import type { ChatServerThreadActions } from "../../app-server/actions/threads";
import type { ChatInboundHandler } from "../../app-server/inbound/handler";
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
import {
type ConversationTurnActions as ChatPanelConversationTurnActions,
createConversationTurnActions,
} from "../application/conversation/composition";
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 { ChatPanelRuntimeProjection } from "../panel/runtime-status-projection";
import type { ChatPanelEnvironment } from "./contracts";
import type { ChatViewDeferredTasks } from "./deferred-work";
} from "../../application/conversation/composition";
import { createPendingRequestActions, type PendingRequestActions } from "../../application/pending-requests/pending-request-actions";
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 { ChatPanelRuntimeProjection } from "../../panel/runtime-status-projection";
import type { ChatPanelRuntimeSettingsActions } from "./runtime-bundle";
import type {
ChatPanelGoalActions,
@ -27,16 +21,13 @@ import type {
} from "./thread-bundle";
interface ChatPanelTurnStatus {
set: (statusText: string, phase?: ChatConnectionPhase) => void;
set: (statusText: string) => void;
addSystemMessage: (text: string) => void;
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
}
interface ChatPanelTurnHost {
environment: ChatPanelEnvironment;
stateStore: ChatStateStore;
deferredTasks: ChatViewDeferredTasks;
connectionWork: ConnectionWorkTracker;
messageScrollController: {
showLatest(): void;
};
@ -44,14 +35,11 @@ interface ChatPanelTurnHost {
export interface ChatPanelTurnBundle {
pendingRequests: PendingRequestActions;
reconnect: () => Promise<void>;
turnActions: ChatPanelConversationTurnActions;
}
interface ChatPanelTurnInput {
connection: ConnectionManager;
localItemIds: LocalIdSource;
ensureConnected: () => Promise<void>;
appServer: ChatAppServerGateway;
status: ChatPanelTurnStatus;
inboundHandler: ChatInboundHandler;
@ -63,7 +51,7 @@ interface ChatPanelTurnInput {
serverThreads: ChatServerThreadActions;
goals: ChatPanelGoalActions;
autoTitleCoordinator: AutoTitleCoordinator;
invalidateThreadWork: () => void;
reconnect: () => Promise<void>;
runtimeProjection: ChatPanelRuntimeProjection;
refreshDiagnostics: () => Promise<void>;
refreshLiveState: () => void;
@ -72,9 +60,7 @@ interface ChatPanelTurnInput {
export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnInput): ChatPanelTurnBundle {
const {
connection,
localItemIds,
ensureConnected,
appServer,
status,
inboundHandler,
@ -86,7 +72,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
serverThreads,
goals,
autoTitleCoordinator,
invalidateThreadWork,
reconnect,
runtimeProjection,
refreshDiagnostics,
refreshLiveState,
@ -101,30 +87,6 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
},
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 threadReferenceResolver = appServer.threadReferences({
prepareInput: (text) => composerController.preparedInput(text),
addSystemMessage: status.addSystemMessage,
@ -187,7 +149,6 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
return {
pendingRequests,
reconnect,
turnActions,
};
}

View file

@ -1,7 +1,7 @@
import { type App, normalizePath, type Vault } from "obsidian";
import { DEFAULT_ATTACHMENT_FOLDER } from "../../../settings/model";
import type { ComposerAttachment, ComposerAttachmentHandler } from "../application/composer/attachments";
import { DEFAULT_ATTACHMENT_FOLDER } from "../../../../settings/model";
import type { ComposerAttachment, ComposerAttachmentHandler } from "../../application/composer/attachments";
interface VaultComposerAttachmentHandlerOptions {
app: App;

View file

@ -5,7 +5,7 @@ import type {
ComposerContextRange,
ComposerContextReferenceProvider,
ComposerContextReferences,
} from "../application/composer/context-references";
} from "../../application/composer/context-references";
import { displayNameForFile, linktextForFile } from "./vault-note-links.obsidian";
interface EventSource {

View file

@ -1,8 +1,8 @@
import type { App, EventRef } from "obsidian";
import { stripHeadingForLink, TFile } from "obsidian";
import type { NoteCandidateProvider, WikiLinkMention } from "../application/composer/note-context";
import type { NoteCandidate } from "../application/composer/suggestions";
import type { NoteCandidateProvider, WikiLinkMention } from "../../application/composer/note-context";
import type { NoteCandidate } from "../../application/composer/suggestions";
import { displayNameForFile, linktextForFile } from "./vault-note-links.obsidian";
interface FileCandidate {

View file

@ -13,15 +13,16 @@ import { createStructuredSystemItem, createSystemItem } from "../domain/message-
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
import type { ChatComposerController } from "../panel/composer-controller";
import type { ChatMessageScrollController } from "../panel/surface/message-stream-scroll";
import { createComposerBundle } from "./composer-bundle";
import { type ChatPanelConnectionBundle, createConnectionBundle } from "./connection-bundle";
import { createComposerBundle } from "./bundles/composer-bundle";
import { type ChatPanelConnectionBundle, createConnectionBundle } from "./bundles/connection-bundle";
import { createReconnectAction } from "./bundles/reconnect-bundle";
import { createRuntimeBundle } from "./bundles/runtime-bundle";
import { type ChatPanelShellBundle, createShellBundle } from "./bundles/shell-bundle";
import { createThreadActionBundle, createThreadFoundation, createThreadLifecycleBundle } from "./bundles/thread-bundle";
import { createTurnBundle } from "./bundles/turn-bundle";
import type { ChatPanelEnvironment } from "./contracts";
import type { ChatViewDeferredTasks } from "./deferred-work";
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";
import type { ChatViewDeferredTasks } from "./session/deferred-work";
import { type ChatPanelSharedStateBinding, createChatPanelSharedStateBinding } from "./session/shared-state-binding";
export interface ChatPanelSessionGraph {
connection: {
@ -163,10 +164,17 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
refreshActiveThreads,
notifyActiveThreadIdentityChanged,
});
const turn = createTurnBundle(host, {
const reconnect = createReconnectAction(host, {
connection,
localItemIds,
ensureConnected,
invalidateThreadWork: () => {
threadFoundation.invalidateThreadWork();
},
resumeThread: (threadId) => threadLifecycle.resume.resumeThread(threadId),
status,
});
const turn = createTurnBundle(host, {
localItemIds,
appServer,
status,
inboundHandler,
@ -178,9 +186,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
serverThreads,
goals: threadLifecycle.goals,
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
invalidateThreadWork: () => {
threadFoundation.invalidateThreadWork();
},
reconnect,
runtimeProjection: runtime.projection,
refreshDiagnostics: () => connectionController.refreshDiagnostics(),
refreshLiveState,
@ -194,7 +200,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
threadActions: threadActions.actions,
toolbarPanelActions: threadActions.toolbarPanelActions,
navigation: threadActions.navigation,
reconnect: turn.reconnect,
reconnect,
history: threadFoundation.history,
pendingRequests: turn.pendingRequests,
turn,

View file

@ -10,7 +10,7 @@ import { ChatResumeWorkTracker } from "../application/threads/resume-work";
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom";
import { type ChatMessageScrollController, createChatMessageScrollController } from "../panel/surface/message-stream-scroll";
import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot, ChatWorkspacePanelTurnLifecycle } from "./contracts";
import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./deferred-work";
import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./session/deferred-work";
import { type ChatPanelSessionGraph, createChatPanelSessionGraph } from "./session-graph";
export class ChatPanelSession implements ChatPanelHandle {

View file

@ -1,4 +1,4 @@
import { DeferredTask, type DeferredTaskWindow } from "../../../shared/lifecycle/deferred-task";
import { DeferredTask, type DeferredTaskWindow } from "../../../../shared/lifecycle/deferred-task";
export interface ChatViewDeferredTasks {
scheduleDiagnostics(callback: () => void): void;

View file

@ -1,10 +1,10 @@
import type { ModelMetadata } from "../../../domain/catalog/metadata";
import type { SharedServerMetadata } from "../../../domain/server/metadata";
import type { Thread } from "../../../domain/threads/model";
import type { ObservedResult } from "../../../shared/query/observed-result";
import { observedValue } from "../../../shared/query/observed-result";
import type { ChatStateStore } from "../application/state/store";
import type { ChatPanelConnectionBundle } from "./connection-bundle";
import type { ModelMetadata } from "../../../../domain/catalog/metadata";
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
import type { Thread } from "../../../../domain/threads/model";
import type { ObservedResult } from "../../../../shared/query/observed-result";
import { observedValue } from "../../../../shared/query/observed-result";
import type { ChatStateStore } from "../../application/state/store";
import type { ChatPanelConnectionBundle } from "../bundles/connection-bundle";
type ThreadObserver = (result: ObservedResult<readonly Thread[]>) => void;
type MetadataObserver = (result: ObservedResult<SharedServerMetadata>) => void;

View file

@ -2,7 +2,7 @@
import { describe, expect, it, vi } from "vitest";
import { createVaultComposerAttachmentHandler } from "../../../../src/features/chat/host/composer-attachments.obsidian";
import { createVaultComposerAttachmentHandler } from "../../../../src/features/chat/host/obsidian/composer-attachments.obsidian";
describe("vault composer attachments", () => {
it("saves unnamed pasted images with a generated filename and Obsidian embed marker", async () => {

View file

@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { scheduleDeferredDiagnosticsRefresh } from "../../../../src/features/chat/host/connection-bundle";
import { scheduleDeferredDiagnosticsRefresh } from "../../../../src/features/chat/host/bundles/connection-bundle";
describe("connection bundle deferred diagnostics", () => {
it("reports deferred diagnostics failures without returning a blocking promise", async () => {

View file

@ -2,7 +2,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-work";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/session/deferred-work";
describe("createChatViewDeferredTasks", () => {
beforeEach(() => {

View file

@ -0,0 +1,57 @@
import { describe, expect, it, vi } from "vitest";
import { ConnectionManager } from "../../../../src/app-server/connection/connection-manager";
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
import { createReconnectAction } from "../../../../src/features/chat/host/bundles/reconnect-bundle";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/session/deferred-work";
import { ConnectionWorkTracker } from "../../../../src/shared/lifecycle/connection-work";
describe("createReconnectAction", () => {
it("wires host connection lifecycle cleanup into panel reconnect", async () => {
const stateStore = createChatStateStore();
stateStore.dispatch({
type: "active-thread/resumed",
thread: { id: "thread-1" } as never,
cwd: "/vault",
model: null,
reasoningEffort: null,
serviceTier: null,
approvalsReviewer: null,
});
const connectionWork = new ConnectionWorkTracker();
const activeConnectionWork = connectionWork.begin();
const deferredTasks = createChatViewDeferredTasks(() => ({ setTimeout, clearTimeout }) as Pick<Window, "setTimeout" | "clearTimeout">);
const connection = new ConnectionManager(() => "codex", "/vault");
const resetConnection = vi.spyOn(connection, "resetConnection");
const clearDiagnostics = vi.spyOn(deferredTasks, "clearDiagnostics");
const status = {
set: vi.fn(),
addSystemMessage: vi.fn(),
};
const ensureConnected = vi.fn().mockResolvedValue(undefined);
const resumeThread = vi.fn().mockResolvedValue(undefined);
const reconnect = createReconnectAction(
{
stateStore,
connectionWork,
deferredTasks,
},
{
connection,
ensureConnected,
invalidateThreadWork: vi.fn(),
resumeThread,
status,
},
);
await reconnect();
expect(connectionWork.isStale(activeConnectionWork)).toBe(true);
expect(clearDiagnostics).toHaveBeenCalledOnce();
expect(resetConnection).toHaveBeenCalledOnce();
expect(status.set).toHaveBeenCalledWith("Reconnecting...", { kind: "connecting" });
expect(ensureConnected).toHaveBeenCalledOnce();
expect(resumeThread).toHaveBeenCalledWith("thread-1");
});
});

View file

@ -9,7 +9,7 @@ import { type ChatStateStore, createChatStateStore } from "../../../../src/featu
import { HistoryController } from "../../../../src/features/chat/application/threads/history-controller";
import { ChatResumeWorkTracker } from "../../../../src/features/chat/application/threads/resume-work";
import type { ChatPanelEnvironment } from "../../../../src/features/chat/host/contracts";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-work";
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/session/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";

View file

@ -3,8 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import { createServerDiagnostics, diagnosticsWithToolInventory } from "../../../../src/domain/server/diagnostics";
import type { ToolInventorySnapshot } from "../../../../src/domain/server/tool-inventory";
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
import { createTurnBundle } from "../../../../src/features/chat/host/turn-bundle";
import { ConnectionWorkTracker } from "../../../../src/shared/lifecycle/connection-work";
import { createTurnBundle } from "../../../../src/features/chat/host/bundles/turn-bundle";
describe("createTurnBundle", () => {
it("uses cached tool inventory for /tools without refreshing diagnostics", async () => {
@ -51,24 +50,13 @@ function turnBundleFixture(options: { stateStore?: ReturnType<typeof createChatS
const refreshDiagnostics = vi.fn().mockResolvedValue(undefined);
const bundle = createTurnBundle(
{
environment: {
plugin: {
settingsRef: {
vaultPath: "/vault",
},
},
},
stateStore,
deferredTasks: {},
connectionWork: new ConnectionWorkTracker(),
messageScrollController: {
showLatest: vi.fn(),
},
} as never,
{
connection: { resetConnection: vi.fn() },
localItemIds: { next: vi.fn(() => "local-id") },
ensureConnected: vi.fn().mockResolvedValue(undefined),
appServer: {
connectionAvailable: vi.fn(() => true),
threadReferences: vi.fn(() => ({ referThread: vi.fn() })),
@ -100,7 +88,7 @@ function turnBundleFixture(options: { stateStore?: ReturnType<typeof createChatS
serverThreads: {},
goals: {},
autoTitleCoordinator: { resetThreadTurnPresence: vi.fn() },
invalidateThreadWork: vi.fn(),
reconnect: vi.fn(),
runtimeProjection,
refreshDiagnostics,
refreshLiveState: vi.fn(),

View file

@ -1,8 +1,8 @@
import { type App, type EventRef, TFile } from "obsidian";
import { describe, expect, it, vi } from "vitest";
import { VaultComposerContextReferenceProvider } from "../../../../src/features/chat/host/vault-composer-context-reference-provider.obsidian";
import { VaultNoteCandidateProvider } from "../../../../src/features/chat/host/vault-note-candidate-provider.obsidian";
import { VaultComposerContextReferenceProvider } from "../../../../src/features/chat/host/obsidian/vault-composer-context-reference-provider.obsidian";
import { VaultNoteCandidateProvider } from "../../../../src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian";
describe("VaultNoteCandidateProvider", () => {
it("builds note candidates from markdown files", () => {

View file

@ -1,6 +1,6 @@
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { copyFile, mkdir, mkdtemp, readdir, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
@ -19,8 +19,6 @@ const APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE =
"Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol at its app-server boundary; feature state and UI must use Panel-owned models.";
const RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE =
"Keep responsibility-split source roots free of module files; add modules to the matching subfolder instead of the root.";
const APP_SERVER_ROOT_MODULE_IMPORT_MESSAGE =
"Import app-server boundary modules from responsibility subfolders, not src/app-server root modules.";
const APP_SERVER_SUBFOLDER_ROOT_IMPORT_MESSAGE =
"App-server subfolders must not import sibling root modules; move the dependency into a responsibility subfolder.";
const SETTINGS_APP_SERVER_BOUNDARY_MESSAGE =
@ -889,26 +887,14 @@ export const value = statusText;
expect(pluginDiagnostics(report, "src/settings/app-server/dynamic-data.ts")).toEqual([]);
});
it("keeps app-server root from becoming a boundary escape hatch", async () => {
const cwd = await tempBiomeWorkspace([
"no-responsibility-root-module-files.grit",
"no-app-server-root-module-imports.grit",
"no-app-server-subfolder-root-imports.grit",
]);
it("keeps app-server root modules from becoming boundary escape hatches", async () => {
const cwd = await tempBiomeWorkspace(["no-responsibility-root-module-files.grit", "no-app-server-subfolder-root-imports.grit"]);
await writeFile(
path.join(cwd, "src/app-server/escape.ts"),
`
import type { AppServerClient } from "./connection/client";
export type Escape = AppServerClient;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/app-server/root-import.ts"),
`
import { listThreads } from "../../../../app-server/threads";
export const read = listThreads;
`.trimStart(),
);
await writeFile(
@ -916,6 +902,22 @@ export const read = listThreads;
`
import { createChatAppServerGateway } from "../app-server/session-gateway";
export const gateway = createChatAppServerGateway;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/host/bundles/chat-app-server-root-import.ts"),
`
import { createChatAppServerGateway } from "../../app-server/session-gateway";
export const gateway = createChatAppServerGateway;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/host/session/chat-app-server-root-import.ts"),
`
import { createChatAppServerGateway } from "../../app-server/session-gateway";
export const gateway = createChatAppServerGateway;
`.trimStart(),
);
@ -941,8 +943,9 @@ export const read = listThreads;
const report = biomeLint(
[
"src/app-server/escape.ts",
"src/features/chat/app-server/root-import.ts",
"src/features/chat/host/chat-app-server-root-import.ts",
"src/features/chat/host/bundles/chat-app-server-root-import.ts",
"src/features/chat/host/session/chat-app-server-root-import.ts",
"src/app-server/services/root-import.ts",
"src/app-server/services/allowed.ts",
],
@ -950,8 +953,9 @@ export const read = listThreads;
);
expect(pluginMessages(report, "src/app-server/escape.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]);
expect(pluginMessages(report, "src/features/chat/app-server/root-import.ts")).toEqual([APP_SERVER_ROOT_MODULE_IMPORT_MESSAGE]);
expect(pluginDiagnostics(report, "src/features/chat/host/chat-app-server-root-import.ts")).toEqual([]);
expect(pluginDiagnostics(report, "src/features/chat/host/bundles/chat-app-server-root-import.ts")).toEqual([]);
expect(pluginDiagnostics(report, "src/features/chat/host/session/chat-app-server-root-import.ts")).toEqual([]);
expect(pluginMessages(report, "src/app-server/services/root-import.ts")).toEqual([APP_SERVER_SUBFOLDER_ROOT_IMPORT_MESSAGE]);
expect(pluginDiagnostics(report, "src/app-server/services/allowed.ts")).toEqual([]);
});
@ -963,7 +967,7 @@ export const read = listThreads;
"Keep app-server projection RPCs behind app-server facades; consume Panel-owned snapshots or view models here.",
]);
expect(pluginDiagnostics(report, "src/app-server/services/threads.ts")).toEqual([]);
expect(pluginDiagnostics(report, "src/features/chat/host/connection-bundle.ts")).toEqual([]);
expect(pluginDiagnostics(report, "src/features/chat/host/bundles/connection-bundle.ts")).toEqual([]);
});
it("keeps CSS on design tokens and scoped selectors", async () => {
@ -1397,9 +1401,9 @@ export async function read(client: AppServerClient): Promise<void> {
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/host/connection-bundle.ts"),
path.join(cwd, "src/features/chat/host/bundles/connection-bundle.ts"),
`
import type { AppServerClient } from "../../../app-server/connection/client";
import type { AppServerClient } from "../../../../app-server/connection/client";
export async function resume(client: AppServerClient): Promise<void> {
await client.request("thread/resume", { threadId: "thread", cwd: "/vault" });
@ -1462,7 +1466,7 @@ export const load = listHookCatalog;
"src/shared/connection-client.ts",
"src/features/chat/application/threads/history.ts",
"src/app-server/services/threads.ts",
"src/features/chat/host/connection-bundle.ts",
"src/features/chat/host/bundles/connection-bundle.ts",
"src/settings/dynamic-sections-controller.ts",
"src/settings/adapter-leak.ts",
"src/settings/app-server/dynamic-data.ts",
@ -1473,7 +1477,6 @@ export const load = listHookCatalog;
async function tempBiomeWorkspace(plugins) {
const cwd = await mkdtemp(path.join(tmpdir(), "codex-panel-grit-policy-"));
const tempPlugins = await Promise.all(plugins.map((plugin) => tempProjectPluginConfig(cwd, plugin)));
await mkdir(cwd, { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/domain/message-stream"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/application/state"), { recursive: true });
@ -1482,6 +1485,8 @@ async function tempBiomeWorkspace(plugins) {
await mkdir(path.join(cwd, "src/features/chat/app-server/inbound"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/app-server/mappers/message-stream"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/host"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/host/bundles"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/host/session"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/panel"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/panel/surface"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/presentation"), { recursive: true });
@ -1506,7 +1511,7 @@ async function tempBiomeWorkspace(plugins) {
JSON.stringify({
$schema: "https://biomejs.dev/schemas/2.5.1/schema.json",
vcs: { enabled: false },
plugins: tempPlugins,
plugins: plugins.map((plugin) => projectPluginConfig(plugin)),
css: { linter: { enabled: true } },
}),
);
@ -1530,20 +1535,16 @@ function biomeLint(files, cwd, options = {}) {
return report;
}
async function tempProjectPluginConfig(cwd, plugin) {
function projectPluginConfig(plugin) {
const projectPlugin = projectPluginByName.get(plugin);
if (!projectPlugin) {
throw new Error(`Missing ${plugin} in biome.jsonc plugins`);
}
const projectPluginRelativePath = normalizeProjectRelativePath(projectPluginPath(projectPlugin));
const projectPluginPathAbsolute = path.resolve(repoRoot, projectPluginPath(projectPlugin));
const tempPluginPath = path.join(cwd, projectPluginRelativePath);
await mkdir(path.dirname(tempPluginPath), { recursive: true });
await copyFile(projectPluginPathAbsolute, tempPluginPath);
const pluginPath = path.resolve(repoRoot, projectPluginPath(projectPlugin));
if (typeof projectPlugin === "string") {
return tempPluginPath;
return pluginPath;
}
return { ...projectPlugin, path: tempPluginPath };
return { ...projectPlugin, path: pluginPath };
}
function projectPluginPath(plugin) {