mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Move chat app-server actions to owned workflows
This commit is contained in:
parent
3f2c4fd970
commit
d81044c17b
24 changed files with 1177 additions and 1177 deletions
|
|
@ -1,123 +0,0 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import { readRateLimitMetadataProbe } from "../../../../app-server/query/metadata-probes";
|
||||
import { listModelMetadata } from "../../../../app-server/services/catalog";
|
||||
import { readToolInventory } from "../../../../app-server/services/tool-inventory";
|
||||
import {
|
||||
cloneServerDiagnostics,
|
||||
type DiagnosticProbeId,
|
||||
type DiagnosticProbeResult,
|
||||
diagnosticProbeError,
|
||||
diagnosticProbeOk,
|
||||
diagnosticsWithProbe,
|
||||
diagnosticsWithToolInventory,
|
||||
upsertMcpServerStatusDiagnostics,
|
||||
} from "../../../../domain/server/diagnostics";
|
||||
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
|
||||
import { type ChatServerActionsHost, captureChatServerClientScope } from "./host";
|
||||
|
||||
interface RefreshServerDiagnosticsOptions {
|
||||
appServerMetadataSnapshot?: boolean;
|
||||
forceResourceProbes?: boolean;
|
||||
}
|
||||
|
||||
interface DiagnosticProbeSnapshot {
|
||||
probe: DiagnosticProbeResult;
|
||||
}
|
||||
|
||||
export interface ChatServerDiagnosticsActionsHost extends ChatServerActionsHost {
|
||||
updateAppServerMetadata: (updater: (metadata: SharedServerMetadata | null) => SharedServerMetadata | null) => SharedServerMetadata | null;
|
||||
appServerMetadataSnapshot: () => SharedServerMetadata | null;
|
||||
}
|
||||
|
||||
export interface ChatServerDiagnosticsActions {
|
||||
refreshServerDiagnostics: (options?: RefreshServerDiagnosticsOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createChatServerDiagnosticsActions(host: ChatServerDiagnosticsActionsHost): ChatServerDiagnosticsActions {
|
||||
return {
|
||||
refreshServerDiagnostics: async (options) => {
|
||||
await refreshServerDiagnostics(host, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshServerDiagnostics(
|
||||
host: ChatServerDiagnosticsActionsHost,
|
||||
options: RefreshServerDiagnosticsOptions = {},
|
||||
): Promise<boolean> {
|
||||
const scope = captureChatServerClientScope(host);
|
||||
if (!scope.client) return false;
|
||||
const client = scope.client;
|
||||
|
||||
const initialDiagnostics = currentMetadataDiagnostics(host);
|
||||
const state = host.stateStore.getState();
|
||||
const activeThreadId = state.activeThread.id;
|
||||
const metadataSnapshot = host.appServerMetadataSnapshot();
|
||||
const cachedSkills =
|
||||
options.forceResourceProbes === true ? undefined : (metadataSnapshot?.availableSkills ?? state.connection.availableSkills);
|
||||
const cachedSkillsProbe =
|
||||
options.forceResourceProbes === true
|
||||
? undefined
|
||||
: (metadataSnapshot?.serverDiagnostics.probes.skills ?? state.connection.serverDiagnostics.probes.skills);
|
||||
const toolInventory = readToolInventory(client, host.vaultPath, {
|
||||
threadId: activeThreadId,
|
||||
mcpDiagnostics: initialDiagnostics.mcpServers,
|
||||
...(cachedSkills !== undefined ? { cachedSkills } : {}),
|
||||
...(cachedSkillsProbe !== undefined ? { cachedSkillsProbe } : {}),
|
||||
});
|
||||
const probes: Promise<DiagnosticProbeSnapshot>[] = [];
|
||||
if (options.forceResourceProbes === true && options.appServerMetadataSnapshot !== true) {
|
||||
probes.push(
|
||||
probeDiagnostic(
|
||||
"models",
|
||||
() => listModelMetadata(client),
|
||||
(models) => `${String(models.length)} models`,
|
||||
),
|
||||
readRateLimitDiagnosticProbe(client),
|
||||
);
|
||||
}
|
||||
|
||||
const [results, toolInventoryResult] = await Promise.all([Promise.all(probes), toolInventory]);
|
||||
if (scope.isStale()) return false;
|
||||
|
||||
let diagnostics = currentMetadataDiagnostics(host);
|
||||
for (const result of results) {
|
||||
diagnostics = diagnosticsWithProbe(diagnostics, result.probe);
|
||||
}
|
||||
for (const probe of toolInventoryResult.probes) {
|
||||
diagnostics = diagnosticsWithProbe(diagnostics, probe);
|
||||
}
|
||||
if (toolInventoryResult.mcpServerStatuses) {
|
||||
diagnostics = upsertMcpServerStatusDiagnostics(diagnostics, toolInventoryResult.mcpServerStatuses);
|
||||
}
|
||||
diagnostics = diagnosticsWithToolInventory(diagnostics, toolInventoryResult.inventory);
|
||||
host.updateAppServerMetadata((metadata) => (metadata ? { ...metadata, serverDiagnostics: diagnostics } : null));
|
||||
host.stateStore.dispatch({ type: "connection/metadata-applied", serverDiagnostics: diagnostics });
|
||||
return true;
|
||||
}
|
||||
|
||||
async function readRateLimitDiagnosticProbe(client: AppServerClient): Promise<DiagnosticProbeSnapshot> {
|
||||
const result = await readRateLimitMetadataProbe(client);
|
||||
return { probe: result.probe };
|
||||
}
|
||||
|
||||
function currentMetadataDiagnostics(host: ChatServerDiagnosticsActionsHost): SharedServerMetadata["serverDiagnostics"] {
|
||||
return (
|
||||
host.appServerMetadataSnapshot()?.serverDiagnostics ?? cloneServerDiagnostics(host.stateStore.getState().connection.serverDiagnostics)
|
||||
);
|
||||
}
|
||||
|
||||
async function probeDiagnostic<T>(
|
||||
id: DiagnosticProbeId,
|
||||
request: () => Promise<T>,
|
||||
summarize: (response: T) => string | null,
|
||||
): Promise<DiagnosticProbeSnapshot> {
|
||||
try {
|
||||
const response = await request();
|
||||
return {
|
||||
probe: diagnosticProbeOk(id, summarize(response), Date.now()),
|
||||
};
|
||||
} catch (error) {
|
||||
return { probe: diagnosticProbeError(id, error, Date.now()) };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
|
||||
export interface ChatServerActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
vaultPath: string;
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
export interface ChatServerClientScope {
|
||||
client: AppServerClient | null;
|
||||
isStale: () => boolean;
|
||||
}
|
||||
|
||||
export function captureChatServerClientScope(host: ChatServerActionsHost): ChatServerClientScope {
|
||||
const client = host.currentClient();
|
||||
return {
|
||||
client,
|
||||
isStale: () => client !== null && host.currentClient() !== client,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import {
|
||||
startThread as startAppServerThread,
|
||||
threadActivationSnapshotFromAppServerResponse,
|
||||
} from "../../../../app-server/services/threads";
|
||||
import { runtimeConfigOrDefault } from "../../../../domain/runtime/config";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { ThreadCatalogEvent } from "../../../threads/catalog/thread-catalog";
|
||||
import { resumedThreadAction } from "../../application/state/actions";
|
||||
import type { ChatState } from "../../application/state/root-reducer";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
import { permissionProfileRequestForThreadStart, serviceTierRequestForThreadStart } from "../../domain/runtime/thread-settings-patch";
|
||||
import { type ChatServerActionsHost, captureChatServerClientScope } from "./host";
|
||||
|
||||
interface StartedThreadSummary {
|
||||
threadId: string;
|
||||
}
|
||||
|
||||
export interface ChatServerThreadActionsHost extends ChatServerActionsHost {
|
||||
runtimeSnapshotForState: (state: ChatState) => RuntimeSnapshot;
|
||||
applyThreadCatalogEvent: (event: ThreadCatalogEvent) => void;
|
||||
syncThreadGoal: (threadId: string) => void;
|
||||
}
|
||||
|
||||
export interface ChatServerThreadActions {
|
||||
applyThreadList: (threads: readonly Thread[]) => void;
|
||||
startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<StartedThreadSummary | null>;
|
||||
}
|
||||
|
||||
export function createChatServerThreadActions(host: ChatServerThreadActionsHost): ChatServerThreadActions {
|
||||
return {
|
||||
applyThreadList: (threads) => {
|
||||
applyThreadList(host, threads);
|
||||
},
|
||||
startThread: (preview, options) => startThread(host, preview, options),
|
||||
};
|
||||
}
|
||||
|
||||
function applyThreadList(host: ChatServerThreadActionsHost, threads: readonly Thread[]): void {
|
||||
host.stateStore.dispatch({ type: "thread-list/applied", threads, threadsLoaded: true });
|
||||
}
|
||||
|
||||
async function startThread(
|
||||
host: ChatServerThreadActionsHost,
|
||||
preview?: string,
|
||||
options: { syncGoal?: boolean } = {},
|
||||
): Promise<StartedThreadSummary | null> {
|
||||
const scope = captureChatServerClientScope(host);
|
||||
if (!scope.client) return null;
|
||||
const requestState = host.stateStore.getState();
|
||||
const runtimeSnapshot = host.runtimeSnapshotForState(requestState);
|
||||
const runtimeConfig = runtimeConfigOrDefault(requestState.connection.runtimeConfig);
|
||||
const serviceTier = serviceTierRequestForThreadStart(runtimeSnapshot, runtimeConfig);
|
||||
const permissions = permissionProfileRequestForThreadStart(runtimeSnapshot, runtimeConfig);
|
||||
const response = await startAppServerThread(scope.client, { cwd: host.vaultPath, serviceTier, permissions });
|
||||
if (scope.isStale()) return null;
|
||||
const state = host.stateStore.getState();
|
||||
const fallbackPreview = preview?.trim();
|
||||
const activationResponse =
|
||||
response.thread.preview.trim().length > 0 || !fallbackPreview
|
||||
? response
|
||||
: { ...response, thread: { ...response.thread, preview: fallbackPreview } };
|
||||
const action = resumedThreadAction({
|
||||
response: threadActivationSnapshotFromAppServerResponse(activationResponse),
|
||||
listedThreads: state.threadList.listedThreads,
|
||||
preserveRequestedRuntimeSettings: requestState.activeThread.id === null,
|
||||
});
|
||||
host.stateStore.dispatch(action);
|
||||
host.applyThreadCatalogEvent({ type: "thread-started", thread: action.thread });
|
||||
if (options.syncGoal ?? true) host.syncThreadGoal(response.thread.id);
|
||||
return { threadId: response.thread.id };
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "../../../../domain/pending-requests/model";
|
||||
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
|
||||
import type { ThreadCatalogEvent } from "../../../threads/catalog/thread-catalog";
|
||||
import type { AppServerResourceEvent } from "../../application/connection/server-metadata-actions";
|
||||
import { activeTurnId } from "../../application/conversation/turn-state";
|
||||
import type { LocalIdSource } from "../../application/local-id-source";
|
||||
import type { ChatAction, ChatState } from "../../application/state/root-reducer";
|
||||
|
|
@ -26,7 +27,6 @@ import {
|
|||
createMcpElicitationResultItem,
|
||||
createUserInputResultItem,
|
||||
} from "../../domain/pending-requests/result-items";
|
||||
import type { AppServerResourceEvent } from "../actions/metadata";
|
||||
import { classifyAppServerLog } from "./app-server-logs";
|
||||
import { type ChatNotificationEffect, planChatNotification } from "./notification-plan";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { threadFromAppServerRecord } from "../../../../app-server/services/threa
|
|||
import { threadTokenUsageFromRuntimeUsage } from "../../../../domain/runtime/metrics";
|
||||
import { normalizeExplicitThreadName } from "../../../../domain/threads/model";
|
||||
import type { ThreadCatalogEvent } from "../../../threads/catalog/thread-catalog";
|
||||
import type { AppServerResourceEvent } from "../../application/connection/server-metadata-actions";
|
||||
import { type ConversationRuntimeOutcome, planConversationRuntimeEvents } from "../../application/conversation/runtime-event-plan";
|
||||
import { activeThreadSettingsAppliedAction } from "../../application/state/actions";
|
||||
import type { ChatAction, ChatState } from "../../application/state/root-reducer";
|
||||
import { goalChangeItem } from "../../domain/message-stream/factories/goal-items";
|
||||
import type { AppServerResourceEvent } from "../actions/metadata";
|
||||
import { type DiagnosticStatusNotification, routeServerNotification, type ThreadLifecycleNotification } from "./notification-routing";
|
||||
import { conversationRuntimeEventsFromNotification } from "./runtime-events";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { withShortLivedAppServerClient } from "../../../app-server/connection/sh
|
|||
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";
|
||||
|
||||
export interface ChatAppServerGatewayHost {
|
||||
|
|
@ -19,7 +20,7 @@ interface ChatThreadReferenceResolverOptions {
|
|||
setStatus(status: string): void;
|
||||
}
|
||||
|
||||
export interface ChatAppServerGateway extends ChatSessionTransports {
|
||||
export interface ChatAppServerGateway extends ChatSessionTransports, ChatMetadataTransports {
|
||||
clientAccess: AppServerClientAccess;
|
||||
connectionAvailable(): boolean;
|
||||
readFileBase64(path: string, options?: { timeoutMs?: number }): Promise<string>;
|
||||
|
|
@ -30,6 +31,7 @@ export function createChatAppServerGateway(host: ChatAppServerGatewayHost): Chat
|
|||
return {
|
||||
clientAccess: createCurrentClientAccess(host),
|
||||
...createChatSessionTransports(host),
|
||||
...createChatMetadataTransports(host),
|
||||
connectionAvailable: () => host.currentClient() !== null,
|
||||
readFileBase64: (path, options) => readCurrentClientFileBase64(host, path, options),
|
||||
threadReferences: (options) =>
|
||||
|
|
|
|||
106
src/features/chat/app-server/transports/metadata-transports.ts
Normal file
106
src/features/chat/app-server/transports/metadata-transports.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import { readRateLimitMetadataProbe, readSkillMetadataProbe } from "../../../../app-server/query/metadata-probes";
|
||||
import { listModelMetadata } from "../../../../app-server/services/catalog";
|
||||
import type { AppServerRequestClient } from "../../../../app-server/services/request-client";
|
||||
import { readToolInventory } from "../../../../app-server/services/tool-inventory";
|
||||
import type { DiagnosticProbeId, DiagnosticProbeResult } from "../../../../domain/server/diagnostics";
|
||||
import { diagnosticProbeError, diagnosticProbeOk } from "../../../../domain/server/diagnostics";
|
||||
import type {
|
||||
MetadataResourceTransport,
|
||||
ServerDiagnosticsSnapshot,
|
||||
ServerDiagnosticsTransport,
|
||||
} from "../../application/connection/metadata-transport";
|
||||
|
||||
interface CurrentChatAppServerClientHost {
|
||||
currentClient(): AppServerClient | null;
|
||||
}
|
||||
|
||||
interface ChatAppServerMetadataTransportHost extends CurrentChatAppServerClientHost {
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export interface ChatMetadataTransports {
|
||||
readonly metadataResource: MetadataResourceTransport;
|
||||
readonly serverDiagnostics: ServerDiagnosticsTransport;
|
||||
}
|
||||
|
||||
interface DiagnosticProbeSnapshot {
|
||||
probe: DiagnosticProbeResult;
|
||||
}
|
||||
|
||||
export function createChatMetadataTransports(host: ChatAppServerMetadataTransportHost): ChatMetadataTransports {
|
||||
return {
|
||||
metadataResource: createChatMetadataResourceTransport(host),
|
||||
serverDiagnostics: createChatServerDiagnosticsTransport(host),
|
||||
};
|
||||
}
|
||||
|
||||
function createChatMetadataResourceTransport(host: ChatAppServerMetadataTransportHost): MetadataResourceTransport {
|
||||
return {
|
||||
readSkillMetadata: (forceReload) =>
|
||||
withCapturedChatAppServerClient(host, (client) => readSkillMetadataProbe(client, host.vaultPath, forceReload)),
|
||||
readRateLimitMetadata: () => withCapturedChatAppServerClient(host, (client) => readRateLimitMetadataProbe(client)),
|
||||
};
|
||||
}
|
||||
|
||||
function createChatServerDiagnosticsTransport(host: ChatAppServerMetadataTransportHost): ServerDiagnosticsTransport {
|
||||
return {
|
||||
readServerDiagnostics: async (request): Promise<ServerDiagnosticsSnapshot | null> => {
|
||||
const client = host.currentClient();
|
||||
if (!client) return null;
|
||||
const toolInventory = readToolInventory(client, host.vaultPath, {
|
||||
threadId: request.threadId,
|
||||
mcpDiagnostics: request.initialDiagnostics.mcpServers,
|
||||
...(request.cachedSkills !== undefined ? { cachedSkills: request.cachedSkills } : {}),
|
||||
...(request.cachedSkillsProbe !== undefined ? { cachedSkillsProbe: request.cachedSkillsProbe } : {}),
|
||||
});
|
||||
const probes: Promise<DiagnosticProbeSnapshot>[] = [];
|
||||
if (request.forceResourceProbes && !request.appServerMetadataSnapshot) {
|
||||
probes.push(
|
||||
probeDiagnostic(
|
||||
"models",
|
||||
() => listModelMetadata(client),
|
||||
(models) => `${String(models.length)} models`,
|
||||
),
|
||||
readRateLimitDiagnosticProbe(client),
|
||||
);
|
||||
}
|
||||
|
||||
const [resourceProbes, toolInventoryResult] = await Promise.all([Promise.all(probes), toolInventory]);
|
||||
if (host.currentClient() !== client) return null;
|
||||
return {
|
||||
resourceProbes: resourceProbes.map((result) => result.probe),
|
||||
toolInventory: toolInventoryResult,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withCapturedChatAppServerClient<T>(
|
||||
host: CurrentChatAppServerClientHost,
|
||||
operation: (client: AppServerClient | null) => Promise<T>,
|
||||
): Promise<T | null> {
|
||||
const client = host.currentClient();
|
||||
const result = await operation(client);
|
||||
return client !== null && host.currentClient() !== client ? null : result;
|
||||
}
|
||||
|
||||
async function readRateLimitDiagnosticProbe(client: AppServerRequestClient): Promise<DiagnosticProbeSnapshot> {
|
||||
const result = await readRateLimitMetadataProbe(client);
|
||||
return { probe: result.probe };
|
||||
}
|
||||
|
||||
async function probeDiagnostic<T>(
|
||||
id: DiagnosticProbeId,
|
||||
request: () => Promise<T>,
|
||||
summarize: (response: T) => string | null,
|
||||
): Promise<DiagnosticProbeSnapshot> {
|
||||
try {
|
||||
const response = await request();
|
||||
return {
|
||||
probe: diagnosticProbeOk(id, summarize(response), Date.now()),
|
||||
};
|
||||
} catch (error) {
|
||||
return { probe: diagnosticProbeError(id, error, Date.now()) };
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
resumeThread,
|
||||
rollbackThread,
|
||||
setThreadGoal,
|
||||
startThread,
|
||||
threadActivationSnapshotFromAppServerResponse,
|
||||
updateThreadSettings,
|
||||
} from "../../../../app-server/services/threads";
|
||||
|
|
@ -26,6 +27,7 @@ import type {
|
|||
ThreadResumeTransport,
|
||||
} from "../../application/threads/thread-loading-transport";
|
||||
import type { ThreadMutationTransport, ThreadRollbackSnapshot } from "../../application/threads/thread-mutation-transport";
|
||||
import type { ThreadStartTransport } from "../../application/threads/thread-start-transport";
|
||||
import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items";
|
||||
|
||||
interface CurrentChatAppServerClientHost {
|
||||
|
|
@ -51,6 +53,7 @@ interface AppServerThreadTurnsPage {
|
|||
export interface ChatSessionTransports {
|
||||
readonly turn: ChatTurnTransport;
|
||||
readonly runtimeSettings: RuntimeSettingsTransport;
|
||||
readonly threadStart: ThreadStartTransport;
|
||||
readonly threadHistory: ThreadHistoryTransport;
|
||||
readonly threadResume: ThreadResumeTransport;
|
||||
readonly threadMutation: ThreadMutationTransport;
|
||||
|
|
@ -62,6 +65,7 @@ export function createChatSessionTransports(host: ChatAppServerTransportHost): C
|
|||
return {
|
||||
turn: createChatTurnTransport(host),
|
||||
runtimeSettings: createChatRuntimeSettingsTransport(host),
|
||||
threadStart: createChatThreadStartTransport(host),
|
||||
threadHistory: createChatThreadHistoryTransport(host),
|
||||
threadResume: createChatThreadResumeTransport(host),
|
||||
threadMutation: createChatThreadMutationTransport(host),
|
||||
|
|
@ -70,6 +74,20 @@ export function createChatSessionTransports(host: ChatAppServerTransportHost): C
|
|||
};
|
||||
}
|
||||
|
||||
function createChatThreadStartTransport(host: ChatAppServerTransportHost): ThreadStartTransport {
|
||||
return {
|
||||
startThread: (request) =>
|
||||
withCurrentChatAppServerClient(host, async (client) => {
|
||||
const response = await startThread(client, {
|
||||
cwd: host.vaultPath,
|
||||
serviceTier: request.serviceTier,
|
||||
permissions: request.permissions,
|
||||
});
|
||||
return threadActivationSnapshotFromAppServerResponse(response);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createChatTurnTransport(host: ChatAppServerTransportHost): ChatTurnTransport {
|
||||
return {
|
||||
ensureConnected: async () => (await host.connectedClient()) !== null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import type { SkillMetadata } from "../../../../domain/catalog/metadata";
|
||||
import type { RateLimitSnapshot } from "../../../../domain/runtime/metrics";
|
||||
import type { DiagnosticProbeResult, Diagnostics } from "../../../../domain/server/diagnostics";
|
||||
import type { McpServerStatusSummary } from "../../../../domain/server/mcp-status";
|
||||
import type { ToolInventorySnapshot } from "../../../../domain/server/tool-inventory";
|
||||
|
||||
interface MetadataProbeResult<T, K extends keyof Diagnostics["probes"]> {
|
||||
value: T;
|
||||
probe: Diagnostics["probes"][K];
|
||||
}
|
||||
|
||||
type SkillMetadataProbeResult = MetadataProbeResult<readonly SkillMetadata[], "skills">;
|
||||
export type RateLimitMetadataProbeResult = MetadataProbeResult<RateLimitSnapshot | null, "rateLimits">;
|
||||
|
||||
export interface MetadataResourceTransport {
|
||||
readSkillMetadata(forceReload?: boolean): Promise<SkillMetadataProbeResult | null>;
|
||||
readRateLimitMetadata(): Promise<RateLimitMetadataProbeResult | null>;
|
||||
}
|
||||
|
||||
interface ServerDiagnosticsReadRequest {
|
||||
threadId: string | null;
|
||||
initialDiagnostics: Diagnostics;
|
||||
cachedSkills?: readonly SkillMetadata[];
|
||||
cachedSkillsProbe?: DiagnosticProbeResult;
|
||||
forceResourceProbes: boolean;
|
||||
appServerMetadataSnapshot: boolean;
|
||||
}
|
||||
|
||||
export interface ServerDiagnosticsSnapshot {
|
||||
resourceProbes: readonly DiagnosticProbeResult[];
|
||||
toolInventory: {
|
||||
inventory: ToolInventorySnapshot;
|
||||
probes: readonly DiagnosticProbeResult[];
|
||||
mcpServerStatuses: readonly McpServerStatusSummary[] | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServerDiagnosticsTransport {
|
||||
readServerDiagnostics(request: ServerDiagnosticsReadRequest): Promise<ServerDiagnosticsSnapshot | null>;
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
cloneServerDiagnostics,
|
||||
diagnosticsWithProbe,
|
||||
diagnosticsWithToolInventory,
|
||||
upsertMcpServerStatusDiagnostics,
|
||||
} from "../../../../domain/server/diagnostics";
|
||||
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import type { ServerDiagnosticsTransport } from "./metadata-transport";
|
||||
|
||||
interface RefreshServerDiagnosticsOptions {
|
||||
appServerMetadataSnapshot?: boolean;
|
||||
forceResourceProbes?: boolean;
|
||||
}
|
||||
|
||||
export interface ServerDiagnosticsActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
diagnosticsTransport: ServerDiagnosticsTransport;
|
||||
updateAppServerMetadata: (updater: (metadata: SharedServerMetadata | null) => SharedServerMetadata | null) => SharedServerMetadata | null;
|
||||
appServerMetadataSnapshot: () => SharedServerMetadata | null;
|
||||
}
|
||||
|
||||
export interface ServerDiagnosticsActions {
|
||||
refreshServerDiagnostics: (options?: RefreshServerDiagnosticsOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createServerDiagnosticsActions(host: ServerDiagnosticsActionsHost): ServerDiagnosticsActions {
|
||||
return {
|
||||
refreshServerDiagnostics: async (options) => {
|
||||
await refreshServerDiagnostics(host, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshServerDiagnostics(
|
||||
host: ServerDiagnosticsActionsHost,
|
||||
options: RefreshServerDiagnosticsOptions = {},
|
||||
): Promise<boolean> {
|
||||
const initialDiagnostics = currentMetadataDiagnostics(host);
|
||||
const state = host.stateStore.getState();
|
||||
const activeThreadId = state.activeThread.id;
|
||||
const metadataSnapshot = host.appServerMetadataSnapshot();
|
||||
const cachedSkills =
|
||||
options.forceResourceProbes === true ? undefined : (metadataSnapshot?.availableSkills ?? state.connection.availableSkills);
|
||||
const cachedSkillsProbe =
|
||||
options.forceResourceProbes === true
|
||||
? undefined
|
||||
: (metadataSnapshot?.serverDiagnostics.probes.skills ?? state.connection.serverDiagnostics.probes.skills);
|
||||
const request = {
|
||||
threadId: activeThreadId,
|
||||
initialDiagnostics,
|
||||
forceResourceProbes: options.forceResourceProbes === true,
|
||||
appServerMetadataSnapshot: options.appServerMetadataSnapshot === true,
|
||||
...(cachedSkills !== undefined ? { cachedSkills } : {}),
|
||||
...(cachedSkillsProbe !== undefined ? { cachedSkillsProbe } : {}),
|
||||
};
|
||||
const snapshot = await host.diagnosticsTransport.readServerDiagnostics(request);
|
||||
if (!snapshot) return false;
|
||||
|
||||
let diagnostics = currentMetadataDiagnostics(host);
|
||||
for (const probe of snapshot.resourceProbes) {
|
||||
diagnostics = diagnosticsWithProbe(diagnostics, probe);
|
||||
}
|
||||
for (const probe of snapshot.toolInventory.probes) {
|
||||
diagnostics = diagnosticsWithProbe(diagnostics, probe);
|
||||
}
|
||||
if (snapshot.toolInventory.mcpServerStatuses) {
|
||||
diagnostics = upsertMcpServerStatusDiagnostics(diagnostics, snapshot.toolInventory.mcpServerStatuses);
|
||||
}
|
||||
diagnostics = diagnosticsWithToolInventory(diagnostics, snapshot.toolInventory.inventory);
|
||||
host.updateAppServerMetadata((metadata) => (metadata ? { ...metadata, serverDiagnostics: diagnostics } : null));
|
||||
host.stateStore.dispatch({ type: "connection/metadata-applied", serverDiagnostics: diagnostics });
|
||||
return true;
|
||||
}
|
||||
|
||||
function currentMetadataDiagnostics(host: ServerDiagnosticsActionsHost): SharedServerMetadata["serverDiagnostics"] {
|
||||
return (
|
||||
host.appServerMetadataSnapshot()?.serverDiagnostics ?? cloneServerDiagnostics(host.stateStore.getState().connection.serverDiagnostics)
|
||||
);
|
||||
}
|
||||
|
|
@ -1,32 +1,30 @@
|
|||
import {
|
||||
type RateLimitMetadataProbeResult,
|
||||
readRateLimitMetadataProbe,
|
||||
readSkillMetadataProbe,
|
||||
} from "../../../../app-server/query/metadata-probes";
|
||||
import { isStaleAppServerSharedQueryContextError } from "../../../../app-server/query/shared-queries";
|
||||
import { cloneServerDiagnostics, diagnosticsWithProbe, upsertMcpServerDiagnostic } from "../../../../domain/server/diagnostics";
|
||||
import type { McpServerStartupStatus } from "../../../../domain/server/mcp-status";
|
||||
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
|
||||
import { type ChatServerActionsHost, captureChatServerClientScope } from "./host";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import type { MetadataResourceTransport, RateLimitMetadataProbeResult } from "./metadata-transport";
|
||||
|
||||
export type AppServerResourceEvent =
|
||||
| { type: "skills-changed"; forceReload: boolean }
|
||||
| { type: "rate-limits-updated"; preserveExistingOnFailure?: boolean }
|
||||
| { type: "mcp-startup-status-updated"; name: string; status: McpServerStartupStatus; message: string | null };
|
||||
|
||||
export interface ChatServerMetadataActionsHost extends ChatServerActionsHost {
|
||||
export interface ServerMetadataActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
metadataResourceTransport: MetadataResourceTransport;
|
||||
updateAppServerMetadata: (updater: (metadata: SharedServerMetadata | null) => SharedServerMetadata | null) => SharedServerMetadata | null;
|
||||
appServerMetadataSnapshot: () => SharedServerMetadata | null;
|
||||
refreshAppServerMetadata: (options?: { forceSkills?: boolean }) => Promise<SharedServerMetadata | null>;
|
||||
isStaleSharedQueryError: (error: unknown) => boolean;
|
||||
}
|
||||
|
||||
export interface ChatServerMetadataActions {
|
||||
export interface ServerMetadataActions {
|
||||
applyAppServerMetadata: (metadata: SharedServerMetadata) => void;
|
||||
refreshAppServerMetadata: () => Promise<SharedServerMetadata | null>;
|
||||
applyAppServerResourceEvent: (event: AppServerResourceEvent) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createChatServerMetadataActions(host: ChatServerMetadataActionsHost): ChatServerMetadataActions {
|
||||
export function createServerMetadataActions(host: ServerMetadataActionsHost): ServerMetadataActions {
|
||||
return {
|
||||
applyAppServerMetadata: (metadata) => {
|
||||
applyAppServerMetadata(host, metadata);
|
||||
|
|
@ -38,7 +36,7 @@ export function createChatServerMetadataActions(host: ChatServerMetadataActionsH
|
|||
};
|
||||
}
|
||||
|
||||
async function applyAppServerResourceEvent(host: ChatServerMetadataActionsHost, event: AppServerResourceEvent): Promise<void> {
|
||||
async function applyAppServerResourceEvent(host: ServerMetadataActionsHost, event: AppServerResourceEvent): Promise<void> {
|
||||
switch (event.type) {
|
||||
case "skills-changed":
|
||||
await refreshSkillResource(host, event.forceReload);
|
||||
|
|
@ -55,7 +53,7 @@ async function applyAppServerResourceEvent(host: ChatServerMetadataActionsHost,
|
|||
}
|
||||
}
|
||||
|
||||
function applyAppServerMetadata(host: ChatServerMetadataActionsHost, metadata: SharedServerMetadata): void {
|
||||
function applyAppServerMetadata(host: ServerMetadataActionsHost, metadata: SharedServerMetadata): void {
|
||||
host.stateStore.dispatch({
|
||||
type: "connection/metadata-applied",
|
||||
runtimeConfig: metadata.runtimeConfig,
|
||||
|
|
@ -67,12 +65,12 @@ function applyAppServerMetadata(host: ChatServerMetadataActionsHost, metadata: S
|
|||
});
|
||||
}
|
||||
|
||||
async function refreshAppServerMetadata(host: ChatServerMetadataActionsHost): Promise<SharedServerMetadata | null> {
|
||||
async function refreshAppServerMetadata(host: ServerMetadataActionsHost): Promise<SharedServerMetadata | null> {
|
||||
let metadata: SharedServerMetadata | null;
|
||||
try {
|
||||
metadata = await host.refreshAppServerMetadata();
|
||||
} catch (error) {
|
||||
if (isStaleAppServerSharedQueryContextError(error)) return null;
|
||||
if (host.isStaleSharedQueryError(error)) return null;
|
||||
throw error;
|
||||
}
|
||||
if (!metadata) return null;
|
||||
|
|
@ -80,15 +78,14 @@ async function refreshAppServerMetadata(host: ChatServerMetadataActionsHost): Pr
|
|||
return metadata;
|
||||
}
|
||||
|
||||
function applyCurrentAppServerMetadataSnapshot(host: ChatServerMetadataActionsHost): void {
|
||||
function applyCurrentAppServerMetadataSnapshot(host: ServerMetadataActionsHost): void {
|
||||
const metadata = host.appServerMetadataSnapshot();
|
||||
if (metadata) applyAppServerMetadata(host, metadata);
|
||||
}
|
||||
|
||||
async function refreshSkillResource(host: ChatServerMetadataActionsHost, forceReload = false): Promise<SharedServerMetadata | null> {
|
||||
const scope = captureChatServerClientScope(host);
|
||||
const skills = await readSkillMetadataProbe(scope.client, host.vaultPath, forceReload);
|
||||
if (scope.isStale()) return null;
|
||||
async function refreshSkillResource(host: ServerMetadataActionsHost, forceReload = false): Promise<SharedServerMetadata | null> {
|
||||
const skills = await host.metadataResourceTransport.readSkillMetadata(forceReload);
|
||||
if (!skills) return null;
|
||||
const next = host.updateAppServerMetadata((metadata) => {
|
||||
if (!metadata) return null;
|
||||
return {
|
||||
|
|
@ -115,12 +112,11 @@ async function refreshSkillResource(host: ChatServerMetadataActionsHost, forceRe
|
|||
}
|
||||
|
||||
async function refreshRateLimitResource(
|
||||
host: ChatServerMetadataActionsHost,
|
||||
host: ServerMetadataActionsHost,
|
||||
options: { preserveExistingOnFailure?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const scope = captureChatServerClientScope(host);
|
||||
const rateLimit = await readRateLimitMetadataProbe(scope.client);
|
||||
if (scope.isStale()) return;
|
||||
const rateLimit = await host.metadataResourceTransport.readRateLimitMetadata();
|
||||
if (!rateLimit) return;
|
||||
const preserveExistingOnFailure = options.preserveExistingOnFailure === true;
|
||||
const next = updateRateLimitMetadata(host, rateLimit, { preserveRateLimitOnFailure: preserveExistingOnFailure });
|
||||
if (next) {
|
||||
|
|
@ -140,7 +136,7 @@ async function refreshRateLimitResource(
|
|||
}
|
||||
|
||||
function updateRateLimitMetadata(
|
||||
host: ChatServerMetadataActionsHost,
|
||||
host: ServerMetadataActionsHost,
|
||||
rateLimit: RateLimitMetadataProbeResult,
|
||||
options: { preserveRateLimitOnFailure: boolean },
|
||||
): SharedServerMetadata | null {
|
||||
|
|
@ -156,7 +152,7 @@ function updateRateLimitMetadata(
|
|||
}
|
||||
|
||||
function applyMcpStartupStatusEvent(
|
||||
host: ChatServerMetadataActionsHost,
|
||||
host: ServerMetadataActionsHost,
|
||||
name: string,
|
||||
startupStatus: McpServerStartupStatus,
|
||||
message: string | null,
|
||||
|
|
@ -175,7 +171,7 @@ function applyMcpStartupStatusEvent(
|
|||
});
|
||||
}
|
||||
|
||||
function currentMetadataDiagnostics(host: ChatServerMetadataActionsHost): SharedServerMetadata["serverDiagnostics"] {
|
||||
function currentMetadataDiagnostics(host: ServerMetadataActionsHost): SharedServerMetadata["serverDiagnostics"] {
|
||||
return (
|
||||
host.appServerMetadataSnapshot()?.serverDiagnostics ?? cloneServerDiagnostics(host.stateStore.getState().connection.serverDiagnostics)
|
||||
);
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { runtimeConfigOrDefault } from "../../../../domain/runtime/config";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
||||
import { permissionProfileRequestForThreadStart, serviceTierRequestForThreadStart } from "../../domain/runtime/thread-settings-patch";
|
||||
import { resumedThreadAction } from "../state/actions";
|
||||
import type { ChatState } from "../state/root-reducer";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import type { ThreadStartTransport } from "./thread-start-transport";
|
||||
|
||||
interface StartedThreadSummary {
|
||||
threadId: string;
|
||||
}
|
||||
|
||||
export interface ThreadStartActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
threadStartTransport: ThreadStartTransport;
|
||||
runtimeSnapshotForState: (state: ChatState) => RuntimeSnapshot;
|
||||
recordStartedThread: (thread: Thread) => void;
|
||||
syncThreadGoal: (threadId: string) => void;
|
||||
}
|
||||
|
||||
export interface ThreadStartActions {
|
||||
startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<StartedThreadSummary | null>;
|
||||
}
|
||||
|
||||
export function createThreadStartActions(host: ThreadStartActionsHost): ThreadStartActions {
|
||||
return {
|
||||
startThread: (preview, options) => startThread(host, preview, options),
|
||||
};
|
||||
}
|
||||
|
||||
async function startThread(
|
||||
host: ThreadStartActionsHost,
|
||||
preview?: string,
|
||||
options: { syncGoal?: boolean } = {},
|
||||
): Promise<StartedThreadSummary | null> {
|
||||
const requestState = host.stateStore.getState();
|
||||
const runtimeSnapshot = host.runtimeSnapshotForState(requestState);
|
||||
const runtimeConfig = runtimeConfigOrDefault(requestState.connection.runtimeConfig);
|
||||
const activation = await host.threadStartTransport.startThread({
|
||||
serviceTier: serviceTierRequestForThreadStart(runtimeSnapshot, runtimeConfig),
|
||||
permissions: permissionProfileRequestForThreadStart(runtimeSnapshot, runtimeConfig),
|
||||
});
|
||||
if (!activation) return null;
|
||||
|
||||
const state = host.stateStore.getState();
|
||||
const fallbackPreview = preview?.trim();
|
||||
const thread =
|
||||
activation.thread.preview.trim().length > 0 || !fallbackPreview
|
||||
? activation.thread
|
||||
: { ...activation.thread, preview: fallbackPreview };
|
||||
const patchedActivation = thread === activation.thread ? activation : { ...activation, thread };
|
||||
const action = resumedThreadAction({
|
||||
response: patchedActivation,
|
||||
listedThreads: state.threadList.listedThreads,
|
||||
preserveRequestedRuntimeSettings: requestState.activeThread.id === null,
|
||||
});
|
||||
host.stateStore.dispatch(action);
|
||||
host.recordStartedThread(action.thread);
|
||||
if (options.syncGoal ?? true) host.syncThreadGoal(action.thread.id);
|
||||
return { threadId: action.thread.id };
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import type { RuntimeServiceTierRequest, RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
|
||||
import type { ThreadActivationSnapshot } from "../../../../domain/threads/activation";
|
||||
|
||||
interface ThreadStartRequest {
|
||||
serviceTier?: RuntimeServiceTierRequest;
|
||||
permissions?: RuntimeSettingsPatch["permissions"];
|
||||
}
|
||||
|
||||
export interface ThreadStartTransport {
|
||||
startThread(request: ThreadStartRequest): Promise<ThreadActivationSnapshot | null>;
|
||||
}
|
||||
|
|
@ -3,22 +3,21 @@ import { Notice } from "obsidian";
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import { type ConnectionManager, StaleConnectionError } from "../../../../app-server/connection/connection-manager";
|
||||
import { isStaleAppServerSharedQueryContextError } from "../../../../app-server/query/shared-queries";
|
||||
import { type ChatServerDiagnosticsActions, createChatServerDiagnosticsActions } from "../../app-server/actions/diagnostics";
|
||||
import { type ChatServerMetadataActions, createChatServerMetadataActions } from "../../app-server/actions/metadata";
|
||||
import { type ChatServerThreadActions, createChatServerThreadActions } from "../../app-server/actions/threads";
|
||||
import type { 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,
|
||||
handleChatConnectionExit,
|
||||
} from "../../application/connection/connection-actions";
|
||||
import type { ConnectionWorkTracker } from "../../application/connection/connection-work";
|
||||
import { createServerDiagnosticsActions } from "../../application/connection/server-diagnostics-actions";
|
||||
import { createServerMetadataActions } from "../../application/connection/server-metadata-actions";
|
||||
import type { LocalIdSource } from "../../application/local-id-source";
|
||||
import { runtimeSnapshotForChatState } from "../../application/runtime/snapshot";
|
||||
import type { ChatConnectionPhase } from "../../application/state/root-reducer";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { AutoTitleCoordinator } from "../../application/threads/auto-title-coordinator";
|
||||
import type { createThreadGoalSyncActions } from "../../application/threads/goal-actions";
|
||||
import type { ChatPanelEnvironment } from "../contracts";
|
||||
import type { ChatViewDeferredTasks } from "../session/deferred-work";
|
||||
|
||||
|
|
@ -26,7 +25,6 @@ type CurrentAppServerClient = () => AppServerClient | null;
|
|||
|
||||
type RespondRequestId = Parameters<AppServerClient["respondToServerRequest"]>[0];
|
||||
type RejectRequestId = Parameters<AppServerClient["rejectServerRequest"]>[0];
|
||||
type ChatPanelGoalSyncActions = ReturnType<typeof createThreadGoalSyncActions>;
|
||||
|
||||
interface ChatPanelConnectionStatus {
|
||||
set: (statusText: string, phase?: ChatConnectionPhase) => void;
|
||||
|
|
@ -36,9 +34,9 @@ interface ChatPanelConnectionStatus {
|
|||
interface ChatPanelConnectionBundleInput {
|
||||
connection: ConnectionManager;
|
||||
currentClient: CurrentAppServerClient;
|
||||
appServer: ChatAppServerGateway;
|
||||
localItemIds: LocalIdSource;
|
||||
status: ChatPanelConnectionStatus;
|
||||
goalSync: ChatPanelGoalSyncActions;
|
||||
autoTitleCoordinator: AutoTitleCoordinator;
|
||||
}
|
||||
|
||||
|
|
@ -59,10 +57,8 @@ export interface ChatPanelConnectionBundle {
|
|||
actions: ChatConnectionActions;
|
||||
};
|
||||
inboundHandler: ChatInboundHandler;
|
||||
serverActions: {
|
||||
threads: ChatServerThreadActions;
|
||||
metadata: ChatServerMetadataActions;
|
||||
diagnostics: ChatServerDiagnosticsActions;
|
||||
sharedStateActions: {
|
||||
applyAppServerMetadata: (metadata: SharedServerMetadata) => void;
|
||||
};
|
||||
refreshSharedThreads: () => Promise<void>;
|
||||
}
|
||||
|
|
@ -113,37 +109,24 @@ export function createConnectionBundle(
|
|||
input: ChatPanelConnectionBundleInput,
|
||||
): ChatPanelConnectionBundle {
|
||||
const { environment, stateStore } = host;
|
||||
const { connection, currentClient, localItemIds, status, goalSync, autoTitleCoordinator } = input;
|
||||
const serverMetadata = createChatServerMetadataActions({
|
||||
const { connection, currentClient, appServer, localItemIds, status, autoTitleCoordinator } = input;
|
||||
const serverMetadata = createServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: environment.plugin.settingsRef.vaultPath,
|
||||
currentClient,
|
||||
metadataResourceTransport: appServer.metadataResource,
|
||||
updateAppServerMetadata: (updater) => environment.plugin.appServerQueries.updateAppServerMetadata(updater),
|
||||
appServerMetadataSnapshot: () => environment.plugin.appServerQueries.appServerMetadataSnapshot(),
|
||||
refreshAppServerMetadata: (options) => environment.plugin.appServerQueries.refreshAppServerMetadata(options),
|
||||
isStaleSharedQueryError: isStaleAppServerSharedQueryContextError,
|
||||
});
|
||||
const serverDiagnostics = createChatServerDiagnosticsActions({
|
||||
const serverDiagnostics = createServerDiagnosticsActions({
|
||||
stateStore,
|
||||
vaultPath: environment.plugin.settingsRef.vaultPath,
|
||||
currentClient,
|
||||
diagnosticsTransport: appServer.serverDiagnostics,
|
||||
updateAppServerMetadata: (updater) => environment.plugin.appServerQueries.updateAppServerMetadata(updater),
|
||||
appServerMetadataSnapshot: () => environment.plugin.appServerQueries.appServerMetadataSnapshot(),
|
||||
});
|
||||
const serverThreads = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: environment.plugin.settingsRef.vaultPath,
|
||||
currentClient,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent: (event) => {
|
||||
environment.plugin.threadCatalog.apply(event);
|
||||
},
|
||||
syncThreadGoal: (threadId) => {
|
||||
void goalSync.syncThreadGoal(threadId);
|
||||
},
|
||||
});
|
||||
const refreshSharedThreads = async (): Promise<void> => {
|
||||
const threads = await environment.plugin.threadCatalog.refreshActive();
|
||||
serverThreads.applyThreadList(threads);
|
||||
stateStore.dispatch({ type: "thread-list/applied", threads, threadsLoaded: true });
|
||||
};
|
||||
const refreshSharedThreadsQuietly = (): void => {
|
||||
void refreshSharedThreads().catch((error: unknown) => {
|
||||
|
|
@ -258,10 +241,10 @@ export function createConnectionBundle(
|
|||
actions: connectionActions,
|
||||
},
|
||||
inboundHandler,
|
||||
serverActions: {
|
||||
threads: serverThreads,
|
||||
metadata: serverMetadata,
|
||||
diagnostics: serverDiagnostics,
|
||||
sharedStateActions: {
|
||||
applyAppServerMetadata: (metadata) => {
|
||||
serverMetadata.applyAppServerMetadata(metadata);
|
||||
},
|
||||
},
|
||||
refreshSharedThreads,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Notice } from "obsidian";
|
|||
import { recoverRolloutTokenUsage } from "../../../../app-server/services/rollout-token-usage";
|
||||
import { createThreadOperations, type ThreadOperations } from "../../../threads/workflows/thread-operations";
|
||||
import { createThreadTitleService, type ThreadTitleService } from "../../../threads/workflows/thread-title-service";
|
||||
import type { ChatServerThreadActions } from "../../app-server/actions/threads";
|
||||
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
|
||||
import type { LocalIdSource } from "../../application/local-id-source";
|
||||
import { messageStreamItems } from "../../application/state/message-stream";
|
||||
|
|
@ -22,6 +21,7 @@ import { createResumeActions, type ResumeActions } from "../../application/threa
|
|||
import type { ChatResumeWorkTracker } from "../../application/threads/resume-work";
|
||||
import { createThreadManagementActions, type ThreadManagementActionsHost } from "../../application/threads/thread-management-actions";
|
||||
import { createThreadNavigationActions } from "../../application/threads/thread-navigation-actions";
|
||||
import type { ThreadStartActions } from "../../application/threads/thread-start-actions";
|
||||
import { threadTitleContextFromMessageStreamItems } from "../../application/threads/title-context";
|
||||
import type { ChatComposerController } from "../../panel/composer-controller";
|
||||
import { createToolbarPanelActions, type ToolbarPanelActions } from "../../panel/toolbar-actions";
|
||||
|
|
@ -75,7 +75,7 @@ interface ChatPanelThreadLifecycleInput {
|
|||
localItemIds: LocalIdSource;
|
||||
ensureConnected: () => Promise<void>;
|
||||
status: ChatPanelThreadStatus;
|
||||
serverThreads: ChatServerThreadActions;
|
||||
threadStart: ThreadStartActions;
|
||||
foundation: ChatPanelThreadFoundation;
|
||||
refreshTabHeader: () => void;
|
||||
refreshLiveState: () => void;
|
||||
|
|
@ -188,7 +188,7 @@ export function createThreadLifecycleBundle(
|
|||
localItemIds,
|
||||
ensureConnected,
|
||||
status,
|
||||
serverThreads,
|
||||
threadStart,
|
||||
foundation,
|
||||
refreshTabHeader,
|
||||
refreshLiveState,
|
||||
|
|
@ -198,7 +198,7 @@ export function createThreadLifecycleBundle(
|
|||
stateStore: host.stateStore,
|
||||
goalTransport: appServer.threadGoal,
|
||||
localItemIds,
|
||||
startThread: (preview, options) => serverThreads.startThread(preview, options),
|
||||
startThread: (preview, options) => threadStart.startThread(preview, options),
|
||||
addSystemMessage: (text) => {
|
||||
status.addSystemMessage(text);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { ChatServerThreadActions } from "../../app-server/actions/threads";
|
||||
import type { ChatInboundHandler } from "../../app-server/inbound/handler";
|
||||
import type { ChatAppServerGateway } from "../../app-server/session-gateway";
|
||||
import {
|
||||
|
|
@ -9,6 +8,7 @@ import type { LocalIdSource } from "../../application/local-id-source";
|
|||
import { createPendingRequestActions, type PendingRequestActions } from "../../application/pending-requests/pending-request-actions";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { AutoTitleCoordinator } from "../../application/threads/auto-title-coordinator";
|
||||
import type { ThreadStartActions } from "../../application/threads/thread-start-actions";
|
||||
import type { MessageStreamNoticeSection } from "../../domain/message-stream/items";
|
||||
import type { ChatComposerController } from "../../panel/composer-controller";
|
||||
import type { ChatPanelRuntimeProjection } from "../../panel/runtime-status-projection";
|
||||
|
|
@ -51,7 +51,7 @@ interface ChatPanelTurnInput {
|
|||
navigation: ChatPanelThreadNavigationActions;
|
||||
composerController: ChatComposerController;
|
||||
runtimeSettings: ChatPanelRuntimeSettingsActions;
|
||||
serverThreads: ChatServerThreadActions;
|
||||
threadStart: ThreadStartActions;
|
||||
goals: ChatPanelGoalActions;
|
||||
autoTitleCoordinator: AutoTitleCoordinator;
|
||||
reconnect: () => Promise<void>;
|
||||
|
|
@ -72,7 +72,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
|
|||
navigation,
|
||||
composerController,
|
||||
runtimeSettings,
|
||||
serverThreads,
|
||||
threadStart,
|
||||
goals,
|
||||
autoTitleCoordinator,
|
||||
reconnect,
|
||||
|
|
@ -153,7 +153,7 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
|
|||
},
|
||||
},
|
||||
{
|
||||
threadStarter: serverThreads,
|
||||
threadStarter: threadStart,
|
||||
runtimeSettings,
|
||||
threadActions,
|
||||
reconnectPanel: reconnect,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import { createChatAppServerGateway } from "../app-server/session-gateway";
|
|||
import type { ConnectionWorkTracker } from "../application/connection/connection-work";
|
||||
import { reconnectPanel } from "../application/connection/reconnect-actions";
|
||||
import { createLocalIdSource, type LocalIdSource } from "../application/local-id-source";
|
||||
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
|
||||
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";
|
||||
import type { RestorationController } from "../application/threads/restoration-controller";
|
||||
import type { ResumeActions } from "../application/threads/resume-actions";
|
||||
import type { ChatResumeWorkTracker } from "../application/threads/resume-work";
|
||||
import { createThreadStartActions } from "../application/threads/thread-start-actions";
|
||||
import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items";
|
||||
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
|
||||
import type { ChatComposerController } from "../panel/composer-controller";
|
||||
|
|
@ -125,8 +127,8 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
{
|
||||
connection,
|
||||
currentClient,
|
||||
appServer,
|
||||
localItemIds,
|
||||
goalSync: threadFoundation.goalSync,
|
||||
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
|
||||
status,
|
||||
},
|
||||
|
|
@ -135,7 +137,6 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
connection: { actions: connectionActions },
|
||||
inboundHandler,
|
||||
} = connectionBundle;
|
||||
const { threads: serverThreads } = connectionBundle.serverActions;
|
||||
ensureConnected = () => connectionActions.ensureConnected();
|
||||
const refreshActiveThreads = () => connectionActions.refreshActiveThreads();
|
||||
const runtime = createRuntimeBundle(host, {
|
||||
|
|
@ -143,12 +144,23 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
appServer,
|
||||
status,
|
||||
});
|
||||
const threadStart = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: appServer.threadStart,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread: (thread) => {
|
||||
environment.plugin.threadCatalog.apply({ type: "thread-started", thread });
|
||||
},
|
||||
syncThreadGoal: (threadId) => {
|
||||
void threadFoundation.goalSync.syncThreadGoal(threadId);
|
||||
},
|
||||
});
|
||||
const threadLifecycle = createThreadLifecycleBundle(host, {
|
||||
appServer,
|
||||
localItemIds,
|
||||
ensureConnected,
|
||||
status,
|
||||
serverThreads,
|
||||
threadStart,
|
||||
foundation: threadFoundation,
|
||||
refreshTabHeader,
|
||||
refreshLiveState,
|
||||
|
|
@ -200,7 +212,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
navigation: threadActions.navigation,
|
||||
composerController,
|
||||
runtimeSettings: runtime.settings,
|
||||
serverThreads,
|
||||
threadStart,
|
||||
goals: threadLifecycle.goals,
|
||||
autoTitleCoordinator: threadFoundation.autoTitleCoordinator,
|
||||
reconnect,
|
||||
|
|
@ -235,7 +247,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
stateStore,
|
||||
threadCatalog: environment.plugin.threadCatalog,
|
||||
appServerQueries: environment.plugin.appServerQueries,
|
||||
serverActions: connectionBundle.serverActions,
|
||||
applyAppServerMetadata: connectionBundle.sharedStateActions.applyAppServerMetadata,
|
||||
refreshTabHeader,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { ModelMetadata } from "../../../../domain/catalog/metadata";
|
|||
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { ChatPanelConnectionBundle } from "../bundles/connection-bundle";
|
||||
|
||||
type ThreadObserver = (result: ObservedResult<readonly Thread[]>) => void;
|
||||
type MetadataObserver = (result: ObservedResult<SharedServerMetadata>) => void;
|
||||
|
|
@ -31,16 +30,16 @@ export interface ChatPanelSharedStateBindingOptions {
|
|||
stateStore: ChatStateStore;
|
||||
threadCatalog: SharedStateThreadCatalog;
|
||||
appServerQueries: SharedStateAppServerQueries;
|
||||
serverActions: ChatPanelConnectionBundle["serverActions"];
|
||||
applyAppServerMetadata: (metadata: SharedServerMetadata) => void;
|
||||
refreshTabHeader: () => void;
|
||||
}
|
||||
|
||||
export function createChatPanelSharedStateBinding(options: ChatPanelSharedStateBindingOptions): ChatPanelSharedStateBinding {
|
||||
const unsubscribers: (() => void)[] = [];
|
||||
const { stateStore, threadCatalog, appServerQueries, serverActions, refreshTabHeader } = options;
|
||||
const { stateStore, threadCatalog, appServerQueries, applyAppServerMetadata, refreshTabHeader } = options;
|
||||
|
||||
const receiveThreads = (threads: readonly Thread[]): void => {
|
||||
serverActions.threads.applyThreadList(threads);
|
||||
stateStore.dispatch({ type: "thread-list/applied", threads, threadsLoaded: true });
|
||||
refreshTabHeader();
|
||||
};
|
||||
const receiveThreadResult = (result: ObservedResult<readonly Thread[]>): void => {
|
||||
|
|
@ -48,7 +47,7 @@ export function createChatPanelSharedStateBinding(options: ChatPanelSharedStateB
|
|||
if (observedThreads) receiveThreads(observedThreads);
|
||||
};
|
||||
const receiveAppServerMetadata = (metadata: SharedServerMetadata): void => {
|
||||
serverActions.metadata.applyAppServerMetadata(metadata);
|
||||
applyAppServerMetadata(metadata);
|
||||
};
|
||||
const receiveAppServerMetadataResult = (result: ObservedResult<SharedServerMetadata>): void => {
|
||||
const observedMetadata = result.value;
|
||||
|
|
@ -68,9 +67,9 @@ export function createChatPanelSharedStateBinding(options: ChatPanelSharedStateB
|
|||
};
|
||||
const applyCached = (): void => {
|
||||
const threads = threadCatalog.activeSnapshot();
|
||||
if (threads) serverActions.threads.applyThreadList(threads);
|
||||
if (threads) stateStore.dispatch({ type: "thread-list/applied", threads, threadsLoaded: true });
|
||||
const metadata = appServerQueries.appServerMetadataSnapshot();
|
||||
if (metadata) serverActions.metadata.applyAppServerMetadata(metadata);
|
||||
if (metadata) applyAppServerMetadata(metadata);
|
||||
const models = appServerQueries.modelsSnapshot();
|
||||
if (models) receiveModels(models);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,853 +0,0 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AppServerClient, ClientResponseByMethod } from "../../../../../src/app-server/connection/client";
|
||||
import type { ClientRequestParams } from "../../../../../src/app-server/connection/rpc-messages";
|
||||
import {
|
||||
type CatalogModel,
|
||||
type CatalogSkillMetadata,
|
||||
modelMetadataFromCatalogModels,
|
||||
} from "../../../../../src/app-server/protocol/catalog";
|
||||
import { threadFromThreadRecord } from "../../../../../src/app-server/protocol/thread";
|
||||
import { StaleAppServerSharedQueryContextError } from "../../../../../src/app-server/query/shared-queries";
|
||||
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
|
||||
import type { RateLimitSnapshot } from "../../../../../src/domain/runtime/metrics";
|
||||
import {
|
||||
createServerDiagnostics,
|
||||
diagnosticProbeError,
|
||||
diagnosticProbeOk,
|
||||
diagnosticsWithProbe,
|
||||
} from "../../../../../src/domain/server/diagnostics";
|
||||
import type { McpServerStatus } from "../../../../../src/domain/server/mcp-status";
|
||||
import type { SharedServerMetadata } from "../../../../../src/domain/server/metadata";
|
||||
import { createChatServerDiagnosticsActions } from "../../../../../src/features/chat/app-server/actions/diagnostics";
|
||||
import { createChatServerMetadataActions } from "../../../../../src/features/chat/app-server/actions/metadata";
|
||||
import { createChatServerThreadActions } from "../../../../../src/features/chat/app-server/actions/threads";
|
||||
import { runtimeSnapshotForChatState } from "../../../../../src/features/chat/application/runtime/snapshot";
|
||||
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
|
||||
import { setCollaborationModeIntent } from "../../../../../src/features/chat/domain/runtime/intent";
|
||||
import { toolInventoryDiagnosticSections } from "../../../../../src/features/chat/presentation/runtime/tool-inventory-diagnostic-sections";
|
||||
import { chatStateFixture, chatStateWith } from "../../support/state";
|
||||
|
||||
type ThreadStartResponse = ClientResponseByMethod["thread/start"];
|
||||
|
||||
describe("chat app-server actions", () => {
|
||||
it("publishes newly started threads before the first turn completes", async () => {
|
||||
let state = chatStateFixture();
|
||||
const existing = threadFixture("existing");
|
||||
state = chatStateWith(state, { threadList: { listedThreads: [threadFromThreadRecord(existing)] } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const started = threadFixture("started");
|
||||
const optimistic = threadFromThreadRecord({ ...started, preview: "first prompt" });
|
||||
const existingThread = threadFromThreadRecord(existing);
|
||||
const applyThreadCatalogEvent = vi.fn();
|
||||
const syncThreadGoal = vi.fn();
|
||||
const client = startThreadClient(
|
||||
vi.fn().mockResolvedValue({
|
||||
thread: started,
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
reasoningEffort: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const actions = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent,
|
||||
syncThreadGoal,
|
||||
});
|
||||
|
||||
await actions.startThread("first prompt");
|
||||
|
||||
expect(stateStore.getState().threadList.listedThreads).toEqual([optimistic, existingThread]);
|
||||
expect(applyThreadCatalogEvent).toHaveBeenCalledWith({ type: "thread-started", thread: optimistic });
|
||||
expect(syncThreadGoal).toHaveBeenCalledWith("started");
|
||||
});
|
||||
|
||||
it("keeps empty-panel runtime reservations when starting the first thread", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
stateStore.dispatch({ type: "runtime/model-requested", model: "gpt-5.5" });
|
||||
stateStore.dispatch({ type: "runtime/permission-profile-requested", permissionProfile: ":workspace" });
|
||||
stateStore.dispatch({ type: "runtime/reasoning-effort-requested", effort: "high" });
|
||||
stateStore.dispatch({ type: "runtime/fast-mode-requested", fastMode: "enabled" });
|
||||
stateStore.dispatch({ type: "runtime/approvals-reviewer-requested", approvalsReviewer: "auto_review" });
|
||||
stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "plan" });
|
||||
const started = threadFixture("started");
|
||||
const startThread = vi.fn().mockResolvedValue({
|
||||
thread: started,
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
serviceTier: "fast",
|
||||
approvalsReviewer: null,
|
||||
reasoningEffort: null,
|
||||
});
|
||||
const client = startThreadClient(startThread);
|
||||
|
||||
const actions = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent: vi.fn(),
|
||||
syncThreadGoal: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.startThread("first prompt");
|
||||
|
||||
expect(startThread).toHaveBeenCalledWith({
|
||||
cwd: "/vault",
|
||||
permissions: ":workspace",
|
||||
serviceName: "codex-panel",
|
||||
serviceTier: "fast",
|
||||
});
|
||||
expect(stateStore.getState().runtime.active.model).toBe("gpt-5");
|
||||
expect(stateStore.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
expect(stateStore.getState().runtime.pending.permissionProfile).toEqual({ kind: "set", value: ":workspace" });
|
||||
expect(stateStore.getState().runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
|
||||
expect(stateStore.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
|
||||
expect(stateStore.getState().runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(stateStore.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("plan"));
|
||||
});
|
||||
|
||||
it("can skip newly started thread goal sync when the caller sets the first goal", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const started = threadFixture("started");
|
||||
const syncThreadGoal = vi.fn();
|
||||
const client = startThreadClient(
|
||||
vi.fn().mockResolvedValue({
|
||||
thread: started,
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
reasoningEffort: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const actions = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent: vi.fn(),
|
||||
syncThreadGoal,
|
||||
});
|
||||
|
||||
await actions.startThread("first goal", { syncGoal: false });
|
||||
|
||||
expect(syncThreadGoal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts threads with service tier from explicit effective config", async () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { connection: { runtimeConfig: { ...emptyRuntimeConfigSnapshot(), serviceTier: "flex" } } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const started = threadFixture("started");
|
||||
const startThread = vi.fn().mockResolvedValue({
|
||||
thread: started,
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
serviceTier: "flex",
|
||||
approvalsReviewer: null,
|
||||
reasoningEffort: null,
|
||||
});
|
||||
const client = startThreadClient(startThread);
|
||||
|
||||
const actions = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent: vi.fn(),
|
||||
syncThreadGoal: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.startThread();
|
||||
|
||||
expect(startThread).toHaveBeenCalledWith({
|
||||
cwd: "/vault",
|
||||
serviceName: "codex-panel",
|
||||
serviceTier: "flex",
|
||||
});
|
||||
});
|
||||
|
||||
it("starts threads with permission profile from explicit config", async () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, {
|
||||
connection: {
|
||||
runtimeConfig: {
|
||||
...emptyRuntimeConfigSnapshot(),
|
||||
startupPermissions: {
|
||||
...emptyRuntimeConfigSnapshot().startupPermissions,
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const stateStore = createChatStateStore(state);
|
||||
const started = threadFixture("started");
|
||||
const startThread = vi.fn().mockResolvedValue({
|
||||
thread: started,
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
reasoningEffort: null,
|
||||
});
|
||||
const client = startThreadClient(startThread);
|
||||
|
||||
const actions = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent: vi.fn(),
|
||||
syncThreadGoal: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.startThread();
|
||||
|
||||
expect(startThread).toHaveBeenCalledWith({
|
||||
cwd: "/vault",
|
||||
permissions: ":workspace",
|
||||
serviceName: "codex-panel",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps app-server preview when newly started threads already have one", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const started = threadFixture("started", { preview: "server preview" });
|
||||
const applyThreadCatalogEvent = vi.fn();
|
||||
const client = startThreadClient(
|
||||
vi.fn().mockResolvedValue({
|
||||
thread: started,
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
reasoningEffort: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const actions = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent,
|
||||
syncThreadGoal: () => undefined,
|
||||
});
|
||||
|
||||
await actions.startThread("local preview");
|
||||
|
||||
expect(applyThreadCatalogEvent).toHaveBeenCalledWith({ type: "thread-started", thread: threadFromThreadRecord(started) });
|
||||
});
|
||||
|
||||
it("does not apply newly started threads after the client changes", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const start = deferred<ThreadStartResponse>();
|
||||
const firstClient = startThreadClient(vi.fn().mockReturnValue(start.promise));
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const applyThreadCatalogEvent = vi.fn();
|
||||
const syncThreadGoal = vi.fn();
|
||||
const actions = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => currentClient,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
applyThreadCatalogEvent,
|
||||
syncThreadGoal,
|
||||
});
|
||||
|
||||
const starting = actions.startThread("local preview");
|
||||
currentClient = secondClient;
|
||||
start.resolve({
|
||||
thread: threadFixture("stale-started"),
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
modelProvider: "openai",
|
||||
serviceTier: null,
|
||||
approvalPolicy: "on-request",
|
||||
runtimeWorkspaceRoots: [],
|
||||
instructionSources: [],
|
||||
approvalsReviewer: "user",
|
||||
activePermissionProfile: null,
|
||||
sandbox: { type: "readOnly", networkAccess: false },
|
||||
reasoningEffort: null,
|
||||
multiAgentMode: "explicitRequestOnly",
|
||||
});
|
||||
|
||||
await expect(starting).resolves.toBeNull();
|
||||
expect(stateStore.getState().activeThread.id).toBeNull();
|
||||
expect(stateStore.getState().threadList.listedThreads).toEqual([]);
|
||||
expect(applyThreadCatalogEvent).not.toHaveBeenCalled();
|
||||
expect(syncThreadGoal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses cached app-server metadata for deferred diagnostics", async () => {
|
||||
const state = chatStateFixture();
|
||||
const stateStore = createChatStateStore(state);
|
||||
|
||||
const listMcpServerStatus = vi.fn().mockResolvedValue({ data: [] });
|
||||
const refreshedMetadata = serverMetadataFixture({
|
||||
availableModels: modelMetadataFromCatalogModels([modelFixture("gpt-5.1")]),
|
||||
availableSkills: [{ name: "writer", description: "", path: "/tmp/writer", enabled: true }],
|
||||
rateLimit: rateLimitFixture(),
|
||||
serverDiagnostics: diagnosticsWithProbe(
|
||||
diagnosticsWithProbe(
|
||||
diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 models", 1)),
|
||||
diagnosticProbeOk("skills", "1 skills", 1),
|
||||
),
|
||||
diagnosticProbeOk("rateLimits", "available", 1),
|
||||
),
|
||||
});
|
||||
const refreshAppServerMetadata = vi.fn<() => Promise<SharedServerMetadata | null>>().mockResolvedValue(refreshedMetadata);
|
||||
const client = requestClient({
|
||||
"mcpServerStatus/list": listMcpServerStatus,
|
||||
});
|
||||
const metadataCache = metadataCacheHost({ current: null });
|
||||
|
||||
const metadata = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCache,
|
||||
refreshAppServerMetadata: async () => {
|
||||
const next = await refreshAppServerMetadata();
|
||||
metadataCache.updateAppServerMetadata(() => next);
|
||||
return next;
|
||||
},
|
||||
});
|
||||
const diagnostics = createChatServerDiagnosticsActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCache,
|
||||
});
|
||||
|
||||
await metadata.refreshAppServerMetadata();
|
||||
|
||||
await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true });
|
||||
|
||||
expect(refreshAppServerMetadata).toHaveBeenCalledOnce();
|
||||
expect(listMcpServerStatus).toHaveBeenCalledWith({ detail: "toolsAndAuthOnly", limit: 100 });
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "1 models",
|
||||
});
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.skills).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "1 skills",
|
||||
});
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "available",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores stale shared app-server metadata refreshes without applying state", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const metadata = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => null,
|
||||
...metadataCacheHost({ current: null }),
|
||||
refreshAppServerMetadata: async () => {
|
||||
throw new StaleAppServerSharedQueryContextError();
|
||||
},
|
||||
});
|
||||
|
||||
await expect(metadata.refreshAppServerMetadata()).resolves.toBeNull();
|
||||
|
||||
expect(stateStore.getState().connection.availableModels).toEqual([]);
|
||||
expect(stateStore.getState().connection.runtimeConfig).toBeNull();
|
||||
});
|
||||
|
||||
it("uses metadata diagnostics as the default resource probe source", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const metadataCache = metadataCacheHost({
|
||||
current: serverMetadataFixture({
|
||||
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "cached models", 1)),
|
||||
}),
|
||||
});
|
||||
const listModels = vi.fn().mockResolvedValue({ data: [modelFixture("gpt-direct")] });
|
||||
const listSkills = vi.fn().mockResolvedValue({ data: [{ skills: [skillFixture("direct-skill")] }] });
|
||||
const readAccountRateLimits = vi.fn().mockResolvedValue({ rateLimits: rateLimitFixture(), rateLimitsByLimitId: null });
|
||||
const listMcpServerStatus = vi.fn().mockResolvedValue({ data: [] });
|
||||
const client = requestClient({
|
||||
"model/list": listModels,
|
||||
"skills/list": listSkills,
|
||||
"account/rateLimits/read": readAccountRateLimits,
|
||||
"mcpServerStatus/list": listMcpServerStatus,
|
||||
});
|
||||
const diagnostics = createChatServerDiagnosticsActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCache,
|
||||
});
|
||||
|
||||
await diagnostics.refreshServerDiagnostics();
|
||||
|
||||
expect(listModels).not.toHaveBeenCalled();
|
||||
expect(listSkills).not.toHaveBeenCalled();
|
||||
expect(readAccountRateLimits).not.toHaveBeenCalled();
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "cached models",
|
||||
});
|
||||
expect(listMcpServerStatus).toHaveBeenCalledWith({ detail: "toolsAndAuthOnly", limit: 100 });
|
||||
});
|
||||
|
||||
it("can force resource probes for explicit health checks", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const listModels = vi.fn().mockResolvedValue({ data: [modelFixture("gpt-direct")] });
|
||||
const listSkills = vi.fn().mockResolvedValue({ data: [{ skills: [skillFixture("direct-skill")] }] });
|
||||
const readAccountRateLimits = vi.fn().mockResolvedValue({ rateLimits: rateLimitFixture(), rateLimitsByLimitId: null });
|
||||
const client = requestClient({
|
||||
"model/list": listModels,
|
||||
"skills/list": listSkills,
|
||||
"account/rateLimits/read": readAccountRateLimits,
|
||||
"mcpServerStatus/list": vi.fn().mockResolvedValue({ data: [] }),
|
||||
});
|
||||
const diagnostics = createChatServerDiagnosticsActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCacheHost(),
|
||||
});
|
||||
|
||||
await diagnostics.refreshServerDiagnostics({ forceResourceProbes: true });
|
||||
|
||||
expect(listModels).toHaveBeenCalledWith({ includeHidden: false, limit: 100 });
|
||||
expect(listSkills).toHaveBeenCalledWith({ cwds: ["/vault"], forceReload: false });
|
||||
expect(readAccountRateLimits).toHaveBeenCalledWith(undefined);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "1 models",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not apply or publish diagnostic probes after the client changes", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const mcpStatusRefresh = deferred<{ data: ReturnType<typeof mcpServerStatus>[] }>();
|
||||
const listMcpServerStatus = vi.fn().mockReturnValue(mcpStatusRefresh.promise);
|
||||
const firstClient = requestClient({
|
||||
"mcpServerStatus/list": listMcpServerStatus,
|
||||
});
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const updateAppServerMetadata = vi.fn(() => null);
|
||||
const diagnostics = createChatServerDiagnosticsActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => currentClient,
|
||||
appServerMetadataSnapshot: () => null,
|
||||
updateAppServerMetadata,
|
||||
});
|
||||
|
||||
const refreshing = diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true });
|
||||
currentClient = secondClient;
|
||||
mcpStatusRefresh.resolve({ data: [mcpServerStatus()] });
|
||||
|
||||
await refreshing;
|
||||
expect(listMcpServerStatus).toHaveBeenCalledWith({ detail: "toolsAndAuthOnly", limit: 100 });
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.mcpServers.status).toBe("unknown");
|
||||
expect(stateStore.getState().connection.serverDiagnostics.mcpServers).toEqual([]);
|
||||
expect(updateAppServerMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not apply or publish app-server metadata when the client changes before refresh completes", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const refreshAppServerMetadata = vi.fn().mockResolvedValue(null);
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => ({}) as AppServerClient,
|
||||
...metadataCacheHost(),
|
||||
refreshAppServerMetadata,
|
||||
});
|
||||
|
||||
const refreshing = actions.refreshAppServerMetadata();
|
||||
|
||||
await expect(refreshing).resolves.toBeNull();
|
||||
expect(stateStore.getState().connection.availableModels).toEqual([]);
|
||||
expect(stateStore.getState().connection.availableSkills).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps query-cached models visible when metadata model refresh fails", async () => {
|
||||
const state = chatStateFixture();
|
||||
const stateStore = createChatStateStore(state);
|
||||
const metadata = serverMetadataFixture({
|
||||
availableModels: modelMetadataFromCatalogModels([modelFixture("gpt-cached")]),
|
||||
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("models", new Error("offline"), 1)),
|
||||
});
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => ({}) as AppServerClient,
|
||||
...metadataCacheHost({ current: metadata }),
|
||||
refreshAppServerMetadata: async () => metadata,
|
||||
});
|
||||
|
||||
await actions.refreshAppServerMetadata();
|
||||
|
||||
expect(stateStore.getState().connection.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("does not use chat state as a second model source when metadata model refresh fails", async () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { connection: { availableModels: modelMetadataFromCatalogModels([modelFixture("gpt-state-only")]) } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const metadata = serverMetadataFixture({
|
||||
availableModels: [],
|
||||
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("models", new Error("offline"), 1)),
|
||||
});
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => ({}) as AppServerClient,
|
||||
...metadataCacheHost({ current: metadata }),
|
||||
refreshAppServerMetadata: async () => metadata,
|
||||
});
|
||||
|
||||
await actions.refreshAppServerMetadata();
|
||||
|
||||
expect(stateStore.getState().connection.availableModels).toEqual([]);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("does not apply or publish refreshed skills after the client changes", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const skillRefresh = deferred<{ data: { skills: CatalogSkillMetadata[] }[] }>();
|
||||
const listSkills = vi.fn().mockReturnValue(skillRefresh.promise);
|
||||
const firstClient = requestClient({ "skills/list": listSkills });
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const updateAppServerMetadata = vi.fn(() => null);
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => currentClient,
|
||||
appServerMetadataSnapshot: () => null,
|
||||
updateAppServerMetadata,
|
||||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
const refreshing = actions.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
|
||||
currentClient = secondClient;
|
||||
skillRefresh.resolve({ data: [{ skills: [skillFixture("stale-skill")] }] });
|
||||
|
||||
await refreshing;
|
||||
expect(listSkills).toHaveBeenCalledWith({ cwds: ["/vault"], forceReload: true });
|
||||
expect(stateStore.getState().connection.availableSkills).toEqual([]);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.skills.status).toBe("unknown");
|
||||
expect(updateAppServerMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps previous skills when sparse skill refresh fails", async () => {
|
||||
let state = chatStateFixture();
|
||||
const previousSkills = [{ name: "writer", description: "", path: "/tmp/writer", enabled: true }];
|
||||
state = chatStateWith(state, { connection: { availableSkills: previousSkills } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const listSkills = vi.fn().mockRejectedValue(new Error("offline"));
|
||||
const client = requestClient({ "skills/list": listSkills });
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCacheHost(),
|
||||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
await actions.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
|
||||
|
||||
expect(listSkills).toHaveBeenCalledWith({ cwds: ["/vault"], forceReload: true });
|
||||
expect(stateStore.getState().connection.availableSkills).toEqual(previousSkills);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.skills).toMatchObject({ status: "failed" });
|
||||
});
|
||||
|
||||
it("publishes refreshed rate limits from sparse update notifications", async () => {
|
||||
const state = chatStateFixture();
|
||||
const stateStore = createChatStateStore(state);
|
||||
const rateLimit = rateLimitFixture({ primary: { usedPercent: 64, windowDurationMins: 300, resetsAt: null } });
|
||||
const cachedMetadata = { current: serverMetadataFixture() as SharedServerMetadata | null };
|
||||
const client = requestClient({
|
||||
"account/rateLimits/read": vi.fn().mockResolvedValue({ rateLimits: rateLimit, rateLimitsByLimitId: null }),
|
||||
});
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCacheHost(cachedMetadata),
|
||||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
await actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
|
||||
|
||||
expect(stateStore.getState().connection.rateLimit).toMatchObject({ primary: { usedPercent: 64 } });
|
||||
expect(cachedMetadata.current?.rateLimit).toStrictEqual(rateLimit);
|
||||
});
|
||||
|
||||
it("keeps the previous rate limit snapshot when sparse update refresh fails", async () => {
|
||||
let state = chatStateFixture();
|
||||
const previousRateLimit = rateLimitFixture({
|
||||
limitName: "Codex",
|
||||
primary: { usedPercent: 42, windowDurationMins: 300, resetsAt: null },
|
||||
});
|
||||
state = chatStateWith(state, { connection: { rateLimit: previousRateLimit } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const client = requestClient({
|
||||
"account/rateLimits/read": vi.fn().mockRejectedValue(new Error("offline")),
|
||||
});
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCacheHost(),
|
||||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
await actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
|
||||
|
||||
expect(stateStore.getState().connection.rateLimit).toBe(previousRateLimit);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits).toMatchObject({ status: "failed" });
|
||||
});
|
||||
|
||||
it("does not apply or publish sparse rate limit refreshes after the client changes", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const rateLimitRefresh = deferred<{ rateLimits: RateLimitSnapshot; rateLimitsByLimitId: null }>();
|
||||
const firstClient = requestClient({
|
||||
"account/rateLimits/read": vi.fn().mockReturnValue(rateLimitRefresh.promise),
|
||||
});
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const updateAppServerMetadata = vi.fn(() => null);
|
||||
const actions = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => currentClient,
|
||||
appServerMetadataSnapshot: () => null,
|
||||
updateAppServerMetadata,
|
||||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
const refreshing = actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
|
||||
currentClient = secondClient;
|
||||
rateLimitRefresh.resolve({
|
||||
rateLimits: rateLimitFixture({ primary: { usedPercent: 88, windowDurationMins: 300, resetsAt: null } }),
|
||||
rateLimitsByLimitId: null,
|
||||
});
|
||||
|
||||
await refreshing;
|
||||
expect(stateStore.getState().connection.rateLimit).toBeNull();
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits.status).toBe("unknown");
|
||||
expect(updateAppServerMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refreshes tool provider snapshots with cached MCP startup diagnostics", async () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { activeThread: { id: "thread-1" } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const listMcpServerStatus = vi.fn().mockResolvedValue({ data: [mcpServerStatus()] });
|
||||
const client = requestClient({
|
||||
"app/list": vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
|
||||
"plugin/installed": vi.fn().mockResolvedValue({ marketplaces: [], marketplaceLoadErrors: [] }),
|
||||
"mcpServerStatus/list": listMcpServerStatus,
|
||||
"skills/list": vi.fn().mockResolvedValue({ data: [{ cwd: "/vault", skills: [] }] }),
|
||||
});
|
||||
const metadataCache = metadataCacheHost({ current: serverMetadataFixture() });
|
||||
const metadata = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCache,
|
||||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
const diagnostics = createChatServerDiagnosticsActions({
|
||||
stateStore,
|
||||
vaultPath: "/vault",
|
||||
currentClient: () => client,
|
||||
...metadataCache,
|
||||
});
|
||||
|
||||
await metadata.applyAppServerResourceEvent({
|
||||
type: "mcp-startup-status-updated",
|
||||
name: "github",
|
||||
status: "ready",
|
||||
message: null,
|
||||
});
|
||||
|
||||
await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true });
|
||||
|
||||
const sections = toolInventoryDiagnosticSections(stateStore.getState().connection.serverDiagnostics);
|
||||
const toolProviderRows = sections.find((section) => section.title === "Tool providers")?.rows ?? [];
|
||||
|
||||
expect(sections.map((section) => section.title)).toEqual(["Plugins", "Tool providers", "Skills"]);
|
||||
expect(toolProviderRows.map((row) => `${row.label}: ${row.value}`)).toEqual([
|
||||
"github: MCP server, ready, auth oAuth, 1 tool, 0 resources",
|
||||
]);
|
||||
expect(listMcpServerStatus).toHaveBeenCalledWith({
|
||||
detail: "toolsAndAuthOnly",
|
||||
limit: 100,
|
||||
threadId: "thread-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function requestClient(handlers: Record<string, ReturnType<typeof vi.fn>> = {}): AppServerClient {
|
||||
const methodHandlers: Record<string, (params: unknown) => Promise<unknown>> = {
|
||||
"skills/list": async (params) => ({ data: [{ cwd: (params as { cwds: string[] }).cwds[0] ?? "", skills: [] }] }),
|
||||
"app/list": async () => ({ data: [], nextCursor: null }),
|
||||
"plugin/installed": async () => ({ marketplaces: [], marketplaceLoadErrors: [] }),
|
||||
"plugin/read": async () => ({ plugin: { skills: [], hooks: [], apps: [], appTemplates: [], mcpServers: [] } }),
|
||||
...handlers,
|
||||
};
|
||||
return {
|
||||
request: vi.fn((method: string, params: unknown) => {
|
||||
const handler = methodHandlers[method];
|
||||
if (!handler) throw new Error(`Unexpected app-server request: ${method}`);
|
||||
return handler(params);
|
||||
}),
|
||||
} as unknown as AppServerClient;
|
||||
}
|
||||
|
||||
function startThreadClient(startThread: ReturnType<typeof vi.fn>): AppServerClient {
|
||||
return {
|
||||
request: vi.fn((method: string, params: ClientRequestParams<"thread/start">) => {
|
||||
if (method !== "thread/start") throw new Error(`Unexpected app-server request: ${method}`);
|
||||
if (!params.cwd) throw new Error("Expected thread/start cwd.");
|
||||
return (startThread as unknown as (params: ClientRequestParams<"thread/start">) => Promise<ThreadStartResponse>)(params);
|
||||
}),
|
||||
} as unknown as AppServerClient;
|
||||
}
|
||||
|
||||
function threadFixture(id: string, overrides: Partial<ThreadStartResponse["thread"]> = {}): ThreadStartResponse["thread"] {
|
||||
return {
|
||||
id,
|
||||
extra: null,
|
||||
sessionId: id,
|
||||
forkedFromId: null,
|
||||
parentThreadId: null,
|
||||
preview: "",
|
||||
ephemeral: false,
|
||||
historyMode: "paginated",
|
||||
modelProvider: "openai",
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
recencyAt: null,
|
||||
status: { type: "idle" },
|
||||
path: null,
|
||||
cwd: "/vault",
|
||||
cliVersion: "test",
|
||||
source: "unknown",
|
||||
threadSource: null,
|
||||
agentNickname: null,
|
||||
agentRole: null,
|
||||
gitInfo: null,
|
||||
name: null,
|
||||
turns: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function modelFixture(model: string): CatalogModel {
|
||||
return {
|
||||
id: model,
|
||||
model,
|
||||
upgrade: null,
|
||||
upgradeInfo: null,
|
||||
availabilityNux: null,
|
||||
displayName: model,
|
||||
description: "",
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: "medium",
|
||||
inputModalities: ["text"],
|
||||
supportsPersonality: false,
|
||||
additionalSpeedTiers: [],
|
||||
serviceTiers: [],
|
||||
defaultServiceTier: null,
|
||||
isDefault: false,
|
||||
};
|
||||
}
|
||||
|
||||
function skillFixture(name: string): CatalogSkillMetadata {
|
||||
return {
|
||||
name,
|
||||
description: "",
|
||||
path: `/skills/${name}`,
|
||||
scope: "repo",
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
function rateLimitFixture(overrides: Partial<RateLimitSnapshot> = {}): RateLimitSnapshot {
|
||||
return {
|
||||
limitId: "codex",
|
||||
limitName: "Codex",
|
||||
primary: null,
|
||||
secondary: null,
|
||||
individualLimit: null,
|
||||
rateLimitReachedType: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function serverMetadataFixture(overrides: Partial<SharedServerMetadata> = {}): SharedServerMetadata {
|
||||
return {
|
||||
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
||||
availableModels: [],
|
||||
availableSkills: [],
|
||||
availablePermissionProfiles: [],
|
||||
rateLimit: null,
|
||||
serverDiagnostics: createServerDiagnostics(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function metadataCacheHost(cache: { current: SharedServerMetadata | null } = { current: null }): {
|
||||
appServerMetadataSnapshot: () => SharedServerMetadata | null;
|
||||
updateAppServerMetadata: (updater: (metadata: SharedServerMetadata | null) => SharedServerMetadata | null) => SharedServerMetadata | null;
|
||||
} {
|
||||
return {
|
||||
appServerMetadataSnapshot: () => cache.current,
|
||||
updateAppServerMetadata: (updater) => {
|
||||
const next = updater(cache.current);
|
||||
cache.current = next;
|
||||
return next;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mcpServerStatus(): McpServerStatus {
|
||||
return {
|
||||
name: "github",
|
||||
serverInfo: null,
|
||||
tools: {
|
||||
search_issues: { name: "search_issues", inputSchema: {} },
|
||||
},
|
||||
resources: [],
|
||||
resourceTemplates: [],
|
||||
authStatus: "oAuth",
|
||||
} as McpServerStatus;
|
||||
}
|
||||
|
||||
interface Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
}
|
||||
|
||||
function deferred<T>(): Deferred<T> {
|
||||
let resolve: (value: T) => void = () => undefined;
|
||||
const promise = new Promise<T>((settle) => {
|
||||
resolve = settle;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import * as shortLivedClient from "../../../../src/app-server/connection/short-l
|
|||
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 { createThreadReferenceResolver } from "../../../../src/features/chat/app-server/thread-reference-resolver";
|
||||
import { preparedUserInputWithWikiLinkMentionsSkillsAndContext } from "../../../../src/features/chat/application/composer/wikilink-context";
|
||||
|
|
@ -13,6 +14,43 @@ import { deferred } from "../../../support/async";
|
|||
const textInput = (text: string): CodexInput => [{ type: "text", text }];
|
||||
|
||||
describe("chat app-server transports", () => {
|
||||
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;
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
}).threadStart;
|
||||
|
||||
const snapshot = await transport.startThread({ serviceTier: "priority", permissions: ":workspace" });
|
||||
|
||||
expect(request).toHaveBeenCalledWith("thread/start", {
|
||||
cwd: "/vault",
|
||||
serviceName: "codex-panel",
|
||||
serviceTier: "priority",
|
||||
permissions: ":workspace",
|
||||
});
|
||||
expect(snapshot?.thread.id).toBe("thread");
|
||||
expect(snapshot?.cwd).toBe("/vault");
|
||||
});
|
||||
|
||||
it("drops stale thread start responses after the current client changes", async () => {
|
||||
const start = deferred<AppServerThreadStartResponse>();
|
||||
const firstClient = { request: vi.fn().mockReturnValue(start.promise) } as unknown as AppServerClient;
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => currentClient,
|
||||
connectedClient: vi.fn().mockResolvedValue(firstClient),
|
||||
}).threadStart;
|
||||
|
||||
const starting = transport.startThread({});
|
||||
currentClient = secondClient;
|
||||
start.resolve(threadStartResponse("thread"));
|
||||
|
||||
await expect(starting).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("starts turns with the session vault path and returns chat-owned turn ids", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ turn: { id: "turn-1" } });
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
|
|
@ -305,6 +343,91 @@ describe("chat app-server transports", () => {
|
|||
expect(request).toHaveBeenCalledWith("thread/settings/update", { threadId: "thread", model: "gpt-5.5" });
|
||||
});
|
||||
|
||||
it("reads sparse skill metadata through the current app-server client", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ data: [{ cwd: "/vault", skills: [] }] });
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
}).metadataResource;
|
||||
|
||||
await expect(transport.readSkillMetadata(true)).resolves.toMatchObject({
|
||||
value: [],
|
||||
probe: { status: "ok" },
|
||||
});
|
||||
|
||||
expect(request).toHaveBeenCalledWith("skills/list", { cwds: ["/vault"], forceReload: true });
|
||||
});
|
||||
|
||||
it("reads diagnostics probes and tool inventory at the app-server boundary", async () => {
|
||||
const request = vi.fn((method: string) => {
|
||||
switch (method) {
|
||||
case "model/list":
|
||||
return Promise.resolve({ data: [] });
|
||||
case "account/rateLimits/read":
|
||||
return Promise.resolve({ rateLimits: null, rateLimitsByLimitId: null });
|
||||
case "plugin/installed":
|
||||
return Promise.resolve({ marketplaces: [], marketplaceLoadErrors: [] });
|
||||
case "mcpServerStatus/list":
|
||||
return Promise.resolve({ data: [] });
|
||||
case "skills/list":
|
||||
return Promise.resolve({ data: [{ cwd: "/vault", skills: [] }] });
|
||||
default:
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
}
|
||||
});
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
}).serverDiagnostics;
|
||||
|
||||
const snapshot = await transport.readServerDiagnostics({
|
||||
threadId: "thread",
|
||||
initialDiagnostics: createServerDiagnostics(),
|
||||
cachedSkills: [],
|
||||
cachedSkillsProbe: diagnosticProbeOk("skills", "0 skills", 1),
|
||||
forceResourceProbes: true,
|
||||
appServerMetadataSnapshot: false,
|
||||
});
|
||||
|
||||
expect(snapshot?.resourceProbes.map((probe) => probe.id)).toEqual(["models", "rateLimits"]);
|
||||
expect(request).toHaveBeenCalledWith("model/list", { includeHidden: false, limit: 100 });
|
||||
expect(request).toHaveBeenCalledWith("account/rateLimits/read", undefined);
|
||||
expect(request).toHaveBeenCalledWith("mcpServerStatus/list", {
|
||||
detail: "toolsAndAuthOnly",
|
||||
limit: 100,
|
||||
threadId: "thread",
|
||||
});
|
||||
expect(request).not.toHaveBeenCalledWith("skills/list", expect.anything());
|
||||
});
|
||||
|
||||
it("reads skills for diagnostics tool inventory when no cached skills are provided", async () => {
|
||||
const request = vi.fn((method: string) => {
|
||||
switch (method) {
|
||||
case "plugin/installed":
|
||||
return Promise.resolve({ marketplaces: [], marketplaceLoadErrors: [] });
|
||||
case "mcpServerStatus/list":
|
||||
return Promise.resolve({ data: [] });
|
||||
case "skills/list":
|
||||
return Promise.resolve({ data: [{ cwd: "/vault", skills: [] }] });
|
||||
default:
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
}
|
||||
});
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
}).serverDiagnostics;
|
||||
|
||||
await transport.readServerDiagnostics({
|
||||
threadId: null,
|
||||
initialDiagnostics: createServerDiagnostics(),
|
||||
forceResourceProbes: false,
|
||||
appServerMetadataSnapshot: true,
|
||||
});
|
||||
|
||||
expect(request).toHaveBeenCalledWith("skills/list", { cwds: ["/vault"], forceReload: false });
|
||||
});
|
||||
|
||||
it("resolves referenced thread input at the app-server boundary", async () => {
|
||||
const request = vi.fn().mockResolvedValue({
|
||||
data: [turn([userMessage("u1", "元の依頼"), agentMessage("a1", "回答")])],
|
||||
|
|
@ -394,6 +517,7 @@ describe("chat app-server transports", () => {
|
|||
});
|
||||
});
|
||||
|
||||
type AppServerThreadStartResponse = ClientResponseByMethod["thread/start"];
|
||||
type AppServerThreadResumeResponse = ClientResponseByMethod["thread/resume"];
|
||||
|
||||
function createTestGateway(options: {
|
||||
|
|
@ -437,6 +561,25 @@ function threadRecord(id: string, turns: readonly TurnRecord[] = [], overrides:
|
|||
};
|
||||
}
|
||||
|
||||
function threadStartResponse(threadId: string, overrides: Partial<AppServerThreadStartResponse> = {}): AppServerThreadStartResponse {
|
||||
return {
|
||||
thread: threadRecord(threadId) as AppServerThreadStartResponse["thread"],
|
||||
cwd: "/vault",
|
||||
model: "gpt-test",
|
||||
modelProvider: "openai",
|
||||
serviceTier: null,
|
||||
runtimeWorkspaceRoots: [],
|
||||
instructionSources: [],
|
||||
approvalPolicy: "never",
|
||||
approvalsReviewer: "user",
|
||||
sandbox: { type: "readOnly", networkAccess: false },
|
||||
activePermissionProfile: null,
|
||||
reasoningEffort: null,
|
||||
multiAgentMode: "explicitRequestOnly",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function threadResumeResponse(threadId: string, overrides: Partial<AppServerThreadResumeResponse> = {}): AppServerThreadResumeResponse {
|
||||
return {
|
||||
thread: threadRecord(threadId) as AppServerThreadResumeResponse["thread"],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,412 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ModelMetadata, SkillMetadata } from "../../../../../src/domain/catalog/metadata";
|
||||
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
|
||||
import type { RateLimitSnapshot } from "../../../../../src/domain/runtime/metrics";
|
||||
import {
|
||||
createServerDiagnostics,
|
||||
type DiagnosticProbeResult,
|
||||
diagnosticProbeError,
|
||||
diagnosticProbeOk,
|
||||
diagnosticsWithProbe,
|
||||
} from "../../../../../src/domain/server/diagnostics";
|
||||
import type { McpServerStatusSummary } from "../../../../../src/domain/server/mcp-status";
|
||||
import type { SharedServerMetadata } from "../../../../../src/domain/server/metadata";
|
||||
import type { ToolInventorySnapshot } from "../../../../../src/domain/server/tool-inventory";
|
||||
import type { MetadataResourceTransport } from "../../../../../src/features/chat/application/connection/metadata-transport";
|
||||
import { createServerDiagnosticsActions } from "../../../../../src/features/chat/application/connection/server-diagnostics-actions";
|
||||
import { createServerMetadataActions } from "../../../../../src/features/chat/application/connection/server-metadata-actions";
|
||||
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
|
||||
import { toolInventoryDiagnosticSections } from "../../../../../src/features/chat/presentation/runtime/tool-inventory-diagnostic-sections";
|
||||
import { chatStateFixture, chatStateWith } from "../../support/state";
|
||||
|
||||
describe("server metadata actions", () => {
|
||||
it("applies refreshed app-server metadata", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const metadata = serverMetadataFixture({ availableModels: [modelFixture("gpt-5.1")] });
|
||||
const actions = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport(),
|
||||
...metadataCacheHost(),
|
||||
refreshAppServerMetadata: vi.fn().mockResolvedValue(metadata),
|
||||
isStaleSharedQueryError: () => false,
|
||||
});
|
||||
|
||||
await actions.refreshAppServerMetadata();
|
||||
|
||||
expect(stateStore.getState().connection.availableModels.map((model) => model.model)).toEqual(["gpt-5.1"]);
|
||||
});
|
||||
|
||||
it("ignores stale shared app-server metadata refreshes without applying state", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const stale = new Error("stale");
|
||||
const actions = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport(),
|
||||
...metadataCacheHost(),
|
||||
refreshAppServerMetadata: vi.fn().mockRejectedValue(stale),
|
||||
isStaleSharedQueryError: (error) => error === stale,
|
||||
});
|
||||
|
||||
await expect(actions.refreshAppServerMetadata()).resolves.toBeNull();
|
||||
|
||||
expect(stateStore.getState().connection.availableModels).toEqual([]);
|
||||
expect(stateStore.getState().connection.runtimeConfig).toBeNull();
|
||||
});
|
||||
|
||||
it("does not apply stale sparse skill refreshes", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const updateAppServerMetadata = vi.fn(() => null);
|
||||
const actions = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport({ readSkillMetadata: vi.fn().mockResolvedValue(null) }),
|
||||
appServerMetadataSnapshot: () => null,
|
||||
updateAppServerMetadata,
|
||||
refreshAppServerMetadata: vi.fn().mockResolvedValue(null),
|
||||
isStaleSharedQueryError: () => false,
|
||||
});
|
||||
|
||||
await actions.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
|
||||
|
||||
expect(stateStore.getState().connection.availableSkills).toEqual([]);
|
||||
expect(updateAppServerMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps previous skills when sparse skill refresh fails", async () => {
|
||||
let state = chatStateFixture();
|
||||
const previousSkills = [skillFixture("writer")];
|
||||
state = chatStateWith(state, { connection: { availableSkills: previousSkills } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const actions = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport({
|
||||
readSkillMetadata: vi.fn().mockResolvedValue({
|
||||
value: [],
|
||||
probe: diagnosticProbeError("skills", new Error("offline"), 1),
|
||||
}),
|
||||
}),
|
||||
...metadataCacheHost(),
|
||||
refreshAppServerMetadata: vi.fn().mockResolvedValue(null),
|
||||
isStaleSharedQueryError: () => false,
|
||||
});
|
||||
|
||||
await actions.applyAppServerResourceEvent({ type: "skills-changed", forceReload: true });
|
||||
|
||||
expect(stateStore.getState().connection.availableSkills).toEqual(previousSkills);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.skills).toMatchObject({ status: "failed" });
|
||||
});
|
||||
|
||||
it("publishes refreshed rate limits from sparse update notifications", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const rateLimit = rateLimitFixture({ primary: { usedPercent: 64, windowDurationMins: 300, resetsAt: null } });
|
||||
const cachedMetadata = { current: serverMetadataFixture() as SharedServerMetadata | null };
|
||||
const actions = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport({
|
||||
readRateLimitMetadata: vi.fn().mockResolvedValue({
|
||||
value: rateLimit,
|
||||
probe: diagnosticProbeOk("rateLimits", "available", 1),
|
||||
}),
|
||||
}),
|
||||
...metadataCacheHost(cachedMetadata),
|
||||
refreshAppServerMetadata: vi.fn().mockResolvedValue(null),
|
||||
isStaleSharedQueryError: () => false,
|
||||
});
|
||||
|
||||
await actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
|
||||
|
||||
expect(stateStore.getState().connection.rateLimit).toMatchObject({ primary: { usedPercent: 64 } });
|
||||
expect(cachedMetadata.current?.rateLimit).toStrictEqual(rateLimit);
|
||||
});
|
||||
|
||||
it("keeps the previous rate limit snapshot when sparse update refresh fails", async () => {
|
||||
let state = chatStateFixture();
|
||||
const previousRateLimit = rateLimitFixture({
|
||||
limitName: "Codex",
|
||||
primary: { usedPercent: 42, windowDurationMins: 300, resetsAt: null },
|
||||
});
|
||||
state = chatStateWith(state, { connection: { rateLimit: previousRateLimit } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const actions = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport({
|
||||
readRateLimitMetadata: vi.fn().mockResolvedValue({
|
||||
value: null,
|
||||
probe: diagnosticProbeError("rateLimits", new Error("offline"), 1),
|
||||
}),
|
||||
}),
|
||||
...metadataCacheHost(),
|
||||
refreshAppServerMetadata: vi.fn().mockResolvedValue(null),
|
||||
isStaleSharedQueryError: () => false,
|
||||
});
|
||||
|
||||
await actions.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
|
||||
|
||||
expect(stateStore.getState().connection.rateLimit).toBe(previousRateLimit);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits).toMatchObject({ status: "failed" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("server diagnostics actions", () => {
|
||||
it("reuses refreshed app-server metadata for deferred diagnostics", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const refreshedMetadata = serverMetadataFixture({
|
||||
availableModels: [modelFixture("gpt-5.1")],
|
||||
availableSkills: [skillFixture("writer")],
|
||||
serverDiagnostics: diagnosticsWithProbe(
|
||||
diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 models", 1)),
|
||||
diagnosticProbeOk("skills", "1 skills", 1),
|
||||
),
|
||||
});
|
||||
const metadataCache = metadataCacheHost({ current: null });
|
||||
const metadata = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport(),
|
||||
...metadataCache,
|
||||
refreshAppServerMetadata: vi.fn().mockImplementation(async () => {
|
||||
metadataCache.updateAppServerMetadata(() => refreshedMetadata);
|
||||
return refreshedMetadata;
|
||||
}),
|
||||
isStaleSharedQueryError: () => false,
|
||||
});
|
||||
const readServerDiagnostics = vi.fn().mockResolvedValue(serverDiagnosticsSnapshot());
|
||||
const diagnostics = createServerDiagnosticsActions({
|
||||
stateStore,
|
||||
diagnosticsTransport: { readServerDiagnostics },
|
||||
...metadataCache,
|
||||
});
|
||||
|
||||
await metadata.refreshAppServerMetadata();
|
||||
await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true });
|
||||
|
||||
expect(readServerDiagnostics).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
appServerMetadataSnapshot: true,
|
||||
cachedSkills: [skillFixture("writer")],
|
||||
cachedSkillsProbe: expect.objectContaining({ status: "ok", summary: "1 skills" }),
|
||||
}),
|
||||
);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "1 models",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses metadata diagnostics as the default resource probe source", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const metadataCache = metadataCacheHost({
|
||||
current: serverMetadataFixture({
|
||||
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "cached models", 1)),
|
||||
}),
|
||||
});
|
||||
const readServerDiagnostics = vi.fn().mockResolvedValue(serverDiagnosticsSnapshot());
|
||||
const diagnostics = createServerDiagnosticsActions({
|
||||
stateStore,
|
||||
diagnosticsTransport: { readServerDiagnostics },
|
||||
...metadataCache,
|
||||
});
|
||||
|
||||
await diagnostics.refreshServerDiagnostics();
|
||||
|
||||
expect(readServerDiagnostics).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
appServerMetadataSnapshot: false,
|
||||
forceResourceProbes: false,
|
||||
}),
|
||||
);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "cached models",
|
||||
});
|
||||
});
|
||||
|
||||
it("can force resource probes for explicit health checks", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const readServerDiagnostics = vi.fn().mockResolvedValue(
|
||||
serverDiagnosticsSnapshot({
|
||||
resourceProbes: [diagnosticProbeOk("models", "1 models", 1), diagnosticProbeOk("rateLimits", "available", 1)],
|
||||
}),
|
||||
);
|
||||
const diagnostics = createServerDiagnosticsActions({
|
||||
stateStore,
|
||||
diagnosticsTransport: { readServerDiagnostics },
|
||||
...metadataCacheHost(),
|
||||
});
|
||||
|
||||
await diagnostics.refreshServerDiagnostics({ forceResourceProbes: true });
|
||||
|
||||
expect(readServerDiagnostics).toHaveBeenCalledWith(expect.objectContaining({ forceResourceProbes: true }));
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
|
||||
status: "ok",
|
||||
summary: "1 models",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not apply diagnostic probes when the transport returns no snapshot", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const updateAppServerMetadata = vi.fn(() => null);
|
||||
const diagnostics = createServerDiagnosticsActions({
|
||||
stateStore,
|
||||
diagnosticsTransport: { readServerDiagnostics: vi.fn().mockResolvedValue(null) },
|
||||
appServerMetadataSnapshot: () => null,
|
||||
updateAppServerMetadata,
|
||||
});
|
||||
|
||||
await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true });
|
||||
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes.mcpServers.status).toBe("unknown");
|
||||
expect(updateAppServerMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refreshes tool provider snapshots with cached MCP startup diagnostics", async () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { activeThread: { id: "thread-1" } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const metadataCache = metadataCacheHost({ current: serverMetadataFixture() });
|
||||
const metadata = createServerMetadataActions({
|
||||
stateStore,
|
||||
metadataResourceTransport: metadataResourceTransport(),
|
||||
...metadataCache,
|
||||
refreshAppServerMetadata: vi.fn().mockResolvedValue(null),
|
||||
isStaleSharedQueryError: () => false,
|
||||
});
|
||||
const diagnostics = createServerDiagnosticsActions({
|
||||
stateStore,
|
||||
diagnosticsTransport: {
|
||||
readServerDiagnostics: vi.fn().mockResolvedValue(
|
||||
serverDiagnosticsSnapshot({
|
||||
mcpServerStatuses: [mcpServerStatus()],
|
||||
}),
|
||||
),
|
||||
},
|
||||
...metadataCache,
|
||||
});
|
||||
|
||||
await metadata.applyAppServerResourceEvent({
|
||||
type: "mcp-startup-status-updated",
|
||||
name: "github",
|
||||
status: "ready",
|
||||
message: null,
|
||||
});
|
||||
await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true });
|
||||
|
||||
const sections = toolInventoryDiagnosticSections(stateStore.getState().connection.serverDiagnostics);
|
||||
const toolProviderRows = sections.find((section) => section.title === "Tool providers")?.rows ?? [];
|
||||
|
||||
expect(sections.map((section) => section.title)).toEqual(["Plugins", "Tool providers", "Skills"]);
|
||||
expect(toolProviderRows.map((row) => `${row.label}: ${row.value}`)).toEqual(["github: MCP server, ready, auth oAuth, 1 tool"]);
|
||||
});
|
||||
});
|
||||
|
||||
function metadataResourceTransport(overrides: Partial<MetadataResourceTransport> = {}): MetadataResourceTransport {
|
||||
return {
|
||||
readSkillMetadata: vi.fn().mockResolvedValue({ value: [], probe: diagnosticProbeOk("skills", "0 skills", 1) }),
|
||||
readRateLimitMetadata: vi.fn().mockResolvedValue({ value: null, probe: diagnosticProbeOk("rateLimits", "unavailable", 1) }),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function serverDiagnosticsSnapshot(
|
||||
overrides: { resourceProbes?: DiagnosticProbeResult[]; mcpServerStatuses?: McpServerStatusSummary[] | null } = {},
|
||||
) {
|
||||
return {
|
||||
resourceProbes: overrides.resourceProbes ?? [],
|
||||
toolInventory: {
|
||||
inventory: toolInventory(),
|
||||
probes: [
|
||||
diagnosticProbeOk("plugins", "0 plugins", 1),
|
||||
diagnosticProbeOk("mcpServers", "0 servers", 1),
|
||||
diagnosticProbeOk("skills", "0 skills", 1),
|
||||
],
|
||||
mcpServerStatuses: overrides.mcpServerStatuses ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toolInventory(): ToolInventorySnapshot {
|
||||
return {
|
||||
checkedAt: 1,
|
||||
plugins: [],
|
||||
pluginMarketplaceErrors: [],
|
||||
pluginsError: null,
|
||||
mcpServers: [],
|
||||
mcpDiagnostics: [],
|
||||
mcpError: null,
|
||||
skills: [],
|
||||
skillsError: null,
|
||||
};
|
||||
}
|
||||
|
||||
function modelFixture(model: string): ModelMetadata {
|
||||
return {
|
||||
id: model,
|
||||
model,
|
||||
displayName: model,
|
||||
description: "",
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: "medium",
|
||||
inputModalities: ["text"],
|
||||
additionalSpeedTiers: [],
|
||||
serviceTiers: [],
|
||||
defaultServiceTier: null,
|
||||
isDefault: false,
|
||||
};
|
||||
}
|
||||
|
||||
function skillFixture(name: string): SkillMetadata {
|
||||
return {
|
||||
name,
|
||||
description: "",
|
||||
path: `/skills/${name}`,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
function rateLimitFixture(overrides: Partial<RateLimitSnapshot> = {}): RateLimitSnapshot {
|
||||
return {
|
||||
limitId: "codex",
|
||||
limitName: "Codex",
|
||||
primary: null,
|
||||
secondary: null,
|
||||
individualLimit: null,
|
||||
rateLimitReachedType: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function serverMetadataFixture(overrides: Partial<SharedServerMetadata> = {}): SharedServerMetadata {
|
||||
return {
|
||||
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
||||
availableModels: [],
|
||||
availableSkills: [],
|
||||
availablePermissionProfiles: [],
|
||||
rateLimit: null,
|
||||
serverDiagnostics: createServerDiagnostics(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function metadataCacheHost(cache: { current: SharedServerMetadata | null } = { current: null }): {
|
||||
appServerMetadataSnapshot: () => SharedServerMetadata | null;
|
||||
updateAppServerMetadata: (updater: (metadata: SharedServerMetadata | null) => SharedServerMetadata | null) => SharedServerMetadata | null;
|
||||
} {
|
||||
return {
|
||||
appServerMetadataSnapshot: () => cache.current,
|
||||
updateAppServerMetadata: (updater) => {
|
||||
const next = updater(cache.current);
|
||||
cache.current = next;
|
||||
return next;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mcpServerStatus(): McpServerStatusSummary {
|
||||
return {
|
||||
name: "github",
|
||||
authStatus: "oAuth",
|
||||
toolCount: 1,
|
||||
resourceCount: 0,
|
||||
resourceTemplateCount: 0,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
|
||||
import type { ThreadActivationSnapshot } from "../../../../../src/domain/threads/activation";
|
||||
import type { Thread } from "../../../../../src/domain/threads/model";
|
||||
import { runtimeSnapshotForChatState } from "../../../../../src/features/chat/application/runtime/snapshot";
|
||||
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
|
||||
import { createThreadStartActions } from "../../../../../src/features/chat/application/threads/thread-start-actions";
|
||||
import { setCollaborationModeIntent } from "../../../../../src/features/chat/domain/runtime/intent";
|
||||
import { chatStateFixture, chatStateWith } from "../../support/state";
|
||||
|
||||
describe("thread start actions", () => {
|
||||
it("publishes newly started threads before the first turn completes", async () => {
|
||||
let state = chatStateFixture();
|
||||
const existing = threadFixture("existing");
|
||||
state = chatStateWith(state, { threadList: { listedThreads: [existing] } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const started = threadFixture("started");
|
||||
const optimistic = { ...started, preview: "first prompt" };
|
||||
const recordStartedThread = vi.fn();
|
||||
const syncThreadGoal = vi.fn();
|
||||
|
||||
const actions = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: { startThread: vi.fn().mockResolvedValue(activationFixture(started)) },
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread,
|
||||
syncThreadGoal,
|
||||
});
|
||||
|
||||
await actions.startThread("first prompt");
|
||||
|
||||
expect(stateStore.getState().threadList.listedThreads).toEqual([optimistic, existing]);
|
||||
expect(recordStartedThread).toHaveBeenCalledWith(optimistic);
|
||||
expect(syncThreadGoal).toHaveBeenCalledWith("started");
|
||||
});
|
||||
|
||||
it("keeps empty-panel runtime reservations when starting the first thread", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
stateStore.dispatch({ type: "runtime/model-requested", model: "gpt-5.5" });
|
||||
stateStore.dispatch({ type: "runtime/permission-profile-requested", permissionProfile: ":workspace" });
|
||||
stateStore.dispatch({ type: "runtime/reasoning-effort-requested", effort: "high" });
|
||||
stateStore.dispatch({ type: "runtime/fast-mode-requested", fastMode: "enabled" });
|
||||
stateStore.dispatch({ type: "runtime/approvals-reviewer-requested", approvalsReviewer: "auto_review" });
|
||||
stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "plan" });
|
||||
const startThread = vi.fn().mockResolvedValue(
|
||||
activationFixture(threadFixture("started"), {
|
||||
model: "gpt-5",
|
||||
serviceTier: "fast",
|
||||
}),
|
||||
);
|
||||
|
||||
const actions = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: { startThread },
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread: vi.fn(),
|
||||
syncThreadGoal: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.startThread("first prompt");
|
||||
|
||||
expect(startThread).toHaveBeenCalledWith({
|
||||
permissions: ":workspace",
|
||||
serviceTier: "fast",
|
||||
});
|
||||
expect(stateStore.getState().runtime.active.model).toBe("gpt-5");
|
||||
expect(stateStore.getState().runtime.pending.model).toEqual({ kind: "set", value: "gpt-5.5" });
|
||||
expect(stateStore.getState().runtime.pending.permissionProfile).toEqual({ kind: "set", value: ":workspace" });
|
||||
expect(stateStore.getState().runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
|
||||
expect(stateStore.getState().runtime.pending.fastMode).toEqual({ kind: "set", value: "enabled" });
|
||||
expect(stateStore.getState().runtime.pending.approvalsReviewer).toEqual({ kind: "set", value: "auto_review" });
|
||||
expect(stateStore.getState().runtime.pending.collaborationMode).toEqual(setCollaborationModeIntent("plan"));
|
||||
});
|
||||
|
||||
it("can skip newly started thread goal sync when the caller sets the first goal", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const syncThreadGoal = vi.fn();
|
||||
const actions = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: { startThread: vi.fn().mockResolvedValue(activationFixture(threadFixture("started"))) },
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread: vi.fn(),
|
||||
syncThreadGoal,
|
||||
});
|
||||
|
||||
await actions.startThread("first goal", { syncGoal: false });
|
||||
|
||||
expect(syncThreadGoal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts threads with service tier from explicit effective config", async () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { connection: { runtimeConfig: { ...emptyRuntimeConfigSnapshot(), serviceTier: "flex" } } });
|
||||
const stateStore = createChatStateStore(state);
|
||||
const startThread = vi.fn().mockResolvedValue(activationFixture(threadFixture("started"), { serviceTier: "flex" }));
|
||||
const actions = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: { startThread },
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread: vi.fn(),
|
||||
syncThreadGoal: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.startThread();
|
||||
|
||||
expect(startThread).toHaveBeenCalledWith({ serviceTier: "flex" });
|
||||
});
|
||||
|
||||
it("starts threads with permission profile from explicit config", async () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, {
|
||||
connection: {
|
||||
runtimeConfig: {
|
||||
...emptyRuntimeConfigSnapshot(),
|
||||
startupPermissions: {
|
||||
...emptyRuntimeConfigSnapshot().startupPermissions,
|
||||
activePermissionProfile: { id: ":workspace", extends: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const stateStore = createChatStateStore(state);
|
||||
const startThread = vi.fn().mockResolvedValue(activationFixture(threadFixture("started")));
|
||||
const actions = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: { startThread },
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread: vi.fn(),
|
||||
syncThreadGoal: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.startThread();
|
||||
|
||||
expect(startThread).toHaveBeenCalledWith({ permissions: ":workspace" });
|
||||
});
|
||||
|
||||
it("keeps app-server preview when newly started threads already have one", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const started = threadFixture("started", { preview: "server preview" });
|
||||
const recordStartedThread = vi.fn();
|
||||
const actions = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: { startThread: vi.fn().mockResolvedValue(activationFixture(started)) },
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread,
|
||||
syncThreadGoal: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.startThread("local preview");
|
||||
|
||||
expect(recordStartedThread).toHaveBeenCalledWith(started);
|
||||
});
|
||||
|
||||
it("does not apply newly started threads after the transport returns no activation", async () => {
|
||||
const stateStore = createChatStateStore(chatStateFixture());
|
||||
const recordStartedThread = vi.fn();
|
||||
const syncThreadGoal = vi.fn();
|
||||
const actions = createThreadStartActions({
|
||||
stateStore,
|
||||
threadStartTransport: { startThread: vi.fn().mockResolvedValue(null) },
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
recordStartedThread,
|
||||
syncThreadGoal,
|
||||
});
|
||||
|
||||
await expect(actions.startThread("local preview")).resolves.toBeNull();
|
||||
expect(stateStore.getState().activeThread.id).toBeNull();
|
||||
expect(stateStore.getState().threadList.listedThreads).toEqual([]);
|
||||
expect(recordStartedThread).not.toHaveBeenCalled();
|
||||
expect(syncThreadGoal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function threadFixture(id: string, overrides: Partial<Thread> = {}): Thread {
|
||||
return {
|
||||
id,
|
||||
preview: "",
|
||||
name: null,
|
||||
archived: false,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
recencyAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function activationFixture(thread: Thread, overrides: Partial<ThreadActivationSnapshot> = {}): ThreadActivationSnapshot {
|
||||
return {
|
||||
thread,
|
||||
cwd: "/vault",
|
||||
model: "gpt-5",
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
reasoningEffort: null,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
approvalPolicyKnown: false,
|
||||
sandboxPolicyKnown: false,
|
||||
permissionProfileKnown: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ function turnBundleFixture(options: { stateStore?: ReturnType<typeof createChatS
|
|||
focusComposer: vi.fn(),
|
||||
},
|
||||
runtimeSettings: {},
|
||||
serverThreads: {},
|
||||
threadStart: {},
|
||||
goals: {},
|
||||
autoTitleCoordinator: { resetThreadTurnPresence: vi.fn() },
|
||||
reconnect: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -663,12 +663,12 @@ export const allowed = true;
|
|||
await writeFile(
|
||||
path.join(cwd, "src/features/chat/panel/outer.tsx"),
|
||||
`
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ChatServerActionsHost } from "../app-server/actions/host";
|
||||
import type { Host } from "../host/contracts";
|
||||
|
||||
export type Escape = AppServerClient | ChatServerActionsHost | Host;
|
||||
`.trimStart(),
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ChatAppServerGateway } from "../app-server/session-gateway";
|
||||
import type { Host } from "../host/contracts";
|
||||
|
||||
export type Escape = AppServerClient | ChatAppServerGateway | Host;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/features/chat/panel/allowed.tsx"),
|
||||
|
|
@ -683,16 +683,16 @@ export const toolbar = Toolbar;
|
|||
await writeFile(
|
||||
path.join(cwd, "src/features/chat/presentation/outer.ts"),
|
||||
`
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { ChatServerActionsHost } from "../app-server/actions/host";
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
import { Toolbar } from "../ui/toolbar";
|
||||
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatServerActionsHost | Host | ToolbarPanelActions;
|
||||
export const toolbar = Toolbar;
|
||||
`.trimStart(),
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { ChatAppServerGateway } from "../app-server/session-gateway";
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
import { Toolbar } from "../ui/toolbar";
|
||||
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatAppServerGateway | Host | ToolbarPanelActions;
|
||||
export const toolbar = Toolbar;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/features/chat/presentation/allowed.ts"),
|
||||
|
|
@ -706,14 +706,14 @@ export type Allowed = Thread | MessageStreamItem;
|
|||
await writeFile(
|
||||
path.join(cwd, "src/features/chat/ui/outer.tsx"),
|
||||
`
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { ChatServerActionsHost } from "../app-server/actions/host";
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatServerActionsHost | Host | ToolbarPanelActions;
|
||||
`.trimStart(),
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { ChatAppServerGateway } from "../app-server/session-gateway";
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatAppServerGateway | Host | ToolbarPanelActions;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/features/chat/ui/allowed.tsx"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue