refactor(chat): split current and connected app-server gateways

This commit is contained in:
murashit 2026-07-16 18:50:26 +09:00
parent f7e5319c3e
commit a099e1fffc
6 changed files with 94 additions and 33 deletions

View file

@ -5,12 +5,22 @@ import type { CodexInput } from "../../../domain/chat/input";
import type { ComposerInputSnapshot } from "../application/composer/input-snapshot";
import { createThreadReferenceResolver, type ThreadReferenceResolver } from "./thread-reference-resolver";
import { type ChatMetadataTransports, createChatMetadataTransports } from "./transports/metadata-transports";
import { type ChatSessionTransports, createChatSessionTransports } from "./transports/session-transports";
import {
type ChatConnectedSessionTransports,
type ChatCurrentSessionTransports,
createChatConnectedSessionTransports,
createChatCurrentSessionTransports,
} from "./transports/session-transports";
export interface ChatAppServerGatewayHost {
export interface ChatCurrentAppServerGatewayHost {
codexPath(): string;
vaultPath: string;
currentClient(): AppServerClient | null;
}
export interface ChatConnectedAppServerGatewayHost {
vaultPath: string;
currentClient(): AppServerClient | null;
connectedClient(): Promise<AppServerClient | null>;
}
@ -20,17 +30,19 @@ interface ChatThreadReferenceResolverOptions {
setStatus(status: string): void;
}
export interface ChatAppServerGateway extends ChatSessionTransports, ChatMetadataTransports {
export interface ChatCurrentAppServerGateway extends ChatCurrentSessionTransports, ChatMetadataTransports {
clientAccess: AppServerClientAccess;
connectionAvailable(): boolean;
readFileBase64(path: string, options?: { timeoutMs?: number }): Promise<string | null>;
threadReferences(options: ChatThreadReferenceResolverOptions): ThreadReferenceResolver;
}
export function createChatAppServerGateway(host: ChatAppServerGatewayHost): ChatAppServerGateway {
export interface ChatAppServerGateway extends ChatCurrentAppServerGateway, ChatConnectedSessionTransports {}
export function createChatCurrentAppServerGateway(host: ChatCurrentAppServerGatewayHost): ChatCurrentAppServerGateway {
return {
clientAccess: createCurrentClientAccess(host),
...createChatSessionTransports(host),
...createChatCurrentSessionTransports(host),
...createChatMetadataTransports(host),
connectionAvailable: () => host.currentClient() !== null,
readFileBase64: (path, options) => readCurrentClientFileBase64(host, path, options),
@ -48,7 +60,17 @@ export function createChatAppServerGateway(host: ChatAppServerGatewayHost): Chat
};
}
function createCurrentClientAccess(host: ChatAppServerGatewayHost): AppServerClientAccess {
export function createChatAppServerGateway(
currentGateway: ChatCurrentAppServerGateway,
host: ChatConnectedAppServerGatewayHost,
): ChatAppServerGateway {
return {
...currentGateway,
...createChatConnectedSessionTransports(host),
};
}
function createCurrentClientAccess(host: ChatCurrentAppServerGatewayHost): AppServerClientAccess {
return {
withClient: async (operation, options: AppServerClientAccessOptions = {}) => {
if (options.serverRequests?.kind === "reject") {
@ -67,7 +89,7 @@ function createCurrentClientAccess(host: ChatAppServerGatewayHost): AppServerCli
}
async function readCurrentClientFileBase64(
host: ChatAppServerGatewayHost,
host: ChatCurrentAppServerGatewayHost,
path: string,
options: { timeoutMs?: number } = {},
): Promise<string | null> {

View file

@ -42,42 +42,52 @@ interface ConnectedChatAppServerClientHost extends CurrentChatAppServerClientHos
connectedClient(): Promise<AppServerClient | null>;
}
interface ChatAppServerTransportHost extends ConnectedChatAppServerClientHost {
interface ChatCurrentAppServerTransportHost extends CurrentChatAppServerClientHost {
vaultPath: string;
}
interface ChatAppServerTransportHost extends ConnectedChatAppServerClientHost, ChatCurrentAppServerTransportHost {}
interface AppServerThreadTurnsPage {
readonly data: ThreadTurnsPage["turns"];
readonly nextCursor: string | null;
}
export interface ChatSessionTransports {
readonly turn: ChatTurnTransport;
export interface ChatCurrentSessionTransports {
readonly runtimeSettings: RuntimeSettingsTransport;
readonly threadStart: ThreadStartTransport;
readonly threadHistory: ThreadHistoryTransport;
readonly threadGoalRead: ThreadGoalReadTransport;
}
export interface ChatConnectedSessionTransports {
readonly turn: ChatTurnTransport;
readonly threadResume: ThreadResumeTransport;
readonly threadMutation: ThreadMutationTransport;
readonly threadEphemeral: EphemeralThreadTransport;
readonly threadGoalRead: ThreadGoalReadTransport;
readonly threadGoal: ThreadGoalTransport;
}
export function createChatSessionTransports(host: ChatAppServerTransportHost): ChatSessionTransports {
export function createChatCurrentSessionTransports(host: ChatCurrentAppServerTransportHost): ChatCurrentSessionTransports {
return {
turn: createChatTurnTransport(host),
runtimeSettings: createChatRuntimeSettingsTransport(host),
threadStart: createChatThreadStartTransport(host),
threadHistory: createChatThreadHistoryTransport(host),
threadGoalRead: createChatThreadGoalReadTransport(host),
};
}
export function createChatConnectedSessionTransports(host: ChatAppServerTransportHost): ChatConnectedSessionTransports {
return {
turn: createChatTurnTransport(host),
threadResume: createChatThreadResumeTransport(host),
threadMutation: createChatThreadMutationTransport(host),
threadEphemeral: createChatEphemeralThreadTransport(host),
threadGoalRead: createChatThreadGoalReadTransport(host),
threadGoal: createChatThreadGoalTransport(host),
};
}
function createChatThreadStartTransport(host: ChatAppServerTransportHost): ThreadStartTransport {
function createChatThreadStartTransport(host: ChatCurrentAppServerTransportHost): ThreadStartTransport {
return {
startThread: (request) =>
withCurrentChatAppServerClient(host, async (client) => {

View file

@ -5,8 +5,8 @@ import { type ConnectionManager, StaleConnectionError } from "../../../../app-se
import { isStaleAppServerSharedQueryContextError } from "../../../../app-server/query/shared-queries";
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
import { type ChatInboundHandler, createChatInboundHandler } from "../../app-server/inbound/handler";
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
import { type ChatConnectionActions, createChatConnectionActions } from "../../application/connection/connection-actions";
import type { ServerDiagnosticsTransport } from "../../application/connection/metadata-transport";
import { createServerDiagnosticsActions } from "../../application/connection/server-diagnostics-actions";
import { createServerMetadataActions } from "../../application/connection/server-metadata-actions";
import type { LocalIdSource } from "../../application/local-id-source";
@ -26,7 +26,7 @@ interface ChatPanelConnectionStatus {
interface ChatPanelConnectionBundleInput {
connection: ConnectionManager;
appServer: ChatAppServerGateway;
diagnosticsTransport: ServerDiagnosticsTransport;
localItemIds: LocalIdSource;
status: ChatPanelConnectionStatus;
autoTitleCoordinator: AutoTitleCoordinator;
@ -120,7 +120,7 @@ export function createConnectionBundle(
input: ChatPanelConnectionBundleInput,
): ChatPanelConnectionBundle {
const { environment, stateStore } = host;
const { connection, appServer, localItemIds, status, autoTitleCoordinator } = input;
const { connection, diagnosticsTransport, localItemIds, status, autoTitleCoordinator } = input;
const serverRequestResponders = createServerRequestResponderRegistry();
const serverMetadata = createServerMetadataActions({
stateStore,
@ -132,7 +132,7 @@ export function createConnectionBundle(
});
const serverDiagnostics = createServerDiagnosticsActions({
stateStore,
diagnosticsTransport: appServer.serverDiagnostics,
diagnosticsTransport,
appServerMetadataSnapshot: () => environment.plugin.appServerQueries.appServerMetadataSnapshot(),
});
const refreshSharedThreads = async (): Promise<void> => {

View file

@ -4,7 +4,7 @@ import { recoverRolloutTokenUsage } from "../../../../app-server/services/rollou
import { createThreadOperationsTransport, createThreadTitleTransport } from "../../../threads/app-server/workflow-transports";
import { createThreadOperations, type ThreadOperations } from "../../../threads/workflows/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../../../threads/workflows/thread-title-service";
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
import type { ChatAppServerGateway, ChatCurrentAppServerGateway } from "../../app-server/session-gateway";
import type { LocalIdSource } from "../../application/local-id-source";
import type { ChatStateStore } from "../../application/state/store";
import { threadStreamItems } from "../../application/state/thread-stream";
@ -56,7 +56,7 @@ interface ChatPanelThreadHost {
}
interface ChatPanelThreadFoundationInput {
appServer: ChatAppServerGateway;
appServer: ChatCurrentAppServerGateway;
localItemIds: LocalIdSource;
status: ChatPanelThreadStatus;
refreshLiveState: () => void;

View file

@ -1,7 +1,7 @@
import { codexPanelAppServerInitializeParams } from "../../../app-server/connection/client-profile";
import { ConnectionManager } from "../../../app-server/connection/connection-manager";
import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries";
import { createChatAppServerGateway } from "../app-server/session-gateway";
import { createChatAppServerGateway, createChatCurrentAppServerGateway } from "../app-server/session-gateway";
import { reconnectPanel } from "../application/connection/reconnect-actions";
import { createLocalIdSource, type LocalIdSource } from "../application/local-id-source";
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
@ -114,15 +114,10 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
const localItemIds = createLocalIdSource();
const connection = createConnectionManager(environment);
const currentClient = () => connection.currentClient();
let ensureConnected = (): Promise<void> => Promise.reject(new Error("Codex app-server connection actions are not initialized."));
const appServer = createChatAppServerGateway({
const currentAppServer = createChatCurrentAppServerGateway({
codexPath: () => environment.plugin.settingsRef.settings.codexPath(),
vaultPath: environment.plugin.settingsRef.vaultPath,
currentClient,
connectedClient: async () => {
await ensureConnected();
return currentClient();
},
});
const status = createSessionStatus(stateStore, localItemIds);
const refreshTabHeader = () => {
@ -141,7 +136,7 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
};
const threadFoundation = createThreadFoundation(host, {
appServer,
appServer: currentAppServer,
localItemIds,
status,
refreshLiveState,
@ -160,7 +155,7 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
},
{
connection,
appServer,
diagnosticsTransport: currentAppServer.serverDiagnostics,
localItemIds,
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
status,
@ -170,7 +165,15 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
connection: { actions: connectionActions },
inboundHandler,
} = connectionBundle;
ensureConnected = () => connectionActions.ensureConnected();
const ensureConnected = () => connectionActions.ensureConnected();
const appServer = createChatAppServerGateway(currentAppServer, {
vaultPath: environment.plugin.settingsRef.vaultPath,
currentClient,
connectedClient: async () => {
await connectionActions.ensureConnected();
return currentClient();
},
});
const refreshActiveThreads = () => connectionActions.refreshActiveThreads();
const runtime = createRuntimeBundle(host, {
connection,

View file

@ -6,7 +6,7 @@ import type { ThreadRecord } from "../../../../src/app-server/protocol/thread";
import type { TurnItem, TurnRecord } from "../../../../src/app-server/protocol/turn";
import type { CodexInput } from "../../../../src/domain/chat/input";
import { createServerDiagnostics, diagnosticProbeOk } from "../../../../src/domain/server/diagnostics";
import { createChatAppServerGateway } from "../../../../src/features/chat/app-server/session-gateway";
import { createChatAppServerGateway, createChatCurrentAppServerGateway } from "../../../../src/features/chat/app-server/session-gateway";
import { createThreadReferenceResolver } from "../../../../src/features/chat/app-server/thread-reference-resolver";
import { preparedUserInputWithWikiLinkMentionsSkillsAndContext } from "../../../../src/features/chat/application/composer/wikilink-context";
import { deferred } from "../../../support/async";
@ -14,6 +14,28 @@ import { deferred } from "../../../support/async";
const textInput = (text: string): CodexInput => [{ type: "text", text }];
describe("chat app-server transports", () => {
it("adds connection-requiring capabilities only after the connected client host exists", async () => {
const client = { request: vi.fn() } as unknown as AppServerClient;
const currentGateway = createChatCurrentAppServerGateway({
codexPath: () => "codex",
vaultPath: "/vault",
currentClient: () => client,
});
const connectedClient = vi.fn().mockResolvedValue(client);
expect(currentGateway.connectionAvailable()).toBe(true);
expect(currentGateway).not.toHaveProperty("turn");
const gateway = createChatAppServerGateway(currentGateway, {
vaultPath: "/vault",
currentClient: () => client,
connectedClient,
});
await expect(gateway.turn.ensureConnected()).resolves.toBe(true);
expect(connectedClient).toHaveBeenCalledOnce();
});
it("starts threads with the session vault path and projects activation snapshots", async () => {
const request = vi.fn().mockResolvedValue(threadStartResponse("thread"));
const client = { request } as unknown as AppServerClient;
@ -521,10 +543,14 @@ function createTestGateway(options: {
connectedClient?: () => Promise<AppServerClient | null>;
}) {
const codexPath = options.codexPath;
return createChatAppServerGateway({
const currentGateway = createChatCurrentAppServerGateway({
codexPath: typeof codexPath === "function" ? codexPath : () => codexPath ?? "codex",
vaultPath: options.vaultPath ?? "/vault",
currentClient: options.currentClient,
});
return createChatAppServerGateway(currentGateway, {
vaultPath: options.vaultPath ?? "/vault",
currentClient: options.currentClient,
connectedClient: options.connectedClient ?? (async () => options.currentClient()),
});
}