Add chat app-server gateway wiring

This commit is contained in:
murashit 2026-06-28 16:03:48 +09:00
parent 1f95937dcf
commit 316806c069
10 changed files with 184 additions and 131 deletions

View file

@ -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.

View file

@ -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")
}

View file

@ -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<AppServerClient | null>;
}
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<string>;
renameThread(threadId: string, name: string): Promise<boolean>;
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<string> {
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<boolean> {
const client = host.currentClient();
if (!client) return false;
await renameAppServerThread(client, threadId, name);
return host.currentClient() === client;
}

View file

@ -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<AppServerClient["respondToServerRequest"]>[0];
type RejectRequestId = Parameters<AppServerClient["rejectServerRequest"]>[0];

View file

@ -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) => {

View file

@ -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<void> = 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,

View file

@ -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<typeof createThreadGoalSyncActions>;
@ -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<ReturnType<CurrentAppServerClient>>;
ensureConnected: () => Promise<void>;
status: ChatPanelThreadStatus;
serverThreads: ChatServerThreadActions;
@ -95,8 +88,7 @@ interface ChatPanelThreadLifecycleBundle {
}
interface ChatPanelThreadActionInput {
currentClient: CurrentAppServerClient;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
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<ReturnType<CurrentAppServerClient>>,
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<ReturnType<CurrentAppServerClient>>;
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,

View file

@ -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<void>;
connectedClient: () => Promise<ReturnType<CurrentAppServerClient>>;
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: {

View file

@ -69,8 +69,11 @@ function turnBundleFixture(options: { stateStore?: ReturnType<typeof createChatS
connection: { resetConnection: vi.fn() },
localItemIds: { next: vi.fn(() => "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: {

View file

@ -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([]);
});