From 316806c06946fc34a59b8df41962a594e59b00a0 Mon Sep 17 00:00:00 2001 From: murashit Date: Sun, 28 Jun 2026 16:03:48 +0900 Subject: [PATCH] Add chat app-server gateway wiring --- docs/development.md | 2 +- .../no-app-server-root-module-imports.grit | 5 +- .../chat/app-server/session-gateway.ts | 102 +++++++++++++++ src/features/chat/host/connection-bundle.ts | 2 +- src/features/chat/host/runtime-bundle.ts | 13 +- src/features/chat/host/session-graph.ts | 32 +++-- src/features/chat/host/thread-bundle.ts | 120 +++++------------- src/features/chat/host/turn-bundle.ts | 22 +--- tests/features/chat/host/turn-bundle.test.ts | 7 +- tests/scripts/grit-policy.test.mjs | 10 ++ 10 files changed, 184 insertions(+), 131 deletions(-) create mode 100644 src/features/chat/app-server/session-gateway.ts diff --git a/docs/development.md b/docs/development.md index 78aba317..d3f61c10 100644 --- a/docs/development.md +++ b/docs/development.md @@ -62,7 +62,7 @@ Keep new code near the state or API it owns. A feature may import another featur Generated app-server types should stay behind app-server connection and protocol adapter modules. Chat-local app-server integration modules may consume app-server protocol projections, but not raw generated bindings. If a domain, shared, settings, workspace, or UI module needs app-server payloads, add or reuse a panel-owned projection at the boundary instead of importing generated payload types directly. -Chat application workflows should not import root `src/app-server/` modules or receive `AppServerClient` access directly. Keep app-server client access, connection freshness checks, vault-path injection, and payload projection in `src/features/chat/app-server/` transports or host-owned wiring, then pass chat-owned workflow contracts into application modules. +Chat application workflows should not import root `src/app-server/` modules or receive `AppServerClient` access directly. Keep app-server client access, connection freshness checks, vault-path injection, and payload projection in `src/features/chat/app-server/` transports or host-owned wiring, then pass chat-owned workflow contracts into application modules. Keep session-level adapter composition at the chat app-server boundary so host bundles consume a composed app-server adapter instead of importing each transport factory. Chat panel-visible state belongs in `ChatStateStore` and should flow through named reducer actions and the shell-state adapter. Use Preact Signals only in `src/features/chat/panel/shell-state.tsx`; lint enforces this boundary. When a surface needs fewer dependencies, add or reuse a named shell-state projection instead of importing `@preact/signals` elsewhere. diff --git a/scripts/grit/import-boundaries/no-app-server-root-module-imports.grit b/scripts/grit/import-boundaries/no-app-server-root-module-imports.grit index dcfad34b..bfd708dc 100644 --- a/scripts/grit/import-boundaries/no-app-server-root-module-imports.grit +++ b/scripts/grit/import-boundaries/no-app-server-root-module-imports.grit @@ -8,6 +8,9 @@ private pattern js_module_reference() { } js_module_reference() as $stmt where { - $stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\./)?app-server|(?:\.\./)+app-server|src/app-server)/[^/\"']+[\"']$" }, + $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") } diff --git a/src/features/chat/app-server/session-gateway.ts b/src/features/chat/app-server/session-gateway.ts new file mode 100644 index 00000000..9f620e37 --- /dev/null +++ b/src/features/chat/app-server/session-gateway.ts @@ -0,0 +1,102 @@ +import type { AppServerClient } from "../../../app-server/connection/client"; +import type { AppServerClientAccess } from "../../../app-server/connection/client-access"; +import { readFileBase64 as readAppServerFileBase64 } from "../../../app-server/services/files"; +import { renameThread as renameAppServerThread } from "../../../app-server/services/threads"; +import type { CodexInput } from "../../../domain/chat/input"; +import type { ChatTurnTransport } from "../application/conversation/turn-transport"; +import type { RuntimeSettingsTransport } from "../application/runtime/settings-transport"; +import type { ThreadGoalReadTransport, ThreadGoalTransport } from "../application/threads/goal-transport"; +import type { ThreadHistoryTransport, ThreadResumeTransport } from "../application/threads/thread-loading-transport"; +import type { ThreadMutationTransport } from "../application/threads/thread-mutation-transport"; +import { createChatThreadGoalReadTransport, createChatThreadGoalTransport } from "./goals/transport"; +import { createThreadReferenceResolver, type ThreadReferenceResolver } from "./references/thread-reference-resolver"; +import { createChatRuntimeSettingsTransport } from "./runtime/thread-settings-transport"; +import { createChatThreadHistoryTransport, createChatThreadResumeTransport } from "./threads/loading-transport"; +import { createChatThreadMutationTransport } from "./threads/transport"; +import { createChatTurnTransport } from "./turns/transport"; + +export interface ChatAppServerGatewayHost { + vaultPath: string; + currentClient(): AppServerClient | null; + connectedClient(): Promise; +} + +interface ChatThreadReferenceResolverOptions { + codexInput(text: string): CodexInput; + addSystemMessage(text: string): void; + setStatus(status: string): void; +} + +export interface ChatAppServerGateway { + clientAccess: AppServerClientAccess; + turn: ChatTurnTransport; + runtimeSettings: RuntimeSettingsTransport; + threadHistory: ThreadHistoryTransport; + threadResume: ThreadResumeTransport; + threadMutation: ThreadMutationTransport; + threadGoalRead: ThreadGoalReadTransport; + threadGoal: ThreadGoalTransport; + connectionAvailable(): boolean; + readFileBase64(path: string, options?: { timeoutMs?: number }): Promise; + renameThread(threadId: string, name: string): Promise; + threadReferences(options: ChatThreadReferenceResolverOptions): ThreadReferenceResolver; +} + +export function createChatAppServerGateway(host: ChatAppServerGatewayHost): ChatAppServerGateway { + return { + clientAccess: createCurrentClientAccess(() => host.currentClient()), + turn: createChatTurnTransport(host), + runtimeSettings: createChatRuntimeSettingsTransport(host), + threadHistory: createChatThreadHistoryTransport(host), + threadResume: createChatThreadResumeTransport(host), + threadMutation: createChatThreadMutationTransport(host), + threadGoalRead: createChatThreadGoalReadTransport(host), + threadGoal: createChatThreadGoalTransport(host), + connectionAvailable: () => host.currentClient() !== null, + readFileBase64: (path, options) => readCurrentClientFileBase64(host, path, options), + renameThread: (threadId, name) => renameCurrentClientThread(host, threadId, name), + threadReferences: (options) => + createThreadReferenceResolver({ + currentClient: () => host.currentClient(), + codexInput: (text) => options.codexInput(text), + addSystemMessage: (text) => { + options.addSystemMessage(text); + }, + setStatus: (status) => { + options.setStatus(status); + }, + }), + }; +} + +function createCurrentClientAccess(currentClient: () => AppServerClient | null): AppServerClientAccess { + return { + withClient: async (operation) => { + const client = currentClient(); + if (!client) throw new Error("Codex app-server is not connected."); + const result = await operation(client); + if (currentClient() !== client) { + throw new Error("Codex app-server connection changed while running the operation."); + } + return result; + }, + }; +} + +async function readCurrentClientFileBase64( + host: ChatAppServerGatewayHost, + path: string, + options: { timeoutMs?: number } = {}, +): Promise { + const client = host.currentClient(); + if (!client) return ""; + const data = await readAppServerFileBase64(client, path, options); + return host.currentClient() === client ? data : ""; +} + +async function renameCurrentClientThread(host: ChatAppServerGatewayHost, threadId: string, name: string): Promise { + const client = host.currentClient(); + if (!client) return false; + await renameAppServerThread(client, threadId, name); + return host.currentClient() === client; +} diff --git a/src/features/chat/host/connection-bundle.ts b/src/features/chat/host/connection-bundle.ts index d3999be9..53d5aeb2 100644 --- a/src/features/chat/host/connection-bundle.ts +++ b/src/features/chat/host/connection-bundle.ts @@ -22,7 +22,7 @@ import type { createThreadGoalSyncActions } from "../application/threads/goal-ac import type { ChatPanelEnvironment } from "./contracts"; import type { ChatViewDeferredTasks } from "./deferred-work"; -export type CurrentAppServerClient = () => AppServerClient | null; +type CurrentAppServerClient = () => AppServerClient | null; type RespondRequestId = Parameters[0]; type RejectRequestId = Parameters[0]; diff --git a/src/features/chat/host/runtime-bundle.ts b/src/features/chat/host/runtime-bundle.ts index 9b393102..dd67abaf 100644 --- a/src/features/chat/host/runtime-bundle.ts +++ b/src/features/chat/host/runtime-bundle.ts @@ -1,11 +1,10 @@ import type { ConnectionManager } from "../../../app-server/connection/connection-manager"; -import { createChatRuntimeSettingsTransport } from "../app-server/runtime/thread-settings-transport"; +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 { CurrentAppServerClient } from "./connection-bundle"; import type { ChatPanelEnvironment } from "./contracts"; export type ChatPanelRuntimeSettingsActions = ChatRuntimeSettingsActions; @@ -28,26 +27,24 @@ export function createRuntimeBundle( host: ChatPanelRuntimeHost, input: { connection: ConnectionManager; - currentClient: CurrentAppServerClient; + appServer: ChatAppServerGateway; status: ChatPanelRuntimeStatus; }, ): ChatPanelRuntimeBundle { return { - settings: createSessionRuntimeSettingsActions(host, input.currentClient, input.status), + settings: createSessionRuntimeSettingsActions(host, input.appServer, input.status), projection: createSessionRuntimeProjection(host, input.connection), }; } function createSessionRuntimeSettingsActions( host: ChatPanelRuntimeHost, - currentClient: CurrentAppServerClient, + appServer: ChatAppServerGateway, status: ChatPanelRuntimeStatus, ): ChatPanelRuntimeSettingsActions { return createChatRuntimeSettingsActions({ stateStore: host.stateStore, - runtimeTransport: createChatRuntimeSettingsTransport({ - currentClient, - }), + runtimeTransport: appServer.runtimeSettings, runtimeSnapshotForState: runtimeSnapshotForChatState, collaborationModeLabel: () => collaborationModeLabel(host.stateStore), addSystemMessage: (text) => { diff --git a/src/features/chat/host/session-graph.ts b/src/features/chat/host/session-graph.ts index 566f033f..6faeeca0 100644 --- a/src/features/chat/host/session-graph.ts +++ b/src/features/chat/host/session-graph.ts @@ -2,6 +2,7 @@ import { ConnectionManager } from "../../../app-server/connection/connection-man import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries"; import { createLocalIdSource, type LocalIdSource } from "../../../shared/id/local-id"; import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; +import { createChatAppServerGateway } from "../app-server/session-gateway"; import type { ChatAction, ChatConnectionPhase } from "../application/state/root-reducer"; import type { ChatStateStore } from "../application/state/store"; import type { ActiveThreadIdentitySync } from "../application/threads/active-thread-identity-sync"; @@ -71,6 +72,18 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch const localItemIds = createLocalIdSource(); const connection = createConnectionManager(environment); const currentClient = () => connection.currentClient(); + let ensureConnected: () => Promise = async () => { + throw new Error("Codex app-server connection controller is not initialized."); + }; + const connectedClient = async () => { + await ensureConnected(); + return currentClient(); + }; + const appServer = createChatAppServerGateway({ + vaultPath: environment.plugin.settingsRef.vaultPath, + currentClient, + connectedClient, + }); const status = createSessionStatus(stateStore, localItemIds); const refreshTabHeader = () => { host.environment.view.refreshTabHeader(); @@ -88,7 +101,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch }; const threadFoundation = createThreadFoundation(host, { - currentClient, + appServer, localItemIds, status, refreshLiveState, @@ -120,21 +133,16 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch inboundHandler, } = connectionBundle; const { threads: serverThreads } = connectionBundle.serverActions; - const ensureConnected = () => connectionController.ensureConnected(); - const connectedClient = async () => { - await ensureConnected(); - return currentClient(); - }; + ensureConnected = () => connectionController.ensureConnected(); const refreshActiveThreads = () => connectionController.refreshActiveThreads(); const runtime = createRuntimeBundle(host, { connection, - currentClient, + appServer, status, }); const threadLifecycle = createThreadLifecycleBundle(host, { - currentClient, + appServer, localItemIds, - connectedClient, ensureConnected, status, serverThreads, @@ -148,8 +156,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch runtimeSettings: runtime.settings, }); const threadActions = createThreadActionBundle(host, { - currentClient, - connectedClient, + appServer, status, composerController: composer.controller, foundation: threadFoundation, @@ -161,8 +168,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch connection, localItemIds, ensureConnected, - connectedClient, - currentClient, + appServer, status, inboundHandler, threadLifecycle: threadLifecycle.lifecycle, diff --git a/src/features/chat/host/thread-bundle.ts b/src/features/chat/host/thread-bundle.ts index 237aa80c..08191308 100644 --- a/src/features/chat/host/thread-bundle.ts +++ b/src/features/chat/host/thread-bundle.ts @@ -1,17 +1,12 @@ import { Notice } from "obsidian"; -import type { AppServerClientAccess } from "../../../app-server/connection/client-access"; -import { readFileBase64 } from "../../../app-server/services/files"; import { recoverRolloutTokenUsage } from "../../../app-server/services/rollout-token-usage"; -import { renameThread as renameAppServerThread } from "../../../app-server/services/threads"; 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 { createChatThreadGoalReadTransport, createChatThreadGoalTransport } from "../app-server/goals/transport"; -import { createChatThreadHistoryTransport, createChatThreadResumeTransport } from "../app-server/threads/loading-transport"; -import { createChatThreadMutationTransport } from "../app-server/threads/transport"; +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"; @@ -32,7 +27,6 @@ import { createThreadNavigationActions } from "../application/threads/thread-nav import { threadTitleContextFromMessageStreamItems } from "../application/threads/title-context"; import type { ChatComposerController } from "../panel/composer-controller"; import { createToolbarPanelActions, type ToolbarPanelActions } from "../panel/toolbar-actions"; -import type { CurrentAppServerClient } from "./connection-bundle"; import type { ChatPanelEnvironment } from "./contracts"; type ChatPanelGoalSyncActions = ReturnType; @@ -57,7 +51,7 @@ interface ChatPanelThreadHost { } interface ChatPanelThreadFoundationInput { - currentClient: CurrentAppServerClient; + appServer: ChatAppServerGateway; localItemIds: LocalIdSource; status: ChatPanelThreadStatus; refreshLiveState: () => void; @@ -73,9 +67,8 @@ interface ChatPanelThreadFoundation { } interface ChatPanelThreadLifecycleInput { - currentClient: CurrentAppServerClient; + appServer: ChatAppServerGateway; localItemIds: LocalIdSource; - connectedClient: () => Promise>; ensureConnected: () => Promise; status: ChatPanelThreadStatus; serverThreads: ChatServerThreadActions; @@ -95,8 +88,7 @@ interface ChatPanelThreadLifecycleBundle { } interface ChatPanelThreadActionInput { - currentClient: CurrentAppServerClient; - connectedClient: () => Promise>; + appServer: ChatAppServerGateway; status: ChatPanelThreadStatus; composerController: ChatComposerController; foundation: ChatPanelThreadFoundation; @@ -112,16 +104,16 @@ interface ChatPanelThreadActionBundle { } export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPanelThreadFoundationInput): ChatPanelThreadFoundation { - const { currentClient, localItemIds, status, refreshLiveState } = input; - const titleService = createSessionThreadTitleService(host, currentClient); - const autoTitleCoordinator = createSessionAutoTitleCoordinator(host, currentClient, titleService); - const history = createSessionHistoryController(host, currentClient, status, autoTitleCoordinator); + const { appServer, localItemIds, status, refreshLiveState } = input; + const titleService = createSessionThreadTitleService(host, appServer); + const autoTitleCoordinator = createSessionAutoTitleCoordinator(host, appServer, titleService); + const history = createSessionHistoryController(host, appServer, status, autoTitleCoordinator); const invalidateThreadWork = () => { host.resumeWork.invalidate(); history.invalidate(); }; - const goalSync = createSessionGoalSyncActions(host, currentClient, localItemIds, status, refreshLiveState); - const threadOperations = createSessionThreadOperations(host.environment, currentClient); + const goalSync = createSessionGoalSyncActions(host, appServer, localItemIds, status, refreshLiveState); + const threadOperations = createSessionThreadOperations(host.environment, appServer); return { titleService, @@ -138,9 +130,8 @@ export function createThreadLifecycleBundle( input: ChatPanelThreadLifecycleInput, ): ChatPanelThreadLifecycleBundle { const { - currentClient, + appServer, localItemIds, - connectedClient, ensureConnected, status, serverThreads, @@ -149,7 +140,7 @@ export function createThreadLifecycleBundle( refreshLiveState, notifyActiveThreadIdentityChanged, } = input; - const goals = createSessionGoalActions(host, currentClient, localItemIds, connectedClient, status, serverThreads, refreshLiveState); + const goals = createSessionGoalActions(host, appServer, localItemIds, status, serverThreads, refreshLiveState); const rename = createSessionThreadRenameEditorActions( host.stateStore, foundation.threadOperations, @@ -158,8 +149,7 @@ export function createThreadLifecycleBundle( status, ); const lifecycle = createSessionThreadLifecycle(host, { - currentClient, - connectedClient, + appServer, status, goals, autoTitleCoordinator: foundation.autoTitleCoordinator, @@ -184,16 +174,7 @@ export function createThreadLifecycleBundle( } export function createThreadActionBundle(host: ChatPanelThreadHost, input: ChatPanelThreadActionInput): ChatPanelThreadActionBundle { - const { - currentClient, - connectedClient, - status, - composerController, - foundation, - lifecycle, - refreshActiveThreads, - notifyActiveThreadIdentityChanged, - } = input; + const { appServer, status, composerController, foundation, lifecycle, refreshActiveThreads, notifyActiveThreadIdentityChanged } = input; const { environment, stateStore } = host; const threadManagementHost: ThreadManagementActionsHost = { stateStore, @@ -201,11 +182,7 @@ export function createThreadActionBundle(host: ChatPanelThreadHost, input: ChatP renameThread: (threadId, value) => foundation.threadOperations.renameThread(threadId, value), archiveThread: async (threadId, options) => (await foundation.threadOperations.archiveThread(threadId, options)) !== null, }, - threadTransport: createChatThreadMutationTransport({ - vaultPath: environment.plugin.settingsRef.vaultPath, - currentClient, - connectedClient, - }), + threadTransport: appServer.threadMutation, addSystemMessage: status.addSystemMessage, setStatus: status.set, setComposerText: (text) => { @@ -242,14 +219,14 @@ export function createThreadActionBundle(host: ChatPanelThreadHost, input: ChatP return { actions, toolbarPanelActions, navigation }; } -function createSessionThreadTitleService(host: ChatPanelThreadHost, currentClient: CurrentAppServerClient): ThreadTitleService { +function createSessionThreadTitleService(host: ChatPanelThreadHost, appServer: ChatAppServerGateway): ThreadTitleService { const { environment, stateStore } = host; return createThreadTitleService({ codexPath: () => environment.plugin.settingsRef.settings.codexPath(), vaultPath: environment.plugin.settingsRef.vaultPath, threadNamingModel: () => environment.plugin.settingsRef.settings.threadNamingModel(), threadNamingEffort: () => environment.plugin.settingsRef.settings.threadNamingEffort(), - clientAccess: createCurrentClientAccess(currentClient), + clientAccess: appServer.clientAccess, visibleContext: (threadId) => activeThreadRenameTitleContext(stateStore.getState(), threadId), visibleCompletedTurnContext: (turnId) => threadTitleContextFromMessageStreamItems(turnId, messageStreamItems(stateStore.getState().messageStream)), @@ -258,7 +235,7 @@ function createSessionThreadTitleService(host: ChatPanelThreadHost, currentClien function createSessionAutoTitleCoordinator( host: ChatPanelThreadHost, - currentClient: CurrentAppServerClient, + appServer: ChatAppServerGateway, titleService: ThreadTitleService, ): AutoTitleCoordinator { return createAutoTitleCoordinator({ @@ -268,11 +245,7 @@ function createSessionAutoTitleCoordinator( renameGeneratedTitle: async (threadId, title, options) => { const name = normalizeExplicitThreadName(title); if (!name) return false; - const client = currentClient(); - if (!client) return false; - - await renameAppServerThread(client, threadId, name); - if (currentClient() !== client) return false; + if (!(await appServer.renameThread(threadId, name))) return false; if (options.shouldPublish()) { host.environment.plugin.threadCatalog.apply({ type: "thread-renamed", threadId, name }); } @@ -283,15 +256,13 @@ function createSessionAutoTitleCoordinator( function createSessionHistoryController( host: ChatPanelThreadHost, - currentClient: CurrentAppServerClient, + appServer: ChatAppServerGateway, status: ChatPanelThreadStatus, autoTitleCoordinator: AutoTitleCoordinator, ): HistoryController { return new HistoryController({ stateStore: host.stateStore, - historyTransport: createChatThreadHistoryTransport({ - currentClient, - }), + historyTransport: appServer.threadHistory, addSystemMessage: status.addSystemMessage, showLatestPageAtBottom: () => { host.messageScrollController.showLatest(); @@ -304,16 +275,14 @@ function createSessionHistoryController( function createSessionGoalSyncActions( host: ChatPanelThreadHost, - currentClient: CurrentAppServerClient, + appServer: ChatAppServerGateway, localItemIds: LocalIdSource, status: ChatPanelThreadStatus, refreshLiveState: () => void, ): ChatPanelGoalSyncActions { return createThreadGoalSyncActions({ stateStore: host.stateStore, - goalTransport: createChatThreadGoalReadTransport({ - currentClient, - }), + goalTransport: appServer.threadGoalRead, localItemIds, addSystemMessage: (text) => { status.addSystemMessage(text); @@ -325,9 +294,9 @@ function createSessionGoalSyncActions( }); } -function createSessionThreadOperations(environment: ChatPanelEnvironment, currentClient: CurrentAppServerClient): ThreadOperations { +function createSessionThreadOperations(environment: ChatPanelEnvironment, appServer: ChatAppServerGateway): ThreadOperations { return createThreadOperations({ - clientAccess: createCurrentClientAccess(currentClient), + clientAccess: appServer.clientAccess, archiveExport: { settings: () => environment.plugin.settingsRef.settings.archiveExportSettings(), enabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled(), @@ -342,35 +311,17 @@ function createSessionThreadOperations(environment: ChatPanelEnvironment, curren }); } -function createCurrentClientAccess(currentClient: CurrentAppServerClient): AppServerClientAccess { - return { - withClient: async (operation) => { - const client = currentClient(); - if (!client) throw new Error("Codex app-server is not connected."); - const result = await operation(client); - if (currentClient() !== client) { - throw new Error("Codex app-server connection changed while running the operation."); - } - return result; - }, - }; -} - function createSessionGoalActions( host: ChatPanelThreadHost, - currentClient: CurrentAppServerClient, + appServer: ChatAppServerGateway, localItemIds: LocalIdSource, - connectedClient: () => Promise>, status: ChatPanelThreadStatus, serverThreads: ChatServerThreadActions, refreshLiveState: () => void, ): ChatPanelGoalActions { return createGoalActions({ stateStore: host.stateStore, - goalTransport: createChatThreadGoalTransport({ - currentClient, - connectedClient, - }), + goalTransport: appServer.threadGoal, localItemIds, startThread: (preview, options) => serverThreads.startThread(preview, options), addSystemMessage: (text) => { @@ -402,8 +353,7 @@ function createSessionThreadRenameEditorActions( function createSessionThreadLifecycle( host: ChatPanelThreadHost, input: { - currentClient: CurrentAppServerClient; - connectedClient: () => Promise>; + appServer: ChatAppServerGateway; status: ChatPanelThreadStatus; goals: ChatPanelGoalActions; autoTitleCoordinator: AutoTitleCoordinator; @@ -415,8 +365,7 @@ function createSessionThreadLifecycle( }, ): ChatPanelThreadLifecycle { const { - currentClient, - connectedClient, + appServer, status, goals, autoTitleCoordinator, @@ -428,21 +377,14 @@ function createSessionThreadLifecycle( } = input; return createThreadLifecycleParts({ stateStore: host.stateStore, - resumeTransport: createChatThreadResumeTransport({ - vaultPath: host.environment.plugin.settingsRef.vaultPath, - currentClient, - connectedClient, - }), + resumeTransport: appServer.threadResume, lifecycle: { resumeWork: host.resumeWork, history, invalidateThreadWork, getClosing: host.getClosing, recoverTokenUsageFromRollout: (path) => - recoverRolloutTokenUsage(path, async (filePath, options) => { - const client = currentClient(); - return client ? readFileBase64(client, filePath, options) : ""; - }), + recoverRolloutTokenUsage(path, (filePath, options) => appServer.readFileBase64(filePath, options)), }, thread: { notifyIdentityChanged: notifyActiveThreadIdentityChanged, diff --git a/src/features/chat/host/turn-bundle.ts b/src/features/chat/host/turn-bundle.ts index ec34d483..29f3367c 100644 --- a/src/features/chat/host/turn-bundle.ts +++ b/src/features/chat/host/turn-bundle.ts @@ -3,8 +3,7 @@ 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 { createThreadReferenceResolver } from "../app-server/references/thread-reference-resolver"; -import { createChatTurnTransport } from "../app-server/turns/transport"; +import type { ChatAppServerGateway } from "../app-server/session-gateway"; import { type ChatReconnectActionsHost, reconnectPanel } from "../application/connection/reconnect-actions"; import { type ConversationTurnActions as ChatPanelConversationTurnActions, @@ -17,7 +16,6 @@ import type { AutoTitleCoordinator } from "../application/threads/auto-title-coo 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 { ChatPanelRuntimeSettingsActions } from "./runtime-bundle"; @@ -54,8 +52,7 @@ interface ChatPanelTurnInput { connection: ConnectionManager; localItemIds: LocalIdSource; ensureConnected: () => Promise; - connectedClient: () => Promise>; - currentClient: CurrentAppServerClient; + appServer: ChatAppServerGateway; status: ChatPanelTurnStatus; inboundHandler: ChatInboundHandler; threadLifecycle: ChatPanelThreadLifecycle; @@ -78,8 +75,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn connection, localItemIds, ensureConnected, - connectedClient, - currentClient, + appServer, status, inboundHandler, threadLifecycle, @@ -129,13 +125,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn }, }; const reconnect = () => reconnectPanel(reconnectHost); - const turnTransport = createChatTurnTransport({ - vaultPath: host.environment.plugin.settingsRef.vaultPath, - currentClient, - connectedClient, - }); - const threadReferenceResolver = createThreadReferenceResolver({ - currentClient, + const threadReferenceResolver = appServer.threadReferences({ codexInput: (text) => composerController.codexInput(text), addSystemMessage: status.addSystemMessage, setStatus: status.set, @@ -144,8 +134,8 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn { stateStore: host.stateStore, localItemIds, - connectionAvailable: () => currentClient() !== null, - turnTransport, + connectionAvailable: () => appServer.connectionAvailable(), + turnTransport: appServer.turn, referThread: (thread, message) => threadReferenceResolver.referThread(thread, message), status, runtime: { diff --git a/tests/features/chat/host/turn-bundle.test.ts b/tests/features/chat/host/turn-bundle.test.ts index e1d5e415..f8816beb 100644 --- a/tests/features/chat/host/turn-bundle.test.ts +++ b/tests/features/chat/host/turn-bundle.test.ts @@ -69,8 +69,11 @@ function turnBundleFixture(options: { stateStore?: ReturnType "local-id") }, ensureConnected: vi.fn().mockResolvedValue(undefined), - connectedClient: vi.fn().mockResolvedValue({}), - currentClient: vi.fn(() => ({})), + appServer: { + connectionAvailable: vi.fn(() => true), + threadReferences: vi.fn(() => ({ referThread: vi.fn() })), + turn: { ensureConnected: vi.fn().mockResolvedValue(true) }, + }, status, inboundHandler: {}, threadLifecycle: { diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index c8d848ea..665462fb 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -866,6 +866,14 @@ export type Escape = AppServerClient; import { listThreads } from "../../../../app-server/threads"; export const read = listThreads; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/chat/host/chat-app-server-root-import.ts"), + ` +import { createChatAppServerGateway } from "../app-server/session-gateway"; + +export const gateway = createChatAppServerGateway; `.trimStart(), ); await writeFile( @@ -891,6 +899,7 @@ export const read = listThreads; [ "src/app-server/escape.ts", "src/features/chat/app-server/root-import.ts", + "src/features/chat/host/chat-app-server-root-import.ts", "src/app-server/services/root-import.ts", "src/app-server/services/allowed.ts", ], @@ -899,6 +908,7 @@ 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(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([]); });