From a14e3d19e715cea65453bde9993033eca0822a40 Mon Sep 17 00:00:00 2001 From: murashit Date: Sat, 27 Jun 2026 20:55:38 +0900 Subject: [PATCH] Narrow chat host boundary dependencies --- biome.jsonc | 4 + .../no-chat-host-rendering-layer-imports.grit | 18 ++++ src/features/chat/host/composer-bundle.ts | 4 +- src/features/chat/host/connection-bundle.ts | 2 +- src/features/chat/host/contracts.ts | 22 ++++- src/features/chat/host/runtime-bundle.ts | 81 ++-------------- src/features/chat/host/session-graph.ts | 2 +- src/features/chat/host/session.ts | 13 ++- src/features/chat/host/shell-bundle.ts | 8 +- src/features/chat/host/thread-bundle.ts | 14 +-- src/features/chat/host/turn-bundle.ts | 3 +- .../chat/host/workspace-panel-snapshot.ts | 8 -- .../chat/panel/runtime-status-projection.ts | 95 +++++++++++++++++++ src/features/chat/panel/toolbar-actions.ts | 6 +- .../chat/panel/toolbar-hit-test.dom.ts | 23 +++++ src/features/chat/ui/toolbar.dom.ts | 27 ------ src/plugin-runtime.ts | 32 ++++++- .../features/chat/host/session-graph.test.ts | 18 ++-- .../chat/host/view-connection.test.ts | 34 +++++-- .../host/workspace-panel-snapshot.test.ts | 13 --- .../panel/runtime-status-projection.test.ts | 61 ++++++++++++ .../chat/panel/surface/projections.test.ts | 48 ---------- tests/features/chat/support/settings.ts | 19 ++++ tests/main.test.ts | 6 +- tests/scripts/grit-policy.test.mjs | 27 ++++++ 25 files changed, 370 insertions(+), 218 deletions(-) create mode 100644 scripts/lint/no-chat-host-rendering-layer-imports.grit delete mode 100644 src/features/chat/host/workspace-panel-snapshot.ts create mode 100644 src/features/chat/panel/runtime-status-projection.ts create mode 100644 src/features/chat/panel/toolbar-hit-test.dom.ts delete mode 100644 tests/features/chat/host/workspace-panel-snapshot.test.ts create mode 100644 tests/features/chat/panel/runtime-status-projection.test.ts create mode 100644 tests/features/chat/support/settings.ts diff --git a/biome.jsonc b/biome.jsonc index 35d9a7a0..d3cda4eb 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -75,6 +75,10 @@ "path": "./scripts/lint/no-workspace-chat-internal-imports.grit", "includes": ["**/src/workspace/**/*.ts"] }, + { + "path": "./scripts/lint/no-chat-host-rendering-layer-imports.grit", + "includes": ["**/src/features/chat/host/**/*.ts"] + }, { "path": "./scripts/lint/no-chat-panel-runtime-boundary-imports.grit", "includes": ["**/src/features/chat/panel/**/*.ts", "**/src/features/chat/panel/**/*.tsx"] diff --git a/scripts/lint/no-chat-host-rendering-layer-imports.grit b/scripts/lint/no-chat-host-rendering-layer-imports.grit new file mode 100644 index 00000000..b757ca00 --- /dev/null +++ b/scripts/lint/no-chat-host-rendering-layer-imports.grit @@ -0,0 +1,18 @@ +language js + +private pattern js_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +js_module_reference() as $stmt where { + $stmt <: contains `$source` where { + $source <: r"^[\"'](?:(?:\.\./)+(?:presentation|ui)|src/features/chat/(?:presentation|ui))(?:/.*)?[\"']$" + }, + register_diagnostic(span=$stmt, message="Chat host modules must not import chat presentation or UI layers; route render-facing projections through panel-owned adapters.", severity="error") +} diff --git a/src/features/chat/host/composer-bundle.ts b/src/features/chat/host/composer-bundle.ts index 8c1fc8a4..38e67827 100644 --- a/src/features/chat/host/composer-bundle.ts +++ b/src/features/chat/host/composer-bundle.ts @@ -65,8 +65,8 @@ function createSessionComposerController( sourcePath: () => environment.obsidian.app.workspace.getActiveFile()?.path ?? "", stateStore, viewId: environment.obsidian.viewId, - sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut, - scrollThreadFromComposerEdges: () => environment.plugin.settingsRef.settings.scrollThreadFromComposerEdges, + sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut(), + scrollThreadFromComposerEdges: () => environment.plugin.settingsRef.settings.scrollThreadFromComposerEdges(), canInterrupt: (state) => { return state.turnBusy && Boolean(state.activeThreadId && state.activeTurnId); }, diff --git a/src/features/chat/host/connection-bundle.ts b/src/features/chat/host/connection-bundle.ts index 7e535380..513d2df9 100644 --- a/src/features/chat/host/connection-bundle.ts +++ b/src/features/chat/host/connection-bundle.ts @@ -220,7 +220,7 @@ export function createConnectionBundle( }, setStatus: status.set, addSystemMessage: status.addSystemMessage, - configuredCommand: () => environment.plugin.settingsRef.settings.codexPath, + configuredCommand: () => environment.plugin.settingsRef.settings.codexPath(), refreshLiveState: () => { host.refreshLiveState(); }, diff --git a/src/features/chat/host/contracts.ts b/src/features/chat/host/contracts.ts index 4dcb3e32..66aca2c9 100644 --- a/src/features/chat/host/contracts.ts +++ b/src/features/chat/host/contracts.ts @@ -4,25 +4,37 @@ import type { AppServerClient } from "../../../app-server/connection/client"; import type { AppServerQueryContext } from "../../../app-server/query/keys"; import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../../app-server/query/thread-catalog"; import type { ArchiveExportDestination } from "../../../app-server/services/thread-archive-markdown"; -import type { ModelMetadata } from "../../../domain/catalog/metadata"; +import type { ModelMetadata, ReasoningEffort } from "../../../domain/catalog/metadata"; import type { PendingRequestCounts } from "../../../domain/pending-requests/aggregate"; import type { SharedServerMetadata } from "../../../domain/server/metadata"; -import type { CodexPanelSettings } from "../../../settings/model"; +import type { ArchiveExportSettings } from "../../../domain/threads/archive-markdown"; import type { ObservedResultListener } from "../../../shared/query/observed-result"; +import type { SendShortcut } from "../../../shared/ui/keyboard"; import type { ChatTurnDiffViewState } from "../domain/turn-diff"; export interface CodexChatHost { - readonly settingsRef: PluginSettingsRef; + readonly settingsRef: ChatPanelSettingsRef; readonly workspace: WorkspacePanels; readonly appServerQueries: ChatAppServerQueries; readonly threadCatalog: ChatThreadCatalog; } -export interface PluginSettingsRef { - readonly settings: CodexPanelSettings; +interface ChatPanelSettingsRef { + readonly settings: ChatPanelSettingsAccess; readonly vaultPath: string; } +export interface ChatPanelSettingsAccess { + archiveExportEnabled(): boolean; + archiveExportSettings(): ArchiveExportSettings; + codexPath(): string; + scrollThreadFromComposerEdges(): boolean; + sendShortcut(): SendShortcut; + showToolbar(): boolean; + threadNamingEffort(): ReasoningEffort | null; + threadNamingModel(): string | null; +} + interface WorkspacePanels { openThreadInNewView(threadId: string): Promise; focusThreadInOpenView(threadId: string): Promise; diff --git a/src/features/chat/host/runtime-bundle.ts b/src/features/chat/host/runtime-bundle.ts index 3b4a629f..9b393102 100644 --- a/src/features/chat/host/runtime-bundle.ts +++ b/src/features/chat/host/runtime-bundle.ts @@ -1,18 +1,10 @@ import type { ConnectionManager } from "../../../app-server/connection/connection-manager"; import { createChatRuntimeSettingsTransport } from "../app-server/runtime/thread-settings-transport"; -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 ChatPanelRuntimeProjection, createChatPanelRuntimeProjection } from "../panel/runtime-status-projection"; import type { CurrentAppServerClient } from "./connection-bundle"; import type { ChatPanelEnvironment } from "./contracts"; @@ -27,14 +19,6 @@ interface ChatPanelRuntimeHost { stateStore: ChatStateStore; } -export interface ChatPanelRuntimeProjection { - connectionDiagnosticDetails: () => MessageStreamNoticeSection[]; - modelStatusLines: () => string[]; - effortStatusLines: () => string[]; - statusSummaryLines: () => string[]; - toolInventoryDetails: () => MessageStreamNoticeSection[]; -} - interface ChatPanelRuntimeBundle { settings: ChatPanelRuntimeSettingsActions; projection: ChatPanelRuntimeProjection; @@ -77,63 +61,10 @@ function collaborationModeLabel(stateStore: ChatStateStore): string { } 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(), + return createChatPanelRuntimeProjection({ + state: () => host.stateStore.getState(), + connected: () => connection.isConnected(), + configuredCommand: () => host.environment.plugin.settingsRef.settings.codexPath(), + 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()); -} diff --git a/src/features/chat/host/session-graph.ts b/src/features/chat/host/session-graph.ts index fbd455b4..a8789199 100644 --- a/src/features/chat/host/session-graph.ts +++ b/src/features/chat/host/session-graph.ts @@ -244,7 +244,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch } function createConnectionManager(environment: ChatPanelEnvironment): ConnectionManager { - return new ConnectionManager(() => environment.plugin.settingsRef.settings.codexPath, environment.plugin.settingsRef.vaultPath); + return new ConnectionManager(() => environment.plugin.settingsRef.settings.codexPath(), environment.plugin.settingsRef.vaultPath); } function createSessionStatus(stateStore: ChatStateStore, localItemIds: LocalIdSource): ChatPanelSessionStatus { diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index 0f57a97d..843d9238 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -9,10 +9,9 @@ import { parseRestoredThreadState, type RestoredThreadPlaceholderState } from ". 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 } from "./contracts"; +import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot, ChatWorkspacePanelTurnLifecycle } from "./contracts"; import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./deferred-work"; import { type ChatPanelSessionGraph, createChatPanelSessionGraph } from "./session-graph"; -import { openPanelTurnLifecycle } from "./workspace-panel-snapshot"; export class ChatPanelSession implements ChatPanelHandle { private readonly stateStore: ChatStateStore = createChatStateStore(); @@ -188,7 +187,7 @@ export class ChatPanelSession implements ChatPanelHandle { if (!root) return; renderChatPanelShell(root, { stateStore: this.stateStore, - showToolbar: this.environment.plugin.settingsRef.settings.showToolbar, + showToolbar: this.environment.plugin.settingsRef.settings.showToolbar(), parts: this.graph.shell.parts, }); } @@ -209,7 +208,7 @@ export class ChatPanelSession implements ChatPanelHandle { private currentAppServerContext(): AppServerQueryContext { return { - codexPath: this.environment.plugin.settingsRef.settings.codexPath, + codexPath: this.environment.plugin.settingsRef.settings.codexPath(), vaultPath: this.environment.plugin.settingsRef.vaultPath, }; } @@ -254,3 +253,9 @@ export class ChatPanelSession implements ChatPanelHandle { }); } } + +function openPanelTurnLifecycle(state: ChatState["turn"]["lifecycle"]): ChatWorkspacePanelTurnLifecycle { + if (state.kind === "running") return { kind: "running", turnId: state.turnId }; + if (state.kind === "starting") return { kind: "starting" }; + return { kind: "idle" }; +} diff --git a/src/features/chat/host/shell-bundle.ts b/src/features/chat/host/shell-bundle.ts index 6a0e5e01..a936ceb8 100644 --- a/src/features/chat/host/shell-bundle.ts +++ b/src/features/chat/host/shell-bundle.ts @@ -9,7 +9,7 @@ import { MessageStreamPresenter } from "../panel/surface/message-stream-presente 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 "../ui/toolbar.dom"; +import { toolbarOutsidePointerHit } from "../panel/toolbar-hit-test.dom"; import type { ChatPanelComposerBundle } from "./composer-bundle"; import type { ChatPanelConnectionBundle } from "./connection-bundle"; import type { ChatPanelEnvironment } from "./contracts"; @@ -75,12 +75,12 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan }, settings: { vaultPath: () => environment.plugin.settingsRef.vaultPath, - configuredCommand: () => environment.plugin.settingsRef.settings.codexPath, - archiveExportEnabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled, + configuredCommand: () => environment.plugin.settingsRef.settings.codexPath(), + archiveExportEnabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled(), }, }; const goalSurface: ChatPanelGoalSurface = { - sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut, + sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut(), actions: goals, }; const messageStreamPresenter = new MessageStreamPresenter({ diff --git a/src/features/chat/host/thread-bundle.ts b/src/features/chat/host/thread-bundle.ts index 0898ae38..0c2ac3db 100644 --- a/src/features/chat/host/thread-bundle.ts +++ b/src/features/chat/host/thread-bundle.ts @@ -243,10 +243,10 @@ export function createThreadActionBundle(host: ChatPanelThreadHost, input: ChatP function createSessionThreadTitleService(host: ChatPanelThreadHost, currentClient: CurrentAppServerClient): ThreadTitleService { const { environment, stateStore } = host; return createThreadTitleService({ - codexPath: () => environment.plugin.settingsRef.settings.codexPath, + codexPath: () => environment.plugin.settingsRef.settings.codexPath(), vaultPath: environment.plugin.settingsRef.vaultPath, - threadNamingModel: () => environment.plugin.settingsRef.settings.threadNamingModel, - threadNamingEffort: () => environment.plugin.settingsRef.settings.threadNamingEffort, + threadNamingModel: () => environment.plugin.settingsRef.settings.threadNamingModel(), + threadNamingEffort: () => environment.plugin.settingsRef.settings.threadNamingEffort(), clientAccess: createCurrentClientAccess(currentClient), visibleContext: (threadId) => activeThreadRenameTitleContext(stateStore.getState(), threadId), visibleCompletedTurnContext: (turnId) => @@ -327,12 +327,8 @@ function createSessionThreadOperations(environment: ChatPanelEnvironment, curren 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, + settings: () => environment.plugin.settingsRef.settings.archiveExportSettings(), + enabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled(), vaultPath: environment.plugin.settingsRef.vaultPath, vaultConfigDir: environment.obsidian.app.vault.configDir, }, diff --git a/src/features/chat/host/turn-bundle.ts b/src/features/chat/host/turn-bundle.ts index 3b0b2c63..ddf56c2d 100644 --- a/src/features/chat/host/turn-bundle.ts +++ b/src/features/chat/host/turn-bundle.ts @@ -16,10 +16,11 @@ 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 { CurrentAppServerClient } from "./connection-bundle"; import type { ChatPanelEnvironment } from "./contracts"; import type { ChatViewDeferredTasks } from "./deferred-work"; -import type { ChatPanelRuntimeProjection, ChatPanelRuntimeSettingsActions } from "./runtime-bundle"; +import type { ChatPanelRuntimeSettingsActions } from "./runtime-bundle"; import type { ChatPanelGoalActions, ChatPanelThreadActions, diff --git a/src/features/chat/host/workspace-panel-snapshot.ts b/src/features/chat/host/workspace-panel-snapshot.ts deleted file mode 100644 index 02d53cce..00000000 --- a/src/features/chat/host/workspace-panel-snapshot.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ChatState } from "../application/state/root-reducer"; -import type { ChatWorkspacePanelTurnLifecycle } from "./contracts"; - -export function openPanelTurnLifecycle(state: ChatState["turn"]["lifecycle"]): ChatWorkspacePanelTurnLifecycle { - if (state.kind === "running") return { kind: "running", turnId: state.turnId }; - if (state.kind === "starting") return { kind: "starting" }; - return { kind: "idle" }; -} diff --git a/src/features/chat/panel/runtime-status-projection.ts b/src/features/chat/panel/runtime-status-projection.ts new file mode 100644 index 00000000..b41fb140 --- /dev/null +++ b/src/features/chat/panel/runtime-status-projection.ts @@ -0,0 +1,95 @@ +import { connectionDiagnosticSectionsFromState } from "../application/connection/diagnostic-sections"; +import { toolInventoryDiagnosticSections } from "../application/connection/tool-inventory-diagnostic-sections"; +import { runtimeSnapshotForChatState } from "../application/runtime/snapshot"; +import type { ChatState } from "../application/state/root-reducer"; +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"; + +export interface ChatPanelRuntimeProjection { + connectionDiagnosticDetails: () => MessageStreamNoticeSection[]; + modelStatusLines: () => string[]; + effortStatusLines: () => string[]; + statusSummaryLines: () => string[]; + toolInventoryDetails: () => MessageStreamNoticeSection[]; +} + +interface ChatPanelRuntimeProjectionInput { + state: () => ChatState; + connected: () => boolean; + configuredCommand: () => string; + nowMs: () => number; +} + +export function createChatPanelRuntimeProjection(input: ChatPanelRuntimeProjectionInput): ChatPanelRuntimeProjection { + return { + connectionDiagnosticDetails: () => connectionDiagnosticDetails(input), + modelStatusLines: () => modelStatusLines(input), + effortStatusLines: () => effortStatusLines(input), + statusSummaryLines: () => statusSummaryLines(input), + toolInventoryDetails: () => toolInventoryDetails(input), + }; +} + +function statusSummaryLines(input: ChatPanelRuntimeProjectionInput): string[] { + const state = input.state(); + return buildStatusSummaryLines({ + activeThreadId: state.activeThread.id, + snapshot: runtimeSnapshot(state), + nowMs: input.nowMs(), + }); +} + +function modelStatusLines(input: ChatPanelRuntimeProjectionInput): string[] { + const state = input.state(); + return buildModelStatusLines({ + runtimeConfig: state.connection.runtimeConfig, + pendingModel: state.runtime.pending.model, + snapshot: runtimeSnapshot(state), + collaborationModeLabel: collaborationModeLabel(state), + }); +} + +function effortStatusLines(input: ChatPanelRuntimeProjectionInput): string[] { + const state = input.state(); + return buildEffortStatusLines({ + runtimeConfig: state.connection.runtimeConfig, + pendingReasoningEffort: state.runtime.pending.reasoningEffort, + snapshot: runtimeSnapshot(state), + }); +} + +function connectionDiagnosticDetails(input: ChatPanelRuntimeProjectionInput): MessageStreamNoticeSection[] { + const sections = connectionDiagnosticSectionsFromState({ + state: input.state(), + connected: input.connected(), + configuredCommand: input.configuredCommand(), + }); + return noticeSectionsFromDiagnostics(sections); +} + +function toolInventoryDetails(input: ChatPanelRuntimeProjectionInput): MessageStreamNoticeSection[] { + return noticeSectionsFromDiagnostics(toolInventoryDiagnosticSections(input.state().connection.serverDiagnostics)); +} + +function noticeSectionsFromDiagnostics( + sections: readonly { title: string; rows: readonly { label: string; value: string }[] }[], +): MessageStreamNoticeSection[] { + return sections.map((section) => ({ + title: section.title, + auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })), + })); +} + +function collaborationModeLabel(state: ChatState): string { + return formatCollaborationModeLabel(state.runtime.pending.collaborationMode); +} + +function runtimeSnapshot(state: ChatState): RuntimeSnapshot { + return runtimeSnapshotForChatState(state); +} diff --git a/src/features/chat/panel/toolbar-actions.ts b/src/features/chat/panel/toolbar-actions.ts index 40d49787..3b9d89ce 100644 --- a/src/features/chat/panel/toolbar-actions.ts +++ b/src/features/chat/panel/toolbar-actions.ts @@ -7,7 +7,6 @@ import type { ThreadRenameEditorActions } from "../application/threads/rename-ed import type { ThreadManagementActions } from "../application/threads/thread-management-actions"; import type { ThreadNavigationActions } from "../application/threads/thread-navigation-actions"; import type { ToolbarActions } from "../ui/toolbar"; -import type { ToolbarOutsidePointerHit } from "../ui/toolbar.dom"; export interface ToolbarPanelActionsHost { stateStore: ChatStateStore; @@ -36,6 +35,11 @@ export interface ToolbarUiActionDependencies { navigation: ThreadNavigationActions; } +export interface ToolbarOutsidePointerHit { + insideToolbarPanel: boolean; + insideArchiveConfirm: boolean; +} + interface ToolbarOutsidePointerContext { hit: ToolbarOutsidePointerHit; renameEditing: boolean; diff --git a/src/features/chat/panel/toolbar-hit-test.dom.ts b/src/features/chat/panel/toolbar-hit-test.dom.ts new file mode 100644 index 00000000..ed2c0afd --- /dev/null +++ b/src/features/chat/panel/toolbar-hit-test.dom.ts @@ -0,0 +1,23 @@ +import type { ToolbarOutsidePointerHit } from "./toolbar-actions"; + +export function toolbarOutsidePointerHit( + event: PointerEvent, + root: HTMLElement | null, + viewWindow: Window | null, +): ToolbarOutsidePointerHit { + const target = event.target; + const domWindow = viewWindow as (Window & { Element: typeof Element }) | null; + if (!root || !domWindow || !(target instanceof domWindow.Element)) { + return { insideToolbarPanel: false, insideArchiveConfirm: false }; + } + + const toolbarPanel = target.closest(".codex-panel__toolbar-primary, .codex-panel__toolbar-panel"); + if (!toolbarPanel || !root.contains(toolbarPanel)) { + return { insideToolbarPanel: false, insideArchiveConfirm: false }; + } + + return { + insideToolbarPanel: true, + insideArchiveConfirm: Boolean(target.closest(".codex-panel__archive-confirm")), + }; +} diff --git a/src/features/chat/ui/toolbar.dom.ts b/src/features/chat/ui/toolbar.dom.ts index f8c9aeaf..d4b9e358 100644 --- a/src/features/chat/ui/toolbar.dom.ts +++ b/src/features/chat/ui/toolbar.dom.ts @@ -4,30 +4,3 @@ export function focusToolbarRenameInput(input: HTMLInputElement | null): void { input.focus(); input.select(); } - -export interface ToolbarOutsidePointerHit { - insideToolbarPanel: boolean; - insideArchiveConfirm: boolean; -} - -export function toolbarOutsidePointerHit( - event: PointerEvent, - root: HTMLElement | null, - viewWindow: Window | null, -): ToolbarOutsidePointerHit { - const target = event.target; - const domWindow = viewWindow as (Window & { Element: typeof Element }) | null; - if (!root || !domWindow || !(target instanceof domWindow.Element)) { - return { insideToolbarPanel: false, insideArchiveConfirm: false }; - } - - const toolbarPanel = target.closest(".codex-panel__toolbar-primary, .codex-panel__toolbar-panel"); - if (!toolbarPanel || !root.contains(toolbarPanel)) { - return { insideToolbarPanel: false, insideArchiveConfirm: false }; - } - - return { - insideToolbarPanel: true, - insideArchiveConfirm: Boolean(target.closest(".codex-panel__archive-confirm")), - }; -} diff --git a/src/plugin-runtime.ts b/src/plugin-runtime.ts index 5a464fd2..289f65b4 100644 --- a/src/plugin-runtime.ts +++ b/src/plugin-runtime.ts @@ -12,11 +12,11 @@ import type { ChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; import { persistedChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; import type { ChatPanelClientSurface, + ChatPanelSettingsAccess, ChatSharedThreadSurface, ChatViewLifecycleSurface, ChatWorkspacePanelSurface, 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"; @@ -24,11 +24,17 @@ import type { ThreadsViewHost, ThreadsViewSettingsAccess } from "./features/thre import type { ThreadsViewPanelActivity } from "./features/threads-view/state"; import { CodexThreadsView } from "./features/threads-view/view.obsidian"; import type { CodexPanelSettingTabHost } from "./settings/host"; +import type { CodexPanelSettings } from "./settings/model"; import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator"; +interface CodexPanelRuntimeSettingsRef { + readonly settings: CodexPanelSettings; + readonly vaultPath: string; +} + export interface CodexPanelRuntimeOptions { app: App; - settingsRef: PluginSettingsRef; + settingsRef: CodexPanelRuntimeSettingsRef; saveSettings(): Promise; } @@ -107,7 +113,10 @@ export class CodexPanelRuntime implements AppServerClientAccess { chatHost(): CodexChatHost { return { - settingsRef: this.options.settingsRef, + settingsRef: { + settings: this.chatSettings(), + vaultPath: this.options.settingsRef.vaultPath, + }, workspace: { openThreadInNewView: (threadId) => this.panels.openThreadInNewView(threadId), focusThreadInOpenView: (threadId) => this.panels.focusThreadInOpenView(threadId), @@ -121,6 +130,23 @@ export class CodexPanelRuntime implements AppServerClientAccess { }; } + private chatSettings(): ChatPanelSettingsAccess { + return { + archiveExportEnabled: () => this.options.settingsRef.settings.archiveExportEnabled, + archiveExportSettings: () => ({ + archiveExportFolderTemplate: this.options.settingsRef.settings.archiveExportFolderTemplate, + archiveExportFilenameTemplate: this.options.settingsRef.settings.archiveExportFilenameTemplate, + archiveExportTags: this.options.settingsRef.settings.archiveExportTags, + }), + codexPath: () => this.options.settingsRef.settings.codexPath, + scrollThreadFromComposerEdges: () => this.options.settingsRef.settings.scrollThreadFromComposerEdges, + sendShortcut: () => this.options.settingsRef.settings.sendShortcut, + showToolbar: () => this.options.settingsRef.settings.showToolbar, + threadNamingEffort: () => this.options.settingsRef.settings.threadNamingEffort, + threadNamingModel: () => this.options.settingsRef.settings.threadNamingModel, + }; + } + threadsHost(): ThreadsViewHost { return { settings: this.threadsSettings(), diff --git a/tests/features/chat/host/session-graph.test.ts b/tests/features/chat/host/session-graph.test.ts index 83f09639..adb7c8b9 100644 --- a/tests/features/chat/host/session-graph.test.ts +++ b/tests/features/chat/host/session-graph.test.ts @@ -14,9 +14,10 @@ import { createChatPanelSessionGraph } from "../../../../src/features/chat/host/ import { ChatComposerController } from "../../../../src/features/chat/panel/composer-controller"; import { MessageStreamPresenter } from "../../../../src/features/chat/panel/surface/message-stream-presenter"; import { createChatMessageScrollController } from "../../../../src/features/chat/panel/surface/message-stream-scroll"; -import { DEFAULT_SETTINGS } from "../../../../src/settings/model"; +import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model"; import { ConnectionWorkTracker } from "../../../../src/shared/lifecycle/connection-work"; import { installObsidianDomShims } from "../../../support/dom"; +import { chatPanelSettingsAccess } from "../support/settings"; installObsidianDomShims(); @@ -238,6 +239,12 @@ describe("createChatPanelSessionGraph actions", () => { function chatPanelEnvironmentFixture(overrides: PartialChatPanelEnvironment = {}): ChatPanelEnvironment { const threadCatalog = threadCatalogFixture(overrides.plugin?.threadCatalog); const appServerQueries = appServerQueriesFixture(overrides.plugin?.appServerQueries); + const settingsRef = overrides.plugin?.settingsRef; + const settingsSource: CodexPanelSettings = { + ...DEFAULT_SETTINGS, + codexPath: "codex", + sendShortcut: "enter", + }; return { obsidian: { app: { @@ -273,13 +280,8 @@ describe("createChatPanelSessionGraph actions", () => { }, plugin: { settingsRef: { - settings: { - ...DEFAULT_SETTINGS, - codexPath: "codex", - sendShortcut: "enter", - }, - vaultPath: "/vault", - ...overrides.plugin?.settingsRef, + settings: settingsRef?.settings ?? chatPanelSettingsAccess(settingsSource), + vaultPath: settingsRef?.vaultPath ?? "/vault", }, workspace: { openThreadInNewView: vi.fn().mockResolvedValue(undefined), diff --git a/tests/features/chat/host/view-connection.test.ts b/tests/features/chat/host/view-connection.test.ts index b2d2e9fa..f07b49bd 100644 --- a/tests/features/chat/host/view-connection.test.ts +++ b/tests/features/chat/host/view-connection.test.ts @@ -11,13 +11,16 @@ import { createServerDiagnostics } from "../../../../src/domain/server/diagnosti import type { SharedServerMetadata } from "../../../../src/domain/server/metadata"; import type { Thread } from "../../../../src/domain/threads/model"; import type { CodexChatHost } from "../../../../src/features/chat/host/contracts"; -import { DEFAULT_SETTINGS } from "../../../../src/settings/model"; +import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model"; import type { ObservedResult } from "../../../../src/shared/query/observed-result"; import { notices } from "../../../mocks/obsidian"; import { deferred, waitForAsyncWork } from "../../../support/async"; import { installObsidianDomShims } from "../../../support/dom"; +import { chatPanelSettingsAccess } from "../support/settings"; -type TestCodexChatHost = CodexChatHost; +interface TestCodexChatHost extends CodexChatHost { + readonly settingsSource: CodexPanelSettings; +} let CodexChatView: typeof import("../../../../src/features/chat/host/view.obsidian")["CodexChatView"]; interface TrackedView { view: { onClose(): Promise | void }; @@ -199,11 +202,11 @@ describe("CodexChatView connection lifecycle", () => { await view.surface.connect(); expect(view.surface.openPanelSnapshot()).toMatchObject({ connected: true }); - host.settingsRef.settings.showToolbar = false; + host.settingsSource.showToolbar = false; view.surface.refreshSettings(); expect(view.surface.openPanelSnapshot()).toMatchObject({ connected: true }); - host.settingsRef.settings.codexPath = "codex-next"; + host.settingsSource.codexPath = "codex-next"; view.surface.refreshSettings(); expect(view.surface.openPanelSnapshot()).toMatchObject({ connected: false }); }); @@ -484,7 +487,25 @@ describe("CodexChatView connection lifecycle", () => { clientUserMessageId: expect.stringMatching(/^local-user-\d+-[A-Za-z0-9_-]+-[a-z0-9]+$/), }); }); + expect(view.surface.openPanelSnapshot()).toMatchObject({ turnLifecycle: { kind: "starting" } }); expect(view.getState()).toEqual({ version: 1, threadId: "thread-1", threadTitle: "Restored thread" }); + connectionMock.state.onNotification?.({ + method: "turn/started", + params: { + threadId: "thread-1", + turn: { + id: "turn-1", + status: "inProgress", + startedAt: 1, + completedAt: null, + durationMs: null, + error: null, + itemsView: "full", + items: [], + }, + }, + } satisfies Extract); + expect(view.surface.openPanelSnapshot()).toMatchObject({ turnLifecycle: { kind: "running", turnId: "turn-1" } }); }); it("requests a workspace layout save after resuming a thread", async () => { @@ -913,7 +934,7 @@ async function flushAsyncTicks(): Promise { } interface ChatHostFixtureOverrides { - settings?: Partial; + settings?: Partial; vaultPath?: string; openThreadInNewView?: CodexChatHost["workspace"]["openThreadInNewView"]; focusThreadInOpenView?: CodexChatHost["workspace"]["focusThreadInOpenView"]; @@ -1021,8 +1042,9 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost { } }; return { + settingsSource: settings, settingsRef: { - settings, + settings: chatPanelSettingsAccess(settings), vaultPath, }, workspace: { diff --git a/tests/features/chat/host/workspace-panel-snapshot.test.ts b/tests/features/chat/host/workspace-panel-snapshot.test.ts deleted file mode 100644 index 633b64cb..00000000 --- a/tests/features/chat/host/workspace-panel-snapshot.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { openPanelTurnLifecycle } from "../../../../src/features/chat/host/workspace-panel-snapshot"; - -describe("chat workspace panel snapshots", () => { - it("projects open panel turn lifecycle without exposing full chat state", () => { - expect(openPanelTurnLifecycle({ kind: "idle" })).toEqual({ kind: "idle" }); - expect(openPanelTurnLifecycle({ kind: "starting", pendingTurnStart: { anchorItemId: "local", promptSubmitHookItemIds: [] } })).toEqual({ - kind: "starting", - }); - expect(openPanelTurnLifecycle({ kind: "running", turnId: "turn" })).toEqual({ kind: "running", turnId: "turn" }); - }); -}); diff --git a/tests/features/chat/panel/runtime-status-projection.test.ts b/tests/features/chat/panel/runtime-status-projection.test.ts new file mode 100644 index 00000000..94b4ffa3 --- /dev/null +++ b/tests/features/chat/panel/runtime-status-projection.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; + +import { type ConfigReadResult, runtimeConfigSnapshotFromAppServerConfig } from "../../../../src/app-server/protocol/runtime-config"; +import type { ModelMetadata } from "../../../../src/domain/catalog/metadata"; +import type { RuntimeConfigSnapshot } from "../../../../src/domain/runtime/config"; +import { createChatPanelRuntimeProjection } from "../../../../src/features/chat/panel/runtime-status-projection"; +import { chatStateFixture, chatStateWith } from "../support/state"; + +describe("createChatPanelRuntimeProjection", () => { + it("builds slash-command status lines from chat state", () => { + let state = chatStateFixture(); + state = chatStateWith(state, { activeThread: { id: "thread-1" } }); + state = chatStateWith(state, { + connection: { + runtimeConfig: runtimeConfigFixture({ + model: "gpt-5.5", + model_provider: "openai", + model_reasoning_effort: "high", + service_tier: "fast", + }), + }, + }); + state = chatStateWith(state, { connection: { availableModels: [modelFixture("gpt-5.5")] } }); + const projection = createChatPanelRuntimeProjection({ + state: () => state, + connected: () => true, + configuredCommand: () => "codex", + nowMs: () => 0, + }); + + expect(projection.statusSummaryLines()[1]).toBe("Thread: thread-1"); + expect(projection.modelStatusLines()).toContain("Model: gpt-5.5"); + expect(projection.modelStatusLines()).toContain("Mode: Default"); + expect(projection.effortStatusLines()).toContain("Supported: high"); + }); +}); + +function runtimeConfigFixture(config: Record): RuntimeConfigSnapshot { + return runtimeConfigSnapshotFromAppServerConfig({ + config: config as ConfigReadResult["config"], + origins: {}, + layers: null, + }); +} + +function modelFixture(model: string): ModelMetadata { + return { + id: model, + model, + displayName: model, + description: "", + hidden: false, + supportedReasoningEfforts: ["high"], + defaultReasoningEffort: "high", + inputModalities: [], + additionalSpeedTiers: [], + serviceTiers: [], + defaultServiceTier: null, + isDefault: true, + }; +} diff --git a/tests/features/chat/panel/surface/projections.test.ts b/tests/features/chat/panel/surface/projections.test.ts index 5e381b28..26a00f04 100644 --- a/tests/features/chat/panel/surface/projections.test.ts +++ b/tests/features/chat/panel/surface/projections.test.ts @@ -8,14 +8,12 @@ import type { RuntimeConfigSnapshot } from "../../../../../src/domain/runtime/co import { createServerDiagnostics } from "../../../../../src/domain/server/diagnostics"; import type { ThreadGoal } from "../../../../../src/domain/threads/goal"; import type { Thread } from "../../../../../src/domain/threads/model"; -import { runtimeSnapshotForChatState } from "../../../../../src/features/chat/application/runtime/snapshot"; import type { ChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { ChatPanelShellStateContext, createChatPanelShellState } from "../../../../../src/features/chat/panel/shell-state"; import type { ChatPanelComposerSurface } from "../../../../../src/features/chat/panel/surface/composer-projection"; import { chatPanelComposerProjection } from "../../../../../src/features/chat/panel/surface/composer-projection"; import { ChatPanelGoal, type ChatPanelGoalSurface } from "../../../../../src/features/chat/panel/surface/goal-projection"; import { ChatPanelToolbar } from "../../../../../src/features/chat/panel/surface/toolbar-projection"; -import { effortStatusLines, modelStatusLines, statusSummaryLines } from "../../../../../src/features/chat/presentation/runtime/status"; import type { ToolbarActions } from "../../../../../src/features/chat/ui/toolbar"; import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root.dom"; import { installObsidianDomShims } from "../../../../support/dom"; @@ -233,48 +231,6 @@ describe("chat panel surface projections", () => { }); }); - it("builds slash-command status lines from chat state", () => { - let state = chatStateFixture(); - state = chatStateWith(state, { activeThread: { id: "thread-1" } }); - state = chatStateWith(state, { - connection: { - runtimeConfig: runtimeConfigFixture({ - model: "gpt-5.5", - model_provider: "openai", - model_reasoning_effort: "high", - service_tier: "fast", - }), - }, - }); - state = chatStateWith(state, { connection: { availableModels: [modelFixture("gpt-5.5")] } }); - const snapshot = runtimeSnapshotFixture(state); - - expect(statusSummaryLines({ activeThreadId: state.activeThread.id, snapshot, nowMs: 0 })[1]).toBe("Thread: thread-1"); - expect( - modelStatusLines({ - runtimeConfig: state.connection.runtimeConfig, - pendingModel: state.runtime.pending.model, - snapshot, - collaborationModeLabel: "Default", - }), - ).toContain("Model: gpt-5.5"); - expect( - modelStatusLines({ - runtimeConfig: state.connection.runtimeConfig, - pendingModel: state.runtime.pending.model, - snapshot, - collaborationModeLabel: "Default", - }), - ).toContain("Mode: Default"); - expect( - effortStatusLines({ - runtimeConfig: state.connection.runtimeConfig, - pendingReasoningEffort: state.runtime.pending.reasoningEffort, - snapshot, - }), - ).toContain("Supported: high"); - }); - it("builds runtime composer choices from immutable chat state snapshots", () => { let state = chatStateFixture(); state = chatStateWith(state, { @@ -520,10 +476,6 @@ function runtimeConfigFixture(config: Record): RuntimeConfigSna }); } -function runtimeSnapshotFixture(state: ChatState) { - return runtimeSnapshotForChatState(state); -} - function threadFixture(id: string, name: string | null): Thread { return { id, diff --git a/tests/features/chat/support/settings.ts b/tests/features/chat/support/settings.ts new file mode 100644 index 00000000..bc00d2a0 --- /dev/null +++ b/tests/features/chat/support/settings.ts @@ -0,0 +1,19 @@ +import type { ChatPanelSettingsAccess } from "../../../../src/features/chat/host/contracts"; +import type { CodexPanelSettings } from "../../../../src/settings/model"; + +export function chatPanelSettingsAccess(settings: CodexPanelSettings): ChatPanelSettingsAccess { + return { + archiveExportEnabled: () => settings.archiveExportEnabled, + archiveExportSettings: () => ({ + archiveExportFolderTemplate: settings.archiveExportFolderTemplate, + archiveExportFilenameTemplate: settings.archiveExportFilenameTemplate, + archiveExportTags: settings.archiveExportTags, + }), + codexPath: () => settings.codexPath, + scrollThreadFromComposerEdges: () => settings.scrollThreadFromComposerEdges, + sendShortcut: () => settings.sendShortcut, + showToolbar: () => settings.showToolbar, + threadNamingEffort: () => settings.threadNamingEffort, + threadNamingModel: () => settings.threadNamingModel, + }; +} diff --git a/tests/main.test.ts b/tests/main.test.ts index a8983ca3..3aa7bb98 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -8,8 +8,9 @@ import type { Thread } from "../src/domain/threads/model"; import type { CodexChatHost } from "../src/features/chat/host/contracts"; import type { CodexChatView } from "../src/features/chat/host/view.obsidian"; import type CodexPanelPlugin from "../src/main"; -import { DEFAULT_SETTINGS } from "../src/settings/model"; +import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../src/settings/model"; import { WorkspacePanelCoordinator } from "../src/workspace/panel-coordinator"; +import { chatPanelSettingsAccess } from "./features/chat/support/settings"; import { waitForAsyncWork } from "./support/async"; import { installObsidianDomShims } from "./support/dom"; @@ -814,9 +815,10 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) { } function chatHostFixture(): CodexChatHost { + const settings: CodexPanelSettings = { ...DEFAULT_SETTINGS, codexPath: "codex", sendShortcut: "enter" }; return { settingsRef: { - settings: { ...DEFAULT_SETTINGS, codexPath: "codex", sendShortcut: "enter" }, + settings: chatPanelSettingsAccess(settings), vaultPath: "/vault", }, workspace: { diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index a43b1ab2..fea42887 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -30,6 +30,8 @@ const CHAT_WORKSPACE_BOUNDARY_MESSAGE = const FEATURE_WORKSPACE_BOUNDARY_MESSAGE = "Feature modules must not import workspace modules; pass workspace capabilities through feature host contracts."; const WORKSPACE_CHAT_INTERNAL_MESSAGE = "Workspace modules may coordinate chat only through chat host contracts and Obsidian views."; +const CHAT_HOST_RENDERING_LAYER_MESSAGE = + "Chat host modules must not import chat presentation or UI layers; route render-facing projections through panel-owned adapters."; const CHAT_PANEL_RUNTIME_BOUNDARY_MESSAGE = "Chat panel modules must not import app-server adapters or chat host internals."; const CHAT_PRESENTATION_OUTER_LAYER_MESSAGE = "Chat presentation modules must stay pure view-model projection; keep application, app-server, host, panel, and UI dependencies outward."; @@ -407,6 +409,7 @@ export function timestamp(): number { "no-chat-workspace-boundary-imports.grit", "no-feature-workspace-boundary-imports.grit", "no-workspace-chat-internal-imports.grit", + "no-chat-host-rendering-layer-imports.grit", "no-chat-panel-runtime-boundary-imports.grit", "no-chat-presentation-outer-layer-imports.grit", "no-chat-ui-outer-layer-imports.grit", @@ -476,6 +479,24 @@ export type Escape = WorkspacePanelCoordinator; import type { ChatStateStore } from "../application/state/store"; export type Allowed = ChatStateStore; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/chat/host/rendering-escape.ts"), + ` +import { statusText } from "../presentation/runtime/status"; +import { Toolbar } from "../ui/toolbar"; + +export const values = [statusText, Toolbar] satisfies unknown[]; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/chat/host/rendering-allowed.ts"), + ` +import type { ChatStateStore } from "../application/state/store"; +import type { ChatPanelToolbarSurface } from "../panel/surface/toolbar-projection"; + +export type Allowed = ChatStateStore | ChatPanelToolbarSurface; `.trimStart(), ); await writeFile( @@ -588,6 +609,8 @@ export const value = statusText; "src/features/chat/app-server/allowed.ts", "src/features/chat/host/workspace-escape.ts", "src/features/chat/host/workspace-allowed.ts", + "src/features/chat/host/rendering-escape.ts", + "src/features/chat/host/rendering-allowed.ts", "src/features/threads-view/workspace-escape.ts", "src/features/threads-view/workspace-allowed.ts", "src/workspace/chat-internal-escape.ts", @@ -613,6 +636,10 @@ export const value = statusText; expect(pluginDiagnostics(report, "src/features/chat/app-server/allowed.ts")).toEqual([]); expect(pluginMessages(report, "src/features/chat/host/workspace-escape.ts")).toEqual([CHAT_WORKSPACE_BOUNDARY_MESSAGE]); expect(pluginDiagnostics(report, "src/features/chat/host/workspace-allowed.ts")).toEqual([]); + expect(pluginMessages(report, "src/features/chat/host/rendering-escape.ts")).toEqual( + Array.from({ length: 2 }, () => CHAT_HOST_RENDERING_LAYER_MESSAGE), + ); + expect(pluginDiagnostics(report, "src/features/chat/host/rendering-allowed.ts")).toEqual([]); expect(pluginMessages(report, "src/features/threads-view/workspace-escape.ts")).toEqual([FEATURE_WORKSPACE_BOUNDARY_MESSAGE]); expect(pluginDiagnostics(report, "src/features/threads-view/workspace-allowed.ts")).toEqual([]); expect(pluginMessages(report, "src/workspace/chat-internal-escape.ts")).toEqual([WORKSPACE_CHAT_INTERNAL_MESSAGE]);