mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Refine chat session composition ownership
This commit is contained in:
parent
39cabcd99d
commit
6fe1a837c8
26 changed files with 539 additions and 690 deletions
21
src/app-server/services/runtime-overrides.ts
Normal file
21
src/app-server/services/runtime-overrides.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { RuntimeOverride, RuntimeOverrideSettings } from "../../domain/runtime/overrides";
|
||||
import { runtimeOverride, validatedRuntimeOverrideForModelMetadata } from "../../domain/runtime/overrides";
|
||||
import type { ModelListResponse } from "../../generated/app-server/v2/ModelListResponse";
|
||||
import { listModelMetadata } from "./catalog";
|
||||
|
||||
interface RuntimeOverrideModelClient {
|
||||
listModels(includeHidden: boolean): Promise<ModelListResponse>;
|
||||
}
|
||||
|
||||
export async function resolvedRuntimeOverrideForClient(
|
||||
client: RuntimeOverrideModelClient,
|
||||
settings: RuntimeOverrideSettings,
|
||||
): Promise<RuntimeOverride> {
|
||||
const runtime = runtimeOverride(settings);
|
||||
if (!runtime.model || !runtime.effort) return runtime;
|
||||
try {
|
||||
return validatedRuntimeOverrideForModelMetadata(settings, await listModelMetadata(client));
|
||||
} catch {
|
||||
return runtime;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,20 +8,12 @@ export type { SharedServerMetadata } from "../../domain/server/metadata";
|
|||
export interface SharedAppServerCacheContext {
|
||||
codexPath: string;
|
||||
vaultPath: string;
|
||||
appServerUserAgent: string | null;
|
||||
}
|
||||
|
||||
type SharedThreadListSnapshotStatus = "ready" | "stale" | "unavailable";
|
||||
|
||||
interface SharedThreadListSnapshot {
|
||||
status: SharedThreadListSnapshotStatus;
|
||||
threads: readonly Thread[];
|
||||
}
|
||||
|
||||
type SharedCache<T> = { kind: "unloaded" } | { kind: "loaded"; context: SharedAppServerCacheContext; data: T };
|
||||
|
||||
export interface SharedAppServerState {
|
||||
threads: SharedCache<SharedThreadListSnapshot>;
|
||||
threads: SharedCache<readonly Thread[]>;
|
||||
appServerMetadata: SharedCache<SharedServerMetadata>;
|
||||
availableModels: SharedCache<readonly ModelMetadata[]>;
|
||||
}
|
||||
|
|
@ -45,7 +37,7 @@ export function applySharedThreadList(
|
|||
threads: {
|
||||
kind: "loaded",
|
||||
context: cloneSharedAppServerCacheContext(context),
|
||||
data: cloneSharedThreadListSnapshot({ status: "ready", threads }),
|
||||
data: cloneThreads(threads),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -99,8 +91,7 @@ export function applySharedModels(
|
|||
export function cachedSharedThreadList(state: SharedAppServerState, context: SharedAppServerCacheContext): readonly Thread[] | null {
|
||||
if (!sharedAppServerCacheContextIsComplete(context)) return null;
|
||||
if (state.threads.kind !== "loaded" || !sharedAppServerCacheContextMatches(state.threads.context, context)) return null;
|
||||
if (state.threads.data.status !== "ready") return null;
|
||||
return cloneThreads(state.threads.data.threads);
|
||||
return cloneThreads(state.threads.data);
|
||||
}
|
||||
|
||||
export function cachedSharedServerMetadata(state: SharedAppServerState, context: SharedAppServerCacheContext): SharedServerMetadata | null {
|
||||
|
|
@ -175,10 +166,6 @@ function mergeServerDiagnostics(previous: SharedServerMetadata, next: SharedServ
|
|||
};
|
||||
}
|
||||
|
||||
function cloneSharedThreadListSnapshot(snapshot: SharedThreadListSnapshot): SharedThreadListSnapshot {
|
||||
return { status: snapshot.status, threads: cloneThreads(snapshot.threads) };
|
||||
}
|
||||
|
||||
function cloneRateLimitSnapshot(snapshot: RateLimitSnapshot): RateLimitSnapshot {
|
||||
return {
|
||||
...snapshot,
|
||||
|
|
@ -215,18 +202,12 @@ export function sharedAppServerCacheContextMatches(left: SharedAppServerCacheCon
|
|||
sharedAppServerCacheContextIsComplete(left) &&
|
||||
sharedAppServerCacheContextIsComplete(right) &&
|
||||
left.codexPath === right.codexPath &&
|
||||
left.vaultPath === right.vaultPath &&
|
||||
left.appServerUserAgent === right.appServerUserAgent
|
||||
left.vaultPath === right.vaultPath
|
||||
);
|
||||
}
|
||||
|
||||
export function sharedAppServerCacheContextIsComplete(context: SharedAppServerCacheContext): boolean {
|
||||
return (
|
||||
nonEmptyString(context.codexPath) &&
|
||||
nonEmptyString(context.vaultPath) &&
|
||||
context.appServerUserAgent !== null &&
|
||||
nonEmptyString(context.appServerUserAgent)
|
||||
);
|
||||
return nonEmptyString(context.codexPath) && nonEmptyString(context.vaultPath);
|
||||
}
|
||||
|
||||
function nonEmptyString(value: string): boolean {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,11 @@ import {
|
|||
type EphemeralStructuredTurnRuntimeClient,
|
||||
type StructuredTurnOutputSchema,
|
||||
} from "./ephemeral-structured-turn";
|
||||
import { listModelMetadata } from "./catalog";
|
||||
import { resolvedRuntimeOverrideForClient } from "./runtime-overrides";
|
||||
import { conversationAssistantTextFromTurnRecord, type TurnRecord } from "../protocol/turn";
|
||||
import type { ModelMetadata, ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import { runtimeOverride, validatedRuntimeOverrideForModelMetadata } from "../../domain/runtime/overrides";
|
||||
import {
|
||||
threadTitleFromGeneratedText,
|
||||
threadTitlePrompt,
|
||||
type ThreadTitleContext,
|
||||
} from "../../domain/threads/title-generation-model";
|
||||
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import type { RuntimeOverride } from "../../domain/runtime/overrides";
|
||||
import { threadTitleFromGeneratedText, threadTitlePrompt, type ThreadTitleContext } from "../../domain/threads/title-generation-model";
|
||||
|
||||
const THREAD_TITLE_SERVICE_NAME = "codex-panel-naming";
|
||||
const THREAD_TITLE_TIMEOUT_MS = 60_000;
|
||||
|
|
@ -71,36 +67,16 @@ export async function generateThreadTitleWithCodex(
|
|||
return threadTitleFromGenerationTurn(turn);
|
||||
}
|
||||
|
||||
interface ThreadTitleRuntimeOverride {
|
||||
model?: string;
|
||||
effort?: ReasoningEffort;
|
||||
}
|
||||
type ThreadTitleRuntimeOverride = RuntimeOverride;
|
||||
|
||||
function threadTitleFromGenerationTurn(turn: TurnRecord): string | null {
|
||||
const response = conversationAssistantTextFromTurnRecord(turn);
|
||||
return response ? threadTitleFromGeneratedText(response) : null;
|
||||
}
|
||||
|
||||
function threadTitleRuntimeOverride(settings: ThreadTitleRuntimeSettings): ThreadTitleRuntimeOverride {
|
||||
return runtimeOverride({ model: settings.threadNamingModel, effort: settings.threadNamingEffort });
|
||||
}
|
||||
|
||||
function validatedThreadTitleRuntimeOverride(
|
||||
settings: ThreadTitleRuntimeSettings,
|
||||
models: readonly ModelMetadata[],
|
||||
): ThreadTitleRuntimeOverride {
|
||||
return validatedRuntimeOverrideForModelMetadata({ model: settings.threadNamingModel, effort: settings.threadNamingEffort }, models);
|
||||
}
|
||||
|
||||
async function threadTitleRuntimeOverrideForClient(
|
||||
client: EphemeralStructuredTurnRuntimeClient,
|
||||
settings: ThreadTitleRuntimeSettings,
|
||||
): Promise<ThreadTitleRuntimeOverride> {
|
||||
const runtime = threadTitleRuntimeOverride(settings);
|
||||
if (!runtime.model || !runtime.effort) return runtime;
|
||||
try {
|
||||
return validatedThreadTitleRuntimeOverride(settings, await listModelMetadata(client));
|
||||
} catch {
|
||||
return runtime;
|
||||
}
|
||||
return resolvedRuntimeOverrideForClient(client, { model: settings.threadNamingModel, effort: settings.threadNamingEffort });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export interface ChatConnectionControllerHost {
|
|||
resetThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
setStatus: (statusText: string, phase?: ChatConnectionPhase) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
publishAppServerIdentity: (userAgent: string | null) => void;
|
||||
configuredCommand: () => string;
|
||||
refreshLiveState: () => void;
|
||||
notifyConnectionFailed: () => void;
|
||||
|
|
@ -75,7 +74,6 @@ export class ChatConnectionController {
|
|||
handleExit(): void {
|
||||
this.invalidate();
|
||||
this.host.invalidateResumeWork();
|
||||
this.host.publishAppServerIdentity(null);
|
||||
this.host.setStatus(STATUS_CONNECTION_STOPPED, { kind: "disconnected", message: STATUS_CONNECTION_STOPPED });
|
||||
this.host.stateStore.dispatch({ type: "connection/scoped-cleared" });
|
||||
this.host.resetThreadTurnPresence(false);
|
||||
|
|
@ -116,12 +114,10 @@ export class ChatConnectionController {
|
|||
}
|
||||
|
||||
private async initializeConnection(connection: ActiveChatConnection): Promise<void> {
|
||||
this.host.publishAppServerIdentity(null);
|
||||
this.host.setStatus(STATUS_CONNECTION_STARTING, { kind: "connecting" });
|
||||
try {
|
||||
const initialization = await this.host.connection.connect();
|
||||
if (this.host.connectionWork.isStale(connection)) return;
|
||||
this.host.publishAppServerIdentity(initialization.userAgent);
|
||||
this.host.stateStore.dispatch({ type: "connection/initialized", initializeResponse: initialization });
|
||||
const client = this.host.connection.currentClient();
|
||||
if (!client) throw new Error("Codex app-server connection did not initialize.");
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export interface CodexChatHost {
|
|||
readonly workspace: WorkspacePanels;
|
||||
readonly sharedCache: SharedAppServerCacheFacade;
|
||||
readonly threadSurfaces: ThreadSurfaceBroadcaster;
|
||||
readonly appServerIdentity: AppServerIdentityPublisher;
|
||||
}
|
||||
|
||||
export interface PluginSettingsRef {
|
||||
|
|
@ -36,7 +35,3 @@ interface SharedAppServerCacheFacade {
|
|||
cachedThreadList(): readonly Thread[] | null;
|
||||
cachedAppServerMetadata(): SharedServerMetadata | null;
|
||||
}
|
||||
|
||||
interface AppServerIdentityPublisher {
|
||||
publishAppServerIdentity(userAgent: string | null): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ interface ThreadPartsContext {
|
|||
getClosing: () => boolean;
|
||||
};
|
||||
thread: {
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
resumeRestoredThread: (threadId: string) => Promise<void>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
notifyIdentityChanged: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
|
|
@ -96,34 +94,6 @@ export function createThreadParts(context: ThreadPartsContext) {
|
|||
},
|
||||
});
|
||||
|
||||
const threadManagementHost: ThreadManagementActionsHost = {
|
||||
stateStore,
|
||||
vaultPath: settingsRef.vaultPath,
|
||||
settings: () => settingsRef.settings,
|
||||
archiveAdapter: obsidian.archiveAdapter,
|
||||
ensureConnected: client.ensureConnected,
|
||||
currentClient,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
showNotice: notify.showNotice,
|
||||
setStatus: status.set,
|
||||
setComposerText: composer.setText,
|
||||
openThreadInNewView: (threadId) => workspace.openThreadInNewView(threadId),
|
||||
openThreadInCurrentPanel: (threadId) => thread.selectThread(threadId),
|
||||
notifyThreadArchived: (threadId) => {
|
||||
threadSurfaces.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
threadSurfaces.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
notifyActiveThreadIdentityChanged: () => {
|
||||
thread.notifyIdentityChanged();
|
||||
},
|
||||
refreshThreads: () => thread.refreshThreads(),
|
||||
refreshSharedThreadListFromOpenSurface: () => {
|
||||
threadSurfaces.refreshSharedThreadListFromOpenSurface();
|
||||
},
|
||||
};
|
||||
const managementActions = createThreadManagementActions(threadManagementHost);
|
||||
const goals = createGoalActions({
|
||||
stateStore,
|
||||
currentClient,
|
||||
|
|
@ -143,7 +113,6 @@ export function createThreadParts(context: ThreadPartsContext) {
|
|||
},
|
||||
lifecycle,
|
||||
thread: {
|
||||
resumeRestoredThread: thread.resumeRestoredThread,
|
||||
notifyIdentityChanged: thread.notifyIdentityChanged,
|
||||
refreshTabHeader: thread.refreshTabHeader,
|
||||
},
|
||||
|
|
@ -156,6 +125,34 @@ export function createThreadParts(context: ThreadPartsContext) {
|
|||
},
|
||||
});
|
||||
const { history, restoration, resume, identity } = threadLifecycle;
|
||||
const threadManagementHost: ThreadManagementActionsHost = {
|
||||
stateStore,
|
||||
vaultPath: settingsRef.vaultPath,
|
||||
settings: () => settingsRef.settings,
|
||||
archiveAdapter: obsidian.archiveAdapter,
|
||||
ensureConnected: client.ensureConnected,
|
||||
currentClient,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
showNotice: notify.showNotice,
|
||||
setStatus: status.set,
|
||||
setComposerText: composer.setText,
|
||||
openThreadInNewView: (threadId) => workspace.openThreadInNewView(threadId),
|
||||
openThreadInCurrentPanel: (threadId) => resume.resumeThread(threadId),
|
||||
notifyThreadArchived: (threadId) => {
|
||||
threadSurfaces.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
threadSurfaces.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
notifyActiveThreadIdentityChanged: () => {
|
||||
thread.notifyIdentityChanged();
|
||||
},
|
||||
refreshThreads: () => thread.refreshThreads(),
|
||||
refreshSharedThreadListFromOpenSurface: () => {
|
||||
threadSurfaces.refreshSharedThreadListFromOpenSurface();
|
||||
},
|
||||
};
|
||||
const managementActions = createThreadManagementActions(threadManagementHost);
|
||||
|
||||
return {
|
||||
history,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export interface ThreadLifecyclePartsContext {
|
|||
getClosing: () => boolean;
|
||||
};
|
||||
thread: {
|
||||
resumeRestoredThread: (threadId: string) => Promise<void>;
|
||||
notifyIdentityChanged: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
};
|
||||
|
|
@ -64,16 +63,17 @@ export function createThreadLifecycleParts(context: ThreadLifecyclePartsContext)
|
|||
resumeWork.invalidate();
|
||||
history.invalidate();
|
||||
};
|
||||
let resume: ResumeController;
|
||||
const restoration = new RestorationController({
|
||||
deferredTasks,
|
||||
opened: lifecycle.getOpened,
|
||||
resumeThread: thread.resumeRestoredThread,
|
||||
resumeThread: (threadId) => resume.resumeThread(threadId),
|
||||
invalidateResumeWork,
|
||||
stateStore,
|
||||
setStatus: status.set,
|
||||
refreshTabHeader: thread.refreshTabHeader,
|
||||
});
|
||||
const resume = new ResumeController({
|
||||
resume = new ResumeController({
|
||||
stateStore,
|
||||
vaultPath: settingsRef.vaultPath,
|
||||
resumeWork,
|
||||
|
|
|
|||
206
src/features/chat/host/connection-bundle.ts
Normal file
206
src/features/chat/host/connection-bundle.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import { Notice } from "obsidian";
|
||||
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import type { ConnectionManager, ConnectionManagerHandlers } from "../../../app-server/connection/connection-manager";
|
||||
import type { ThreadSurfaceBroadcaster } from "../application/ports/chat-host";
|
||||
import type { ChatConnectionWorkTracker, ChatViewDeferredTasks } from "../application/lifecycle";
|
||||
import { ChatConnectionController } from "../application/connection/connection-controller";
|
||||
import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics";
|
||||
import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../app-server/actions/metadata";
|
||||
import { createChatServerThreadActions, type ChatServerThreadActions } from "../app-server/actions/threads";
|
||||
import { ChatInboundController } from "../app-server/inbound/controller";
|
||||
import { rejectServerRequest, respondToServerRequest } from "../app-server/requests/responder";
|
||||
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
|
||||
import type { ChatConnectionPhase } from "../application/state/root-reducer";
|
||||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { GoalActions } from "../application/threads/goal-actions";
|
||||
import type { AutoTitleController } from "../application/threads/auto-title-controller";
|
||||
import type { MessageStreamNoticeSection } from "../domain/message-stream/items";
|
||||
|
||||
export interface ChatConnectionBundle {
|
||||
connectionController: ChatConnectionController;
|
||||
inboundController: ChatInboundController;
|
||||
serverActions: {
|
||||
threads: ChatServerThreadActions;
|
||||
metadata: ChatServerMetadataActions;
|
||||
diagnostics: ChatServerDiagnosticsActions;
|
||||
};
|
||||
}
|
||||
|
||||
interface ChatConnectionClientPorts {
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
interface ChatConnectionRefreshPorts {
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ChatConnectionBundleStatus {
|
||||
set: (statusText: string, phase?: ChatConnectionPhase) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
}
|
||||
|
||||
export interface ChatConnectionBundleContext {
|
||||
stateStore: ChatStateStore;
|
||||
vaultPath: string;
|
||||
connection: ConnectionManager;
|
||||
connectionWork: ChatConnectionWorkTracker;
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
threadSurfaces: ThreadSurfaceBroadcaster;
|
||||
client: ChatConnectionClientPorts;
|
||||
refresh: ChatConnectionRefreshPorts;
|
||||
goals: GoalActions;
|
||||
autoTitle: AutoTitleController;
|
||||
status: ChatConnectionBundleStatus;
|
||||
invalidateResumeWork: () => void;
|
||||
loadSharedThreadList: () => Promise<void>;
|
||||
refreshDeferredDiagnostics: () => Promise<void>;
|
||||
refreshTabHeader: () => void;
|
||||
refreshLiveState: () => void;
|
||||
configuredCommand: () => string;
|
||||
}
|
||||
|
||||
export interface ChatConnectionEventTargets {
|
||||
inbound: ChatInboundController;
|
||||
connectionController: ChatConnectionController;
|
||||
}
|
||||
|
||||
export function createChatConnectionBundle(context: ChatConnectionBundleContext): ChatConnectionBundle {
|
||||
const { stateStore, vaultPath, connection, connectionWork, deferredTasks, threadSurfaces, client, refresh, goals, autoTitle, status } =
|
||||
context;
|
||||
const serverMetadata = createChatServerMetadataActions({
|
||||
stateStore,
|
||||
vaultPath,
|
||||
currentClient: client.currentClient,
|
||||
publishAppServerMetadata: (metadata) => {
|
||||
threadSurfaces.publishAppServerMetadata(metadata);
|
||||
},
|
||||
});
|
||||
const serverDiagnostics = createChatServerDiagnosticsActions({
|
||||
stateStore,
|
||||
vaultPath,
|
||||
currentClient: client.currentClient,
|
||||
publishAppServerMetadata: (metadata) => {
|
||||
threadSurfaces.publishAppServerMetadata(metadata);
|
||||
},
|
||||
serverMetadataSnapshot: () => serverMetadata.serverMetadataSnapshot(),
|
||||
});
|
||||
const serverThreads = createChatServerThreadActions({
|
||||
stateStore,
|
||||
vaultPath,
|
||||
currentClient: client.currentClient,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
publishThreadList: (threads) => {
|
||||
threadSurfaces.applyThreadListSnapshot(threads);
|
||||
},
|
||||
syncThreadGoal: (threadId) => {
|
||||
void goals.syncThreadGoal(threadId);
|
||||
},
|
||||
});
|
||||
const serverRequestHost = {
|
||||
currentClient: client.currentClient,
|
||||
};
|
||||
const inboundController = new ChatInboundController(stateStore, {
|
||||
refreshThreads: () => {
|
||||
void refresh.refreshThreads();
|
||||
},
|
||||
refreshRateLimits: () => {
|
||||
void serverMetadata.refreshPublishedRateLimits();
|
||||
},
|
||||
refreshSkills: (forceReload) => void refresh.refreshSkills(forceReload),
|
||||
publishAppServerMetadata: () => {
|
||||
serverMetadata.publishAppServerMetadataSnapshot();
|
||||
},
|
||||
maybeNameThread: (threadId, turnId, completedSummary) => {
|
||||
autoTitle.maybeAutoTitleThread(threadId, turnId, completedSummary);
|
||||
},
|
||||
notifyThreadArchived: (threadId) => {
|
||||
threadSurfaces.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
threadSurfaces.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
recordMcpStartupStatus: (name, mcpStatus, message) => {
|
||||
serverDiagnostics.recordMcpStartupStatus(name, mcpStatus, message);
|
||||
},
|
||||
respondToServerRequest: (requestId, result) => respondToServerRequest(serverRequestHost, requestId, result),
|
||||
rejectServerRequest: (requestId, code, message) => rejectServerRequest(serverRequestHost, requestId, code, message),
|
||||
});
|
||||
const connectionController = new ChatConnectionController({
|
||||
stateStore,
|
||||
connection,
|
||||
connectionWork,
|
||||
metadata: {
|
||||
refreshPublishedAppServerMetadata: () => serverMetadata.refreshPublishedAppServerMetadata(),
|
||||
refreshPublishedSkills: (forceReload) => serverMetadata.refreshPublishedSkills(forceReload),
|
||||
},
|
||||
diagnostics: {
|
||||
refreshPublishedDiagnosticProbes: () => serverDiagnostics.refreshPublishedDiagnosticProbes(),
|
||||
},
|
||||
invalidateResumeWork: context.invalidateResumeWork,
|
||||
loadSharedThreadList: context.loadSharedThreadList,
|
||||
scheduleDeferredDiagnostics: () => {
|
||||
deferredTasks.scheduleDiagnostics(() => {
|
||||
void context.refreshDeferredDiagnostics();
|
||||
});
|
||||
},
|
||||
clearDeferredDiagnostics: () => {
|
||||
deferredTasks.clearDiagnostics();
|
||||
},
|
||||
refreshTabHeader: context.refreshTabHeader,
|
||||
resetThreadTurnPresence: (hadTurns) => {
|
||||
autoTitle.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
setStatus: status.set,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
configuredCommand: context.configuredCommand,
|
||||
refreshLiveState: context.refreshLiveState,
|
||||
notifyConnectionFailed: () => {
|
||||
new Notice("Codex app-server connection failed.");
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
connectionController,
|
||||
inboundController,
|
||||
serverActions: {
|
||||
threads: serverThreads,
|
||||
metadata: serverMetadata,
|
||||
diagnostics: serverDiagnostics,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatConnectionEventRouter(callbacks: { deferLiveStateRefresh: () => void }): {
|
||||
handlers: ConnectionManagerHandlers;
|
||||
attach: (targets: ChatConnectionEventTargets) => void;
|
||||
} {
|
||||
let targets: ChatConnectionEventTargets | null = null;
|
||||
const currentTargets = () => {
|
||||
if (!targets) throw new Error("Codex app-server connection event received before chat session parts were initialized.");
|
||||
return targets;
|
||||
};
|
||||
return {
|
||||
handlers: {
|
||||
onNotification: (notification) => {
|
||||
currentTargets().inbound.handleNotification(notification);
|
||||
callbacks.deferLiveStateRefresh();
|
||||
},
|
||||
onServerRequest: (request) => {
|
||||
currentTargets().inbound.handleServerRequest(request);
|
||||
callbacks.deferLiveStateRefresh();
|
||||
},
|
||||
onLog: (message) => {
|
||||
currentTargets().inbound.handleAppServerLog(message);
|
||||
},
|
||||
onExit: () => {
|
||||
currentTargets().connectionController.handleExit();
|
||||
},
|
||||
},
|
||||
attach: (nextTargets) => {
|
||||
targets = nextTargets;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { Notice, type App, type Component, type EventRef } from "obsidian";
|
||||
|
||||
import { ConnectionManager, type ConnectionManagerHandlers } from "../../../app-server/connection/connection-manager";
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
import { ConnectionManager } from "../../../app-server/connection/connection-manager";
|
||||
import type { ModelMetadata } from "../../../domain/catalog/metadata";
|
||||
import type { Thread } from "../../../domain/threads/model";
|
||||
import { getThreadTitle } from "../../../domain/threads/model";
|
||||
|
|
@ -10,11 +9,8 @@ import { shortThreadId } from "../../../utils";
|
|||
import type { OpenCodexPanelSnapshot } from "../../../workspace/open-panel-snapshot";
|
||||
import type { ArchiveExportAdapter } from "../../../domain/threads/archive-markdown";
|
||||
import type { CodexChatHost } from "../application/ports/chat-host";
|
||||
import { ChatConnectionController } from "../application/connection/connection-controller";
|
||||
import type { ChatConnectionController } from "../application/connection/connection-controller";
|
||||
import { reconnectPanel, type ChatReconnectActionsHost } from "../application/connection/reconnect-actions";
|
||||
import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics";
|
||||
import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../app-server/actions/metadata";
|
||||
import { createChatServerThreadActions, type ChatServerThreadActions } from "../app-server/actions/threads";
|
||||
import type { ChatComposerController } from "../panel/composer-controller";
|
||||
import { createConversationParts } from "./conversation";
|
||||
import type { ComposerSubmitActions } from "../application/conversation/composer-submit-actions";
|
||||
|
|
@ -31,8 +27,6 @@ import { ChatConnectionWorkTracker, ChatResumeWorkTracker, type ChatViewDeferred
|
|||
import { createChatPanelToolbarActions, createToolbarPanelActions, type ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
import { connectionDiagnosticsModel } from "../panel/surface/toolbar-projection";
|
||||
import { openPanelTurnLifecycle, parseRestoredThreadState } from "../panel/snapshot";
|
||||
import { ChatInboundController } from "../app-server/inbound/controller";
|
||||
import { rejectServerRequest, respondToServerRequest } from "../app-server/requests/responder";
|
||||
import { collaborationModeLabel as formatCollaborationModeLabel } from "../presentation/runtime/messages";
|
||||
import { createChatRuntimeSettingsActions } from "../application/runtime/settings-actions";
|
||||
import { runtimeSnapshotForChatState, type RuntimeSnapshot } from "../application/runtime/snapshot";
|
||||
|
|
@ -41,19 +35,17 @@ import { createChatMessageScrollIntentState, type ChatMessageScrollIntentState }
|
|||
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell";
|
||||
import { chatTurnBusy, type ChatAction, type ChatConnectionPhase, type ChatState } from "../application/state/root-reducer";
|
||||
import { createChatStateStore, type ChatStateStore } from "../application/state/store";
|
||||
import type { GoalActions } from "../application/threads/goal-actions";
|
||||
import type { AutoTitleController } from "../application/threads/auto-title-controller";
|
||||
import type { HistoryController } from "../application/threads/history-controller";
|
||||
import type { IdentitySync } from "../application/threads/identity-sync";
|
||||
import type { ThreadRenameEditorController } from "../application/threads/rename-editor-controller";
|
||||
import type { RestorationController } from "../application/threads/restoration-controller";
|
||||
import type { ResumeController } from "../application/threads/resume-controller";
|
||||
import type { SelectionActions } from "../application/threads/selection-actions";
|
||||
import { createThreadParts, createThreadSelectionActions } from "../application/threads/composition";
|
||||
import { createChatConnectionBundle, createChatConnectionEventRouter, type ChatConnectionBundle } from "./connection-bundle";
|
||||
import type { MessageStreamPresenter } from "../panel/surface/message-stream-presenter";
|
||||
import { pendingRequestsSignature } from "../domain/pending-requests/signatures";
|
||||
import { createChatPanelSurface } from "../panel/surface/create-surface";
|
||||
import type { ChatPanelSurface } from "../panel/surface/model";
|
||||
import { createChatPanelGoalSurface } from "../panel/surface/goal-surface";
|
||||
import type { ChatPanelComposerSurface, ChatPanelGoalSurface, ChatPanelToolbarSurface } from "../panel/surface/model";
|
||||
import type { ToolbarActions } from "../ui/toolbar";
|
||||
|
||||
export interface ChatPanelEnvironment {
|
||||
|
|
@ -81,9 +73,9 @@ interface ChatPanelSessionParts {
|
|||
controller: ChatConnectionController;
|
||||
};
|
||||
serverActions: {
|
||||
threads: ChatServerThreadActions;
|
||||
metadata: ChatServerMetadataActions;
|
||||
diagnostics: ChatServerDiagnosticsActions;
|
||||
threads: ChatConnectionBundle["serverActions"]["threads"];
|
||||
metadata: ChatConnectionBundle["serverActions"]["metadata"];
|
||||
diagnostics: ChatConnectionBundle["serverActions"]["diagnostics"];
|
||||
};
|
||||
thread: {
|
||||
history: HistoryController;
|
||||
|
|
@ -103,32 +95,11 @@ interface ChatPanelSessionParts {
|
|||
render: {
|
||||
messageStreamPresenter: MessageStreamPresenter;
|
||||
};
|
||||
surface: ChatPanelSurface;
|
||||
}
|
||||
|
||||
interface ChatPanelSessionDeferredRef<T> {
|
||||
get: () => T;
|
||||
set: (value: T) => void;
|
||||
}
|
||||
|
||||
interface ChatPanelSessionPorts {
|
||||
currentClient: () => AppServerClient | null;
|
||||
ensureConnected: () => Promise<void>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
selectThread: (threadId: string) => Promise<void>;
|
||||
resumeRestoredThread: (threadId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ChatPanelSessionServerParts {
|
||||
connectionController: ChatConnectionController;
|
||||
inboundController: ChatInboundController;
|
||||
serverActions: ChatPanelSessionParts["serverActions"];
|
||||
}
|
||||
|
||||
interface ChatPanelSessionConnectionEventTargets {
|
||||
inbound: ChatInboundController;
|
||||
connectionController: ChatConnectionController;
|
||||
surface: {
|
||||
toolbar: ChatPanelToolbarSurface;
|
||||
goal: ChatPanelGoalSurface;
|
||||
composer: ChatPanelComposerSurface;
|
||||
};
|
||||
}
|
||||
|
||||
interface ChatSessionSideEffects {
|
||||
|
|
@ -142,19 +113,6 @@ interface ChatSessionSideEffects {
|
|||
};
|
||||
}
|
||||
|
||||
function createChatPanelSessionDeferredRef<T>(name: string): ChatPanelSessionDeferredRef<T> {
|
||||
let value: T | null = null;
|
||||
return {
|
||||
get: () => {
|
||||
if (value === null) throw new Error(`${name} was used before chat session parts were initialized.`);
|
||||
return value;
|
||||
},
|
||||
set: (nextValue) => {
|
||||
value = nextValue;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface ChatPanelWarmupHost {
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
opened: () => boolean;
|
||||
|
|
@ -342,32 +300,25 @@ export class ChatPanelSession {
|
|||
}
|
||||
|
||||
private createSessionParts(): ChatPanelSessionParts {
|
||||
const connectionHandlers = this.createConnectionHandlers();
|
||||
const connectionHandlers = createChatConnectionEventRouter({
|
||||
deferLiveStateRefresh: () => {
|
||||
this.deferLiveStateRefresh();
|
||||
},
|
||||
});
|
||||
const connection = new ConnectionManager(
|
||||
() => this.environment.plugin.settingsRef.settings.codexPath,
|
||||
this.environment.plugin.settingsRef.vaultPath,
|
||||
connectionHandlers.handlers,
|
||||
);
|
||||
const connectionControllerRef = createChatPanelSessionDeferredRef<ChatConnectionController>("chat connection controller");
|
||||
const resumeRef = createChatPanelSessionDeferredRef<ResumeController>("chat thread resume controller");
|
||||
const restorationRef = createChatPanelSessionDeferredRef<RestorationController>("chat thread restoration controller");
|
||||
const selectionRef = createChatPanelSessionDeferredRef<SelectionActions>("chat thread selection actions");
|
||||
const composerControllerRef = createChatPanelSessionDeferredRef<ChatComposerController>("chat composer controller");
|
||||
const sideEffects = this.createSideEffects({
|
||||
composerController: composerControllerRef.get,
|
||||
});
|
||||
const sessionPorts: ChatPanelSessionPorts = {
|
||||
currentClient: () => connection.currentClient(),
|
||||
ensureConnected: () => connectionControllerRef.get().ensureConnected(),
|
||||
refreshThreads: () => connectionControllerRef.get().refreshThreads(),
|
||||
refreshSkills: (forceReload) => connectionControllerRef.get().refreshSkills(forceReload),
|
||||
selectThread: (threadId) => selectionRef.get().selectThread(threadId),
|
||||
resumeRestoredThread: (threadId) => resumeRef.get().resumeThread(threadId),
|
||||
};
|
||||
const sideEffects = this.createSideEffects();
|
||||
const currentClient = () => connection.currentClient();
|
||||
const ensureConnected = () => this.parts.connection.controller.ensureConnected();
|
||||
const refreshThreads = () => this.parts.connection.controller.refreshThreads();
|
||||
const refreshSkills = (forceReload?: boolean) => this.parts.connection.controller.refreshSkills(forceReload);
|
||||
|
||||
const runtimeSettings = createChatRuntimeSettingsActions({
|
||||
stateStore: this.stateStore,
|
||||
currentClient: sessionPorts.currentClient,
|
||||
currentClient,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
collaborationModeLabel: () => this.collaborationModeLabel(),
|
||||
addSystemMessage: sideEffects.status.addSystemMessage,
|
||||
|
|
@ -389,8 +340,8 @@ export class ChatPanelSession {
|
|||
getClosing: () => this.closing,
|
||||
},
|
||||
client: {
|
||||
getClient: sessionPorts.currentClient,
|
||||
ensureConnected: sessionPorts.ensureConnected,
|
||||
getClient: currentClient,
|
||||
ensureConnected,
|
||||
},
|
||||
status: sideEffects.status,
|
||||
notify: {
|
||||
|
|
@ -399,9 +350,7 @@ export class ChatPanelSession {
|
|||
},
|
||||
},
|
||||
thread: {
|
||||
selectThread: sessionPorts.selectThread,
|
||||
resumeRestoredThread: sessionPorts.resumeRestoredThread,
|
||||
refreshThreads: sessionPorts.refreshThreads,
|
||||
refreshThreads,
|
||||
notifyIdentityChanged: () => {
|
||||
this.notifyActiveThreadIdentityChanged();
|
||||
},
|
||||
|
|
@ -425,8 +374,6 @@ export class ChatPanelSession {
|
|||
composer: sideEffects.composer,
|
||||
});
|
||||
const { history, managementActions: threadActions, goals, identity, restoration, resume, rename, autoTitle } = threadParts;
|
||||
resumeRef.set(resume);
|
||||
restorationRef.set(restoration);
|
||||
const toolbarPanels = createToolbarPanelActions({
|
||||
stateStore: this.stateStore,
|
||||
threadActions,
|
||||
|
|
@ -448,7 +395,6 @@ export class ChatPanelSession {
|
|||
},
|
||||
},
|
||||
);
|
||||
selectionRef.set(selection);
|
||||
|
||||
const reconnectHost: ChatReconnectActionsHost = {
|
||||
stateStore: this.stateStore,
|
||||
|
|
@ -465,20 +411,42 @@ export class ChatPanelSession {
|
|||
connection.reconnect();
|
||||
},
|
||||
setStatus: sideEffects.status.set,
|
||||
ensureConnected: sessionPorts.ensureConnected,
|
||||
ensureConnected,
|
||||
resumeThread: (threadId) => resume.resumeThread(threadId),
|
||||
addSystemMessage: sideEffects.status.addSystemMessage,
|
||||
};
|
||||
const reconnect = () => reconnectPanel(reconnectHost);
|
||||
const serverParts = this.createServerParts({
|
||||
const serverParts = createChatConnectionBundle({
|
||||
stateStore: this.stateStore,
|
||||
vaultPath: this.environment.plugin.settingsRef.vaultPath,
|
||||
connection,
|
||||
sideEffects,
|
||||
sessionPorts,
|
||||
connectionWork: this.connectionWork,
|
||||
deferredTasks: this.deferredTasks,
|
||||
threadSurfaces: this.environment.plugin.threadSurfaces,
|
||||
client: {
|
||||
currentClient,
|
||||
},
|
||||
refresh: {
|
||||
refreshThreads,
|
||||
refreshSkills,
|
||||
},
|
||||
goals,
|
||||
autoTitle,
|
||||
status: sideEffects.status,
|
||||
invalidateResumeWork: () => {
|
||||
this.invalidateResumeWork();
|
||||
},
|
||||
loadSharedThreadList: () => this.loadSharedThreadList(),
|
||||
refreshDeferredDiagnostics: () => this.refreshDeferredDiagnostics(),
|
||||
refreshTabHeader: () => {
|
||||
this.refreshTabHeader();
|
||||
},
|
||||
refreshLiveState: () => {
|
||||
this.refreshLiveState();
|
||||
},
|
||||
configuredCommand: () => this.environment.plugin.settingsRef.settings.codexPath,
|
||||
});
|
||||
const { connectionController, inboundController } = serverParts;
|
||||
connectionControllerRef.set(connectionController);
|
||||
connectionHandlers.attach({ inbound: inboundController, connectionController });
|
||||
const { threads: serverThreads, diagnostics: serverDiagnostics } = serverParts.serverActions;
|
||||
|
||||
|
|
@ -497,22 +465,38 @@ export class ChatPanelSession {
|
|||
selection,
|
||||
},
|
||||
);
|
||||
const surface = createChatPanelSurface(
|
||||
const toolbarSurface: ChatPanelToolbarSurface = {
|
||||
state: {
|
||||
connected: () => connection.isConnected(),
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
settings: {
|
||||
vaultPath: () => this.environment.plugin.settingsRef.vaultPath,
|
||||
configuredCommand: () => this.environment.plugin.settingsRef.settings.codexPath,
|
||||
archiveExportEnabled: () => this.environment.plugin.settingsRef.settings.archiveExportEnabled,
|
||||
},
|
||||
};
|
||||
const goalSurface = createChatPanelGoalSurface(
|
||||
{
|
||||
settings: this.environment.plugin.settingsRef.settings,
|
||||
vaultPath: this.environment.plugin.settingsRef.vaultPath,
|
||||
stateStore: this.stateStore,
|
||||
restoredThreadPlaceholder: () => restoration.placeholder(),
|
||||
},
|
||||
{
|
||||
connection,
|
||||
connectionController,
|
||||
inboundController,
|
||||
threadStarter: serverThreads,
|
||||
runtimeSettings,
|
||||
goals,
|
||||
},
|
||||
);
|
||||
const composerSurface: ChatPanelComposerSurface = {
|
||||
thread: {
|
||||
restoredPlaceholder: () => restoration.placeholder(),
|
||||
},
|
||||
runtime: {
|
||||
requestModel: (model) => runtimeSettings.requestModelFromUi(model),
|
||||
requestReasoningEffort: (effort) => runtimeSettings.requestReasoningEffortFromUi(effort),
|
||||
},
|
||||
};
|
||||
const conversationParts = createConversationParts(
|
||||
{
|
||||
obsidian: {
|
||||
|
|
@ -535,7 +519,7 @@ export class ChatPanelSession {
|
|||
this.state.requests.pendingUserInputs,
|
||||
this.state.requests.userInputDrafts,
|
||||
),
|
||||
composerProjection: (state) => chatPanelComposerProjection(surface.composer, state),
|
||||
composerProjection: (state) => chatPanelComposerProjection(composerSurface, state),
|
||||
},
|
||||
runtime: {
|
||||
connectionDiagnosticDetails: () => this.connectionDiagnosticDetails(),
|
||||
|
|
@ -550,14 +534,14 @@ export class ChatPanelSession {
|
|||
},
|
||||
},
|
||||
client: {
|
||||
getClient: sessionPorts.currentClient,
|
||||
ensureConnected: sessionPorts.ensureConnected,
|
||||
getClient: currentClient,
|
||||
ensureConnected,
|
||||
},
|
||||
status: sideEffects.status,
|
||||
thread: {
|
||||
ensureRestoredThreadLoaded: () => restorationRef.get().ensureLoaded(),
|
||||
ensureRestoredThreadLoaded: () => restoration.ensureLoaded(),
|
||||
startNewThread: () => this.startNewThread(),
|
||||
selectThread: sessionPorts.selectThread,
|
||||
selectThread: (threadId) => selection.selectThread(threadId),
|
||||
notifyIdentityChanged: () => {
|
||||
this.notifyActiveThreadIdentityChanged();
|
||||
},
|
||||
|
|
@ -586,7 +570,6 @@ export class ChatPanelSession {
|
|||
);
|
||||
const { composerSubmit, messageStreamPresenter } = conversationParts;
|
||||
const composerController = conversationParts.composerController;
|
||||
composerControllerRef.set(composerController);
|
||||
|
||||
return {
|
||||
connection: {
|
||||
|
|
@ -612,168 +595,15 @@ export class ChatPanelSession {
|
|||
render: {
|
||||
messageStreamPresenter,
|
||||
},
|
||||
surface,
|
||||
};
|
||||
}
|
||||
|
||||
private createServerParts({
|
||||
connection,
|
||||
sideEffects,
|
||||
sessionPorts,
|
||||
goals,
|
||||
autoTitle,
|
||||
}: {
|
||||
connection: ConnectionManager;
|
||||
sideEffects: ChatSessionSideEffects;
|
||||
sessionPorts: ChatPanelSessionPorts;
|
||||
goals: GoalActions;
|
||||
autoTitle: AutoTitleController;
|
||||
}): ChatPanelSessionServerParts {
|
||||
const serverMetadata = createChatServerMetadataActions({
|
||||
stateStore: this.stateStore,
|
||||
vaultPath: this.environment.plugin.settingsRef.vaultPath,
|
||||
currentClient: sessionPorts.currentClient,
|
||||
publishAppServerMetadata: (metadata) => {
|
||||
this.environment.plugin.threadSurfaces.publishAppServerMetadata(metadata);
|
||||
},
|
||||
});
|
||||
const serverDiagnostics = createChatServerDiagnosticsActions({
|
||||
stateStore: this.stateStore,
|
||||
vaultPath: this.environment.plugin.settingsRef.vaultPath,
|
||||
currentClient: sessionPorts.currentClient,
|
||||
publishAppServerMetadata: (metadata) => {
|
||||
this.environment.plugin.threadSurfaces.publishAppServerMetadata(metadata);
|
||||
},
|
||||
serverMetadataSnapshot: () => serverMetadata.serverMetadataSnapshot(),
|
||||
});
|
||||
const serverThreads = createChatServerThreadActions({
|
||||
stateStore: this.stateStore,
|
||||
vaultPath: this.environment.plugin.settingsRef.vaultPath,
|
||||
currentClient: sessionPorts.currentClient,
|
||||
runtimeSnapshotForState: runtimeSnapshotForChatState,
|
||||
publishThreadList: (threads) => {
|
||||
this.environment.plugin.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
},
|
||||
syncThreadGoal: (threadId) => {
|
||||
void goals.syncThreadGoal(threadId);
|
||||
},
|
||||
});
|
||||
const serverRequestHost = {
|
||||
currentClient: sessionPorts.currentClient,
|
||||
};
|
||||
const inboundController = new ChatInboundController(this.stateStore, {
|
||||
refreshThreads: () => {
|
||||
void sessionPorts.refreshThreads();
|
||||
},
|
||||
refreshRateLimits: () => {
|
||||
void serverMetadata.refreshPublishedRateLimits();
|
||||
},
|
||||
refreshSkills: (forceReload) => void sessionPorts.refreshSkills(forceReload),
|
||||
publishAppServerMetadata: () => {
|
||||
serverMetadata.publishAppServerMetadataSnapshot();
|
||||
},
|
||||
maybeNameThread: (threadId, turnId, completedSummary) => {
|
||||
autoTitle.maybeAutoTitleThread(threadId, turnId, completedSummary);
|
||||
},
|
||||
notifyThreadArchived: (threadId) => {
|
||||
this.environment.plugin.threadSurfaces.notifyThreadArchived(threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
this.environment.plugin.threadSurfaces.notifyThreadRenamed(threadId, name);
|
||||
},
|
||||
recordMcpStartupStatus: (name, status, message) => {
|
||||
serverDiagnostics.recordMcpStartupStatus(name, status, message);
|
||||
},
|
||||
respondToServerRequest: (requestId, result) => respondToServerRequest(serverRequestHost, requestId, result),
|
||||
rejectServerRequest: (requestId, code, message) => rejectServerRequest(serverRequestHost, requestId, code, message),
|
||||
});
|
||||
const connectionController = new ChatConnectionController({
|
||||
stateStore: this.stateStore,
|
||||
connection,
|
||||
connectionWork: this.connectionWork,
|
||||
metadata: {
|
||||
refreshPublishedAppServerMetadata: () => serverMetadata.refreshPublishedAppServerMetadata(),
|
||||
refreshPublishedSkills: (forceReload) => serverMetadata.refreshPublishedSkills(forceReload),
|
||||
},
|
||||
diagnostics: {
|
||||
refreshPublishedDiagnosticProbes: () => serverDiagnostics.refreshPublishedDiagnosticProbes(),
|
||||
},
|
||||
invalidateResumeWork: () => {
|
||||
this.invalidateResumeWork();
|
||||
},
|
||||
loadSharedThreadList: () => this.loadSharedThreadList(),
|
||||
scheduleDeferredDiagnostics: () => {
|
||||
this.deferredTasks.scheduleDiagnostics(() => {
|
||||
void this.refreshDeferredDiagnostics();
|
||||
});
|
||||
},
|
||||
clearDeferredDiagnostics: () => {
|
||||
this.deferredTasks.clearDiagnostics();
|
||||
},
|
||||
refreshTabHeader: () => {
|
||||
this.refreshTabHeader();
|
||||
},
|
||||
resetThreadTurnPresence: (hadTurns) => {
|
||||
autoTitle.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
setStatus: sideEffects.status.set,
|
||||
addSystemMessage: sideEffects.status.addSystemMessage,
|
||||
publishAppServerIdentity: (userAgent) => {
|
||||
this.environment.plugin.appServerIdentity.publishAppServerIdentity(userAgent);
|
||||
},
|
||||
configuredCommand: () => this.environment.plugin.settingsRef.settings.codexPath,
|
||||
refreshLiveState: () => {
|
||||
this.refreshLiveState();
|
||||
},
|
||||
notifyConnectionFailed: () => {
|
||||
new Notice("Codex app-server connection failed.");
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
connectionController,
|
||||
inboundController,
|
||||
serverActions: {
|
||||
threads: serverThreads,
|
||||
metadata: serverMetadata,
|
||||
diagnostics: serverDiagnostics,
|
||||
surface: {
|
||||
toolbar: toolbarSurface,
|
||||
goal: goalSurface,
|
||||
composer: composerSurface,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createConnectionHandlers(): {
|
||||
handlers: ConnectionManagerHandlers;
|
||||
attach: (targets: ChatPanelSessionConnectionEventTargets) => void;
|
||||
} {
|
||||
let targets: ChatPanelSessionConnectionEventTargets | null = null;
|
||||
const currentTargets = () => {
|
||||
if (!targets) throw new Error("Codex app-server connection event received before chat session parts were initialized.");
|
||||
return targets;
|
||||
};
|
||||
return {
|
||||
handlers: {
|
||||
onNotification: (notification) => {
|
||||
currentTargets().inbound.handleNotification(notification);
|
||||
this.deferLiveStateRefresh();
|
||||
},
|
||||
onServerRequest: (request) => {
|
||||
currentTargets().inbound.handleServerRequest(request);
|
||||
this.deferLiveStateRefresh();
|
||||
},
|
||||
onLog: (message) => {
|
||||
currentTargets().inbound.handleAppServerLog(message);
|
||||
},
|
||||
onExit: () => {
|
||||
currentTargets().connectionController.handleExit();
|
||||
},
|
||||
},
|
||||
attach: (nextTargets) => {
|
||||
targets = nextTargets;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createSideEffects(refs: { composerController: () => ChatComposerController }): ChatSessionSideEffects {
|
||||
private createSideEffects(): ChatSessionSideEffects {
|
||||
return {
|
||||
status: {
|
||||
set: (statusText, phase) => {
|
||||
|
|
@ -788,7 +618,7 @@ export class ChatPanelSession {
|
|||
},
|
||||
composer: {
|
||||
setText: (text) => {
|
||||
refs.composerController().setDraft(text, { focus: true });
|
||||
this.parts.composer.controller.setDraft(text, { focus: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
import type { CodexPanelSettings } from "../../../../settings/model";
|
||||
import type { ConnectionManager } from "../../../../app-server/connection/connection-manager";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { ChatConnectionController } from "../../application/connection/connection-controller";
|
||||
import type { ChatInboundController } from "../../app-server/inbound/controller";
|
||||
import type { ChatRuntimeSettingsActions } from "../../application/runtime/settings-actions";
|
||||
import type { GoalActions } from "../../application/threads/goal-actions";
|
||||
import type { RestoredThreadTitleSnapshot, ChatPanelSurface } from "./model";
|
||||
import { createChatPanelGoalSurface } from "./goal-surface";
|
||||
|
||||
export interface ChatPanelSurfaceHost {
|
||||
settings: CodexPanelSettings;
|
||||
vaultPath: string;
|
||||
stateStore: ChatStateStore;
|
||||
restoredThreadPlaceholder: () => RestoredThreadTitleSnapshot | null;
|
||||
}
|
||||
|
||||
export interface ChatPanelSurfaceDependencies {
|
||||
connection: ConnectionManager;
|
||||
connectionController: ChatConnectionController;
|
||||
inboundController: ChatInboundController;
|
||||
threadStarter: ChatPanelThreadStarter;
|
||||
runtimeSettings: ChatRuntimeSettingsActions;
|
||||
goals: GoalActions;
|
||||
}
|
||||
|
||||
interface ChatPanelThreadStarter {
|
||||
startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<{ threadId: string } | null>;
|
||||
}
|
||||
|
||||
export function createChatPanelSurface(host: ChatPanelSurfaceHost, deps: ChatPanelSurfaceDependencies): ChatPanelSurface {
|
||||
return {
|
||||
toolbar: {
|
||||
state: {
|
||||
connected: () => deps.connection.isConnected(),
|
||||
nowMs: () => Date.now(),
|
||||
},
|
||||
settings: {
|
||||
vaultPath: () => host.vaultPath,
|
||||
configuredCommand: () => host.settings.codexPath,
|
||||
archiveExportEnabled: () => host.settings.archiveExportEnabled,
|
||||
},
|
||||
},
|
||||
goal: createChatPanelGoalSurface(
|
||||
{
|
||||
settings: host.settings,
|
||||
stateStore: host.stateStore,
|
||||
},
|
||||
{
|
||||
connectionController: deps.connectionController,
|
||||
inboundController: deps.inboundController,
|
||||
threadStarter: deps.threadStarter,
|
||||
goals: deps.goals,
|
||||
},
|
||||
),
|
||||
composer: {
|
||||
thread: {
|
||||
restoredPlaceholder: host.restoredThreadPlaceholder,
|
||||
},
|
||||
runtime: {
|
||||
requestModel: (model) => deps.runtimeSettings.requestModelFromUi(model),
|
||||
requestReasoningEffort: (effort) => deps.runtimeSettings.requestReasoningEffortFromUi(effort),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -83,9 +83,3 @@ export interface ChatPanelComposerSurface {
|
|||
requestReasoningEffort: (effort: ReasoningEffort) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatPanelSurface {
|
||||
toolbar: ChatPanelToolbarSurface;
|
||||
goal: ChatPanelGoalSurface;
|
||||
composer: ChatPanelComposerSurface;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
import { activeAgentRunSummary } from "./agent-summary";
|
||||
import type { AgentRunSummary, MessageStreamItem, TaskProgressMessageStreamItem } from "../../domain/message-stream/items";
|
||||
import {
|
||||
messageStreamIsCoordinationProgress,
|
||||
messageStreamIsTaskProgress,
|
||||
messageStreamSemanticClassifications,
|
||||
} from "../../domain/message-stream/semantics";
|
||||
import type { MessageStreamSemanticClassification } from "../../domain/message-stream/semantics";
|
||||
import { messageStreamLayoutBlocks, type MessageStreamLayoutBlock } from "./layout";
|
||||
|
||||
export interface MessageStreamPresentationBlockInput {
|
||||
activeThreadId: string | null;
|
||||
activeTurnId: string | null;
|
||||
historyCursor: string | null;
|
||||
loadingHistory: boolean;
|
||||
items: readonly MessageStreamItem[];
|
||||
stableItems?: readonly MessageStreamItem[] | undefined;
|
||||
activeItems?: readonly MessageStreamItem[] | undefined;
|
||||
workspaceRoot?: string | null | undefined;
|
||||
turnDiffs?: ReadonlyMap<string, string> | undefined;
|
||||
}
|
||||
|
||||
export type MessageStreamPresentationBlock =
|
||||
| {
|
||||
kind: "historyBar";
|
||||
key: "history-bar";
|
||||
loadingHistory: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "empty";
|
||||
key: "empty";
|
||||
}
|
||||
| {
|
||||
kind: "item";
|
||||
key: string;
|
||||
block: Extract<MessageStreamLayoutBlock, { type: "item" }>;
|
||||
}
|
||||
| {
|
||||
kind: "activityGroup";
|
||||
key: string;
|
||||
block: Extract<MessageStreamLayoutBlock, { type: "activityGroup" }>;
|
||||
}
|
||||
| {
|
||||
kind: "liveTask";
|
||||
key: string;
|
||||
item: TaskProgressMessageStreamItem;
|
||||
}
|
||||
| {
|
||||
kind: "liveAgentSummary";
|
||||
key: string;
|
||||
summary: AgentRunSummary;
|
||||
};
|
||||
|
||||
export function messageStreamPresentationBlocks(input: MessageStreamPresentationBlockInput): MessageStreamPresentationBlock[] {
|
||||
const blocks: MessageStreamPresentationBlock[] = [];
|
||||
|
||||
if (input.activeThreadId && input.historyCursor) {
|
||||
blocks.push({ kind: "historyBar", key: "history-bar", loadingHistory: input.loadingHistory });
|
||||
}
|
||||
|
||||
if (messageStreamBlockItemsEmpty(input)) {
|
||||
blocks.push({ kind: "empty", key: "empty" });
|
||||
return blocks;
|
||||
}
|
||||
|
||||
for (const block of layoutBlocksForInput(input)) {
|
||||
if (block.type === "item") {
|
||||
blocks.push({ kind: "item", key: `item:${block.item.id}`, block });
|
||||
} else {
|
||||
blocks.push({ kind: "activityGroup", key: `activity:${block.id}`, block });
|
||||
}
|
||||
}
|
||||
|
||||
if (input.activeTurnId) blocks.push(...activeTurnLiveBlocks(input, input.activeTurnId));
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function messageStreamBlockItemsEmpty(input: MessageStreamPresentationBlockInput): boolean {
|
||||
if (!input.stableItems && !input.activeItems) return input.items.length === 0;
|
||||
return (input.stableItems?.length ?? 0) === 0 && (input.activeItems?.length ?? 0) === 0;
|
||||
}
|
||||
|
||||
function layoutBlocksForInput(input: MessageStreamPresentationBlockInput): MessageStreamLayoutBlock[] {
|
||||
const { activeTurnId } = input;
|
||||
if (!activeTurnId || !input.stableItems || !input.activeItems) {
|
||||
const streamItems = activeTurnId ? withoutActiveTaskProgress(input.items, activeTurnId) : input.items;
|
||||
return messageStreamLayoutBlocks(streamItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
}
|
||||
const stableBlocks = messageStreamLayoutBlocks(input.stableItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
const activeBlocks = messageStreamLayoutBlocks(
|
||||
withoutActiveTaskProgress(input.activeItems, activeTurnId),
|
||||
activeTurnId,
|
||||
input.workspaceRoot,
|
||||
input.turnDiffs,
|
||||
);
|
||||
return [...stableBlocks, ...activeBlocks];
|
||||
}
|
||||
|
||||
function activeTurnLiveBlocks(
|
||||
input: Pick<MessageStreamPresentationBlockInput, "items" | "activeItems">,
|
||||
activeTurnId: string,
|
||||
): MessageStreamPresentationBlock[] {
|
||||
const items = input.activeItems ?? input.items;
|
||||
const semanticItems = messageStreamSemanticClassifications(items);
|
||||
const agentSummaryAnchorId = activeAgentRunSummaryAnchorId(semanticItems, activeTurnId);
|
||||
const agentSummary = agentSummaryAnchorId ? activeAgentRunSummary(items, activeTurnId) : null;
|
||||
|
||||
return semanticItems.flatMap((classification): MessageStreamPresentationBlock[] => {
|
||||
const { item } = classification;
|
||||
if (messageStreamIsTaskProgress(classification) && item.turnId === activeTurnId) {
|
||||
return [
|
||||
{
|
||||
kind: "liveTask",
|
||||
key: `live-task:${item.id}`,
|
||||
item: taskProgressStreamItem(item),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (item.id === agentSummaryAnchorId) {
|
||||
return agentSummary ? [{ kind: "liveAgentSummary", key: `live-agents:${activeTurnId}`, summary: agentSummary }] : [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function activeAgentRunSummaryAnchorId(items: readonly MessageStreamSemanticClassification[], activeTurnId: string): string | null {
|
||||
const firstActiveAgent = items.find(
|
||||
(classification) => messageStreamIsCoordinationProgress(classification) && classification.item.turnId === activeTurnId,
|
||||
);
|
||||
return firstActiveAgent?.item.id ?? null;
|
||||
}
|
||||
|
||||
function taskProgressStreamItem(item: MessageStreamItem): TaskProgressMessageStreamItem {
|
||||
if (item.kind !== "taskProgress") throw new Error(`Expected task progress presentation item for ${item.id}`);
|
||||
return item;
|
||||
}
|
||||
|
||||
function withoutActiveTaskProgress(items: readonly MessageStreamItem[], activeTurnId: string): MessageStreamItem[] {
|
||||
return items.filter((item) => item.kind !== "taskProgress" || item.turnId !== activeTurnId);
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
import { activeAgentRunSummary } from "./agent-summary";
|
||||
import { messageStreamRenderFamily } from "../../domain/message-stream/semantics";
|
||||
import type { MessageStreamSemanticClassification } from "../../domain/message-stream/semantics";
|
||||
import { messageStreamPresentationBlocks, type MessageStreamPresentationBlock, type MessageStreamPresentationBlockInput } from "./blocks";
|
||||
import type { MessageStreamItemAnnotations, MessageStreamLayoutBlock } from "./layout";
|
||||
import {
|
||||
messageStreamIsCoordinationProgress,
|
||||
messageStreamIsTaskProgress,
|
||||
messageStreamSemanticClassifications,
|
||||
type MessageStreamSemanticClassification,
|
||||
} from "../../domain/message-stream/semantics";
|
||||
import type { AgentRunSummary, MessageStreamItem, TaskProgressMessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamLayoutBlocks, type MessageStreamItemAnnotations, type MessageStreamLayoutBlock } from "./layout";
|
||||
import { messageStreamTextView, type MessageStreamTextView, type TextMessageStreamItem } from "./text-view";
|
||||
import { toolResultView, type ToolResultMessageStreamItem, type ToolResultView } from "./tool-result-view";
|
||||
import {
|
||||
|
|
@ -12,6 +18,49 @@ import {
|
|||
type WorkMessageStreamItem,
|
||||
} from "./work-view";
|
||||
|
||||
export interface MessageStreamPresentationBlockInput {
|
||||
activeThreadId: string | null;
|
||||
activeTurnId: string | null;
|
||||
historyCursor: string | null;
|
||||
loadingHistory: boolean;
|
||||
items: readonly MessageStreamItem[];
|
||||
stableItems?: readonly MessageStreamItem[] | undefined;
|
||||
activeItems?: readonly MessageStreamItem[] | undefined;
|
||||
workspaceRoot?: string | null | undefined;
|
||||
turnDiffs?: ReadonlyMap<string, string> | undefined;
|
||||
}
|
||||
|
||||
export type MessageStreamPresentationBlock =
|
||||
| {
|
||||
kind: "historyBar";
|
||||
key: "history-bar";
|
||||
loadingHistory: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "empty";
|
||||
key: "empty";
|
||||
}
|
||||
| {
|
||||
kind: "item";
|
||||
key: string;
|
||||
block: Extract<MessageStreamLayoutBlock, { type: "item" }>;
|
||||
}
|
||||
| {
|
||||
kind: "activityGroup";
|
||||
key: string;
|
||||
block: Extract<MessageStreamLayoutBlock, { type: "activityGroup" }>;
|
||||
}
|
||||
| {
|
||||
kind: "liveTask";
|
||||
key: string;
|
||||
item: TaskProgressMessageStreamItem;
|
||||
}
|
||||
| {
|
||||
kind: "liveAgentSummary";
|
||||
key: string;
|
||||
summary: AgentRunSummary;
|
||||
};
|
||||
|
||||
export type MessageStreamRenderedItemView =
|
||||
| {
|
||||
kind: "text";
|
||||
|
|
@ -74,6 +123,95 @@ export function messageStreamViewHasEmptyBlock(blocks: readonly MessageStreamVie
|
|||
return blocks.some((block) => block.kind === "empty");
|
||||
}
|
||||
|
||||
export function messageStreamPresentationBlocks(input: MessageStreamPresentationBlockInput): MessageStreamPresentationBlock[] {
|
||||
const blocks: MessageStreamPresentationBlock[] = [];
|
||||
|
||||
if (input.activeThreadId && input.historyCursor) {
|
||||
blocks.push({ kind: "historyBar", key: "history-bar", loadingHistory: input.loadingHistory });
|
||||
}
|
||||
|
||||
if (messageStreamBlockItemsEmpty(input)) {
|
||||
blocks.push({ kind: "empty", key: "empty" });
|
||||
return blocks;
|
||||
}
|
||||
|
||||
for (const block of layoutBlocksForInput(input)) {
|
||||
if (block.type === "item") {
|
||||
blocks.push({ kind: "item", key: `item:${block.item.id}`, block });
|
||||
} else {
|
||||
blocks.push({ kind: "activityGroup", key: `activity:${block.id}`, block });
|
||||
}
|
||||
}
|
||||
|
||||
if (input.activeTurnId) blocks.push(...activeTurnLiveBlocks(input, input.activeTurnId));
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function messageStreamBlockItemsEmpty(input: MessageStreamPresentationBlockInput): boolean {
|
||||
if (!input.stableItems && !input.activeItems) return input.items.length === 0;
|
||||
return (input.stableItems?.length ?? 0) === 0 && (input.activeItems?.length ?? 0) === 0;
|
||||
}
|
||||
|
||||
function layoutBlocksForInput(input: MessageStreamPresentationBlockInput): MessageStreamLayoutBlock[] {
|
||||
const { activeTurnId } = input;
|
||||
if (!activeTurnId || !input.stableItems || !input.activeItems) {
|
||||
const streamItems = activeTurnId ? withoutActiveTaskProgress(input.items, activeTurnId) : input.items;
|
||||
return messageStreamLayoutBlocks(streamItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
}
|
||||
const stableBlocks = messageStreamLayoutBlocks(input.stableItems, activeTurnId, input.workspaceRoot, input.turnDiffs);
|
||||
const activeBlocks = messageStreamLayoutBlocks(
|
||||
withoutActiveTaskProgress(input.activeItems, activeTurnId),
|
||||
activeTurnId,
|
||||
input.workspaceRoot,
|
||||
input.turnDiffs,
|
||||
);
|
||||
return [...stableBlocks, ...activeBlocks];
|
||||
}
|
||||
|
||||
function activeTurnLiveBlocks(
|
||||
input: Pick<MessageStreamPresentationBlockInput, "items" | "activeItems">,
|
||||
activeTurnId: string,
|
||||
): MessageStreamPresentationBlock[] {
|
||||
const items = input.activeItems ?? input.items;
|
||||
const semanticItems = messageStreamSemanticClassifications(items);
|
||||
const agentSummaryAnchorId = activeAgentRunSummaryAnchorId(semanticItems, activeTurnId);
|
||||
const agentSummary = agentSummaryAnchorId ? activeAgentRunSummary(items, activeTurnId) : null;
|
||||
|
||||
return semanticItems.flatMap((classification): MessageStreamPresentationBlock[] => {
|
||||
const { item } = classification;
|
||||
if (messageStreamIsTaskProgress(classification) && item.turnId === activeTurnId) {
|
||||
return [
|
||||
{
|
||||
kind: "liveTask",
|
||||
key: `live-task:${item.id}`,
|
||||
item: taskProgressStreamItem(item),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (item.id === agentSummaryAnchorId) {
|
||||
return agentSummary ? [{ kind: "liveAgentSummary", key: `live-agents:${activeTurnId}`, summary: agentSummary }] : [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function activeAgentRunSummaryAnchorId(items: readonly MessageStreamSemanticClassification[], activeTurnId: string): string | null {
|
||||
const firstActiveAgent = items.find(
|
||||
(classification) => messageStreamIsCoordinationProgress(classification) && classification.item.turnId === activeTurnId,
|
||||
);
|
||||
return firstActiveAgent?.item.id ?? null;
|
||||
}
|
||||
|
||||
function taskProgressStreamItem(item: MessageStreamItem): TaskProgressMessageStreamItem {
|
||||
if (item.kind !== "taskProgress") throw new Error(`Expected task progress presentation item for ${item.id}`);
|
||||
return item;
|
||||
}
|
||||
|
||||
function withoutActiveTaskProgress(items: readonly MessageStreamItem[], activeTurnId: string): MessageStreamItem[] {
|
||||
return items.filter((item) => item.kind !== "taskProgress" || item.turnId !== activeTurnId);
|
||||
}
|
||||
|
||||
function messageStreamViewBlockFromPresentationBlock(
|
||||
block: MessageStreamPresentationBlock,
|
||||
input: MessageStreamPresentationBlockInput,
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import {
|
|||
runEphemeralStructuredTurnForLastAgentText,
|
||||
type StructuredTurnOutputSchema,
|
||||
} from "../../app-server/services/ephemeral-structured-turn";
|
||||
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import { listModelMetadata } from "../../app-server/services/catalog";
|
||||
import type { ModelMetadata } from "../../domain/catalog/metadata";
|
||||
import { runtimeOverride, validatedRuntimeOverrideForModelMetadata } from "../../domain/runtime/overrides";
|
||||
import { resolvedRuntimeOverrideForClient } from "../../app-server/services/runtime-overrides";
|
||||
import type { RuntimeOverride } from "../../domain/runtime/overrides";
|
||||
import type { SelectionRewriteRuntimeSettings } from "./model";
|
||||
import { SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS, SELECTION_REWRITE_SERVICE_NAME } from "./prompt";
|
||||
import { SelectionRewriteOutputError, selectionRewriteOutputParseResultFromText, type SelectionRewriteOutput } from "./output";
|
||||
|
|
@ -75,34 +73,11 @@ export async function runSelectionRewrite(options: RunSelectionRewriteOptions):
|
|||
return output;
|
||||
}
|
||||
|
||||
interface SelectionRewriteRuntimeOverride {
|
||||
model?: string;
|
||||
effort?: ReasoningEffort;
|
||||
}
|
||||
|
||||
function selectionRewriteRuntimeOverride(settings: SelectionRewriteRuntimeSettings): SelectionRewriteRuntimeOverride {
|
||||
return runtimeOverride({ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort });
|
||||
}
|
||||
|
||||
function validatedSelectionRewriteRuntimeOverride(
|
||||
settings: SelectionRewriteRuntimeSettings,
|
||||
models: readonly ModelMetadata[],
|
||||
): SelectionRewriteRuntimeOverride {
|
||||
return validatedRuntimeOverrideForModelMetadata(
|
||||
{ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort },
|
||||
models,
|
||||
);
|
||||
}
|
||||
type SelectionRewriteRuntimeOverride = RuntimeOverride;
|
||||
|
||||
async function selectionRewriteRuntimeOverrideForClient(
|
||||
client: EphemeralStructuredTurnRuntimeClient,
|
||||
settings: SelectionRewriteRuntimeSettings,
|
||||
): Promise<SelectionRewriteRuntimeOverride> {
|
||||
const runtime = selectionRewriteRuntimeOverride(settings);
|
||||
if (!runtime.model || !runtime.effort) return runtime;
|
||||
try {
|
||||
return validatedSelectionRewriteRuntimeOverride(settings, await listModelMetadata(client));
|
||||
} catch {
|
||||
return runtime;
|
||||
}
|
||||
return resolvedRuntimeOverrideForClient(client, { model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export interface CodexThreadsHost {
|
|||
getOpenPanelSnapshots(): OpenCodexPanelSnapshot[];
|
||||
notifyThreadArchived(threadId: string, options?: { closeOpenPanels?: boolean }): void;
|
||||
notifyThreadRenamed(threadId: string, name: string | null): void;
|
||||
publishAppServerIdentity(userAgent: string | null): void;
|
||||
refreshThreadList(fetchThreads: () => Promise<readonly Thread[]>): Promise<readonly Thread[]>;
|
||||
cachedThreadList(): readonly Thread[] | null;
|
||||
}
|
||||
|
|
@ -111,7 +110,6 @@ export class CodexThreadsSession {
|
|||
this.invalidateConnectionWork();
|
||||
this.refreshLifecycle = transitionThreadsViewRefreshLifecycle(this.refreshLifecycle, { type: "invalidated" });
|
||||
this.deferredTasks.clearAll();
|
||||
this.host.publishAppServerIdentity(null);
|
||||
this.connection.disconnect();
|
||||
this.client = null;
|
||||
unmountThreadsView(this.environment.root);
|
||||
|
|
@ -179,12 +177,10 @@ export class CodexThreadsSession {
|
|||
}
|
||||
|
||||
const connection = this.beginConnectionWork();
|
||||
this.host.publishAppServerIdentity(null);
|
||||
const promise = this.connection
|
||||
.connect()
|
||||
.then((initialization) => {
|
||||
.then(() => {
|
||||
if (this.isStaleConnectionWork(connection)) throw new StaleConnectionError();
|
||||
this.host.publishAppServerIdentity(initialization.userAgent);
|
||||
this.client = this.connection.currentClient();
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
|
|||
16
src/main.ts
16
src/main.ts
|
|
@ -47,15 +47,11 @@ interface PluginHostServices {
|
|||
cachedAppServerMetadata: () => SharedServerMetadata | null;
|
||||
cachedModels: () => ReturnType<CodexPanelSettingTabHost["cachedModels"]>;
|
||||
};
|
||||
readonly appServerIdentity: {
|
||||
publishAppServerIdentity: (userAgent: string | null) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export default class CodexPanelPlugin extends Plugin {
|
||||
settings: CodexPanelSettings = DEFAULT_SETTINGS;
|
||||
vaultPath = "";
|
||||
private appServerUserAgent: string | null = null;
|
||||
private readonly sharedAppServerCache = new SharedAppServerCache();
|
||||
private readonly panels = new WorkspacePanelCoordinator({
|
||||
app: this.app,
|
||||
|
|
@ -167,14 +163,9 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
return {
|
||||
codexPath: this.settings.codexPath,
|
||||
vaultPath: this.vaultPath,
|
||||
appServerUserAgent: this.appServerUserAgent,
|
||||
};
|
||||
}
|
||||
|
||||
private publishAppServerIdentity(userAgent: string | null): void {
|
||||
this.appServerUserAgent = userAgent;
|
||||
}
|
||||
|
||||
private applyThreadListSnapshot(threads: readonly Thread[]): void {
|
||||
this.sharedAppServerCache.applyThreadListSnapshot(this.sharedAppServerCacheContext(), threads);
|
||||
this.threadSurfaces.applyThreadListSnapshot(threads);
|
||||
|
|
@ -247,11 +238,6 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
cachedAppServerMetadata: () => this.sharedAppServerCache.cachedAppServerMetadata(this.sharedAppServerCacheContext()),
|
||||
cachedModels: () => this.sharedAppServerCache.cachedModels(this.sharedAppServerCacheContext()),
|
||||
},
|
||||
appServerIdentity: {
|
||||
publishAppServerIdentity: (userAgent) => {
|
||||
this.publishAppServerIdentity(userAgent);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -266,7 +252,6 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
},
|
||||
threadSurfaces: services.threadSurfaces,
|
||||
sharedCache: services.sharedCache,
|
||||
appServerIdentity: services.appServerIdentity,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +265,6 @@ export default class CodexPanelPlugin extends Plugin {
|
|||
getOpenPanelSnapshots: services.workspace.getOpenPanelSnapshots,
|
||||
notifyThreadArchived: services.threadSurfaces.notifyThreadArchived,
|
||||
notifyThreadRenamed: services.threadSurfaces.notifyThreadRenamed,
|
||||
publishAppServerIdentity: services.appServerIdentity.publishAppServerIdentity,
|
||||
refreshThreadList: services.sharedCache.refreshThreadList,
|
||||
cachedThreadList: services.sharedCache.cachedThreadList,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -67,16 +67,10 @@ describe("shared app-server cache state", () => {
|
|||
|
||||
expect(cachedSharedThreadList(state, cacheContext({ vaultPath: "/other-vault" }))).toBeNull();
|
||||
expect(cachedSharedModels(state, cacheContext({ codexPath: "/opt/codex" }))).toBeNull();
|
||||
expect(cachedSharedThreadList(state, cacheContext({ appServerUserAgent: "codex-cli/9.9.9" }))).toBeNull();
|
||||
expect(cachedSharedModels(state, cacheContext({ appServerUserAgent: "codex-cli/9.9.9" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("does not load or return snapshots before the app-server identity is known", () => {
|
||||
const incompleteContexts = [
|
||||
cacheContext({ codexPath: "" }),
|
||||
cacheContext({ vaultPath: " " }),
|
||||
cacheContext({ appServerUserAgent: null }),
|
||||
];
|
||||
it("does not load or return snapshots before the cache context is complete", () => {
|
||||
const incompleteContexts = [cacheContext({ codexPath: "" }), cacheContext({ vaultPath: " " })];
|
||||
|
||||
expect(sharedAppServerCacheContextIsComplete(cacheContext())).toBe(true);
|
||||
|
||||
|
|
@ -155,7 +149,6 @@ function cacheContext(overrides: Partial<SharedAppServerCacheContext> = {}): Sha
|
|||
return {
|
||||
codexPath: "codex",
|
||||
vaultPath: "/vault",
|
||||
appServerUserAgent: "codex-cli/1.2.3",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ describe("SharedAppServerCache", () => {
|
|||
expect(cache.cachedModels(context)).toBeNull();
|
||||
});
|
||||
|
||||
it("does not share or store snapshots before the app-server identity is known", async () => {
|
||||
it("does not share or store snapshots before the cache context is complete", async () => {
|
||||
const cache = new SharedAppServerCache();
|
||||
const context = cacheContext({ appServerUserAgent: null });
|
||||
const context = cacheContext({ codexPath: "" });
|
||||
const firstRefresh = deferred<readonly ReturnType<typeof thread>[]>();
|
||||
const firstFetch = vi.fn(() => firstRefresh.promise);
|
||||
const secondFetch = vi.fn().mockResolvedValue([thread("second")]);
|
||||
|
|
@ -108,10 +108,8 @@ describe("SharedAppServerCache", () => {
|
|||
|
||||
expect(cache.cachedAppServerMetadata(cacheContext({ vaultPath: "/other-vault" }))).toBeNull();
|
||||
expect(cache.cachedAppServerMetadata(cacheContext({ codexPath: "/opt/codex" }))).toBeNull();
|
||||
expect(cache.cachedAppServerMetadata(cacheContext({ appServerUserAgent: "codex-cli/9.9.9" }))).toBeNull();
|
||||
expect(cache.cachedModels(cacheContext({ vaultPath: "/other-vault" }))).toBeNull();
|
||||
expect(cache.cachedModels(cacheContext({ codexPath: "/opt/codex" }))).toBeNull();
|
||||
expect(cache.cachedModels(cacheContext({ appServerUserAgent: "codex-cli/9.9.9" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("stores successful empty thread list snapshots as shared cache truth", async () => {
|
||||
|
|
@ -131,8 +129,8 @@ describe("SharedAppServerCache", () => {
|
|||
|
||||
it("ignores stale thread list refresh snapshots after the app-server cache context changes", async () => {
|
||||
const cache = new SharedAppServerCache();
|
||||
const oldContext = cacheContext({ appServerUserAgent: "codex-cli/1.2.3" });
|
||||
const newContext = cacheContext({ appServerUserAgent: "codex-cli/1.2.4" });
|
||||
const oldContext = cacheContext({ codexPath: "codex-old" });
|
||||
const newContext = cacheContext({ codexPath: "codex-new" });
|
||||
const oldRefresh = deferred<readonly ReturnType<typeof thread>[]>();
|
||||
const newRefresh = deferred<readonly ReturnType<typeof thread>[]>();
|
||||
const oldSnapshot = vi.fn();
|
||||
|
|
@ -155,13 +153,13 @@ describe("SharedAppServerCache", () => {
|
|||
|
||||
it("keys in-flight thread list refreshes by the captured app-server cache context", async () => {
|
||||
const cache = new SharedAppServerCache();
|
||||
const context = cacheContext({ appServerUserAgent: "codex-cli/1.2.3" });
|
||||
const context = cacheContext({ codexPath: "codex-captured" });
|
||||
const capturedContext = { ...context };
|
||||
const refresh = deferred<readonly ReturnType<typeof thread>[]>();
|
||||
const onSnapshot = vi.fn();
|
||||
|
||||
const promise = cache.refreshThreadList(context, () => refresh.promise, onSnapshot);
|
||||
context.appServerUserAgent = "codex-cli/9.9.9";
|
||||
context.codexPath = "codex-mutated";
|
||||
|
||||
refresh.resolve([thread("captured")]);
|
||||
await expect(promise).resolves.toEqual([thread("captured")]);
|
||||
|
|
@ -176,7 +174,6 @@ function cacheContext(overrides: Partial<SharedAppServerCacheContext> = {}): Sha
|
|||
return {
|
||||
codexPath: "codex",
|
||||
vaultPath: "/vault",
|
||||
appServerUserAgent: "codex-cli/1.2.3",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type {
|
||||
AppServerClient,
|
||||
AppServerClientHandlers,
|
||||
AppServerStartStructuredTurnOptions,
|
||||
} from "../../src/app-server/connection/client";
|
||||
import type { AppServerClient, AppServerClientHandlers, AppServerStartStructuredTurnOptions } from "../../src/app-server/connection/client";
|
||||
import type { TurnItem, TurnRecord } from "../../src/app-server/protocol/turn";
|
||||
import type { RequestId, ServerNotification } from "../../src/app-server/connection/rpc-messages";
|
||||
import type { ServerInitialization } from "../../src/domain/server/initialization";
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ function createController({ connected = false, client = {} as AppServerClient }
|
|||
resetThreadTurnPresence: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
publishAppServerIdentity: vi.fn(),
|
||||
configuredCommand: () => "codex",
|
||||
refreshLiveState: vi.fn(),
|
||||
notifyConnectionFailed: vi.fn(),
|
||||
|
|
@ -76,8 +75,6 @@ describe("ChatConnectionController", () => {
|
|||
platformOs: "macos",
|
||||
userAgent: "test",
|
||||
});
|
||||
expect(host.publishAppServerIdentity).toHaveBeenNthCalledWith(1, null);
|
||||
expect(host.publishAppServerIdentity).toHaveBeenNthCalledWith(2, "test");
|
||||
expect(refreshPublishedAppServerMetadata).toHaveBeenCalledOnce();
|
||||
expect(host.loadSharedThreadList).toHaveBeenCalledOnce();
|
||||
expect(host.scheduleDeferredDiagnostics).toHaveBeenCalledOnce();
|
||||
|
|
@ -113,7 +110,6 @@ describe("ChatConnectionController", () => {
|
|||
controller.handleExit();
|
||||
|
||||
expect(host.invalidateResumeWork).toHaveBeenCalledOnce();
|
||||
expect(host.publishAppServerIdentity).toHaveBeenCalledWith(null);
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Codex app-server stopped.", {
|
||||
kind: "disconnected",
|
||||
message: "Codex app-server stopped.",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { messageStreamPresentationBlocks } from "../../../../../src/features/chat/presentation/message-stream/blocks";
|
||||
import { messageStreamPresentationBlocks } from "../../../../../src/features/chat/presentation/message-stream/view-model";
|
||||
import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items";
|
||||
|
||||
describe("message stream presentation blocks", () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { createChatStateStore } from "../../../../src/features/chat/application/
|
|||
import { messageStreamItems } from "../../../../src/features/chat/application/state/message-stream";
|
||||
import { renderChatPanelShell, unmountChatPanelShell, type ChatPanelShellParts } from "../../../../src/features/chat/panel/shell";
|
||||
import type { ChatPanelComposerShellState, ChatPanelMessageStreamShellState } from "../../../../src/features/chat/panel/shell-state";
|
||||
import type { ChatPanelSurface } from "../../../../src/features/chat/panel/surface/model";
|
||||
import type {
|
||||
ChatPanelComposerSurface,
|
||||
ChatPanelGoalSurface,
|
||||
ChatPanelToolbarSurface,
|
||||
} from "../../../../src/features/chat/panel/surface/model";
|
||||
import { installObsidianDomShims } from "../../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -418,7 +422,11 @@ function trackedShellParts(): TestShellParts {
|
|||
};
|
||||
}
|
||||
|
||||
function surfaceFixture(options: { toolbarConnected?: () => boolean } = {}): ChatPanelSurface {
|
||||
function surfaceFixture(options: { toolbarConnected?: () => boolean } = {}): {
|
||||
toolbar: ChatPanelToolbarSurface;
|
||||
goal: ChatPanelGoalSurface;
|
||||
composer: ChatPanelComposerSurface;
|
||||
} {
|
||||
return {
|
||||
toolbar: {
|
||||
state: {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import { act } from "preact/test-utils";
|
|||
import type { Thread } from "../../../../src/domain/threads/model";
|
||||
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
|
||||
import { createToolbarPanelActions, type ToolbarPanelActions } from "../../../../src/features/chat/panel/toolbar-actions";
|
||||
import type { ChatPanelSurface, ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/model";
|
||||
import type {
|
||||
ChatPanelComposerSurface,
|
||||
ChatPanelGoalSurface,
|
||||
ChatPanelToolbarSurface,
|
||||
} from "../../../../src/features/chat/panel/surface/model";
|
||||
import type { ThreadManagementActions } from "../../../../src/features/chat/application/threads/thread-management-actions";
|
||||
import { renderChatPanelShell, unmountChatPanelShell, type ChatPanelShellParts } from "../../../../src/features/chat/panel/shell";
|
||||
import { installObsidianDomShims } from "../../../support/dom";
|
||||
|
|
@ -146,7 +150,14 @@ function shellParts(store: ReturnType<typeof createChatStateStore>, toolbarActio
|
|||
};
|
||||
}
|
||||
|
||||
function surfaceFixture(store: ReturnType<typeof createChatStateStore>, toolbarActions: ToolbarPanelActions): ChatPanelSurface {
|
||||
function surfaceFixture(
|
||||
store: ReturnType<typeof createChatStateStore>,
|
||||
toolbarActions: ToolbarPanelActions,
|
||||
): {
|
||||
toolbar: ChatPanelToolbarSurface;
|
||||
goal: ChatPanelGoalSurface;
|
||||
composer: ChatPanelComposerSurface;
|
||||
} {
|
||||
return {
|
||||
toolbar: toolbarSurface(store, toolbarActions),
|
||||
goal: {
|
||||
|
|
|
|||
|
|
@ -176,16 +176,13 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
|
||||
it("publishes app-server metadata after connecting", async () => {
|
||||
const publishAppServerMetadata = vi.fn();
|
||||
const publishAppServerIdentity = vi.fn();
|
||||
connectionMock.state.client = connectedClient();
|
||||
const view = await chatView({
|
||||
host: chatHost({ publishAppServerMetadata, publishAppServerIdentity }),
|
||||
host: chatHost({ publishAppServerMetadata }),
|
||||
});
|
||||
|
||||
await view.connect();
|
||||
|
||||
expect(publishAppServerIdentity).toHaveBeenNthCalledWith(1, null);
|
||||
expect(publishAppServerIdentity).toHaveBeenNthCalledWith(2, "codex-test");
|
||||
expect(publishAppServerMetadata).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runtimeConfig: expect.any(Object),
|
||||
|
|
@ -1285,7 +1282,6 @@ interface ChatHostFixtureOverrides {
|
|||
refreshThreadList?: CodexChatHost["sharedCache"]["refreshThreadList"];
|
||||
cachedThreadList?: CodexChatHost["sharedCache"]["cachedThreadList"];
|
||||
cachedAppServerMetadata?: CodexChatHost["sharedCache"]["cachedAppServerMetadata"];
|
||||
publishAppServerIdentity?: CodexChatHost["appServerIdentity"]["publishAppServerIdentity"];
|
||||
}
|
||||
|
||||
function chatHost(overrides: ChatHostFixtureOverrides = {}): CodexChatHost {
|
||||
|
|
@ -1322,9 +1318,6 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): CodexChatHost {
|
|||
cachedThreadList: overrides.cachedThreadList ?? vi.fn(() => null),
|
||||
cachedAppServerMetadata: overrides.cachedAppServerMetadata ?? vi.fn(() => null),
|
||||
},
|
||||
appServerIdentity: {
|
||||
publishAppServerIdentity: overrides.publishAppServerIdentity ?? vi.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,8 +99,6 @@ describe("CodexThreadsView", () => {
|
|||
await view.refresh();
|
||||
|
||||
expect(connectionMock.state.connectCalls).toBe(1);
|
||||
expect(host.publishAppServerIdentity).toHaveBeenNthCalledWith(1, null);
|
||||
expect(host.publishAppServerIdentity).toHaveBeenNthCalledWith(2, "codex-test");
|
||||
expect(view.containerEl.textContent).toContain("Thread preview");
|
||||
});
|
||||
|
||||
|
|
@ -421,7 +419,6 @@ function threadsHost(overrides: Record<string, unknown> = {}) {
|
|||
getOpenPanelSnapshots: vi.fn(() => []),
|
||||
notifyThreadArchived: vi.fn(),
|
||||
notifyThreadRenamed: vi.fn(),
|
||||
publishAppServerIdentity: vi.fn(),
|
||||
refreshThreadList: vi.fn((fetchThreads: () => Promise<unknown>) => fetchThreads() as Promise<never[]>),
|
||||
cachedThreadList: vi.fn(() => null),
|
||||
...overrides,
|
||||
|
|
|
|||
|
|
@ -29,10 +29,6 @@ function sharedAppServerCache(plugin: CodexPanelPlugin): SharedAppServerCache {
|
|||
return (plugin as unknown as { sharedAppServerCache: SharedAppServerCache }).sharedAppServerCache;
|
||||
}
|
||||
|
||||
function chatHost(plugin: CodexPanelPlugin): CodexChatHost {
|
||||
return (plugin as unknown as { chatHost(): CodexChatHost }).chatHost();
|
||||
}
|
||||
|
||||
function sharedAppServerCacheContext(
|
||||
plugin: CodexPanelPlugin,
|
||||
overrides: Partial<SharedAppServerCacheContext> = {},
|
||||
|
|
@ -40,7 +36,6 @@ function sharedAppServerCacheContext(
|
|||
return {
|
||||
codexPath: plugin.settings.codexPath,
|
||||
vaultPath: plugin.vaultPath,
|
||||
appServerUserAgent: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -441,7 +436,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
|
|||
);
|
||||
const secondFetch = vi.fn().mockResolvedValue([thread("second")]);
|
||||
|
||||
const context = sharedAppServerCacheContext(plugin, { appServerUserAgent: "codex-cli/1.2.3" });
|
||||
const context = sharedAppServerCacheContext(plugin);
|
||||
const first = sharedAppServerCache(plugin).refreshThreadList(context, fetchThreads);
|
||||
const second = sharedAppServerCache(plugin).refreshThreadList(context, secondFetch);
|
||||
|
||||
|
|
@ -456,8 +451,8 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
|
|||
|
||||
it("keeps shared thread list refreshes separate across app-server cache contexts", async () => {
|
||||
const plugin = await pluginWithLeaves([]);
|
||||
const firstContext = sharedAppServerCacheContext(plugin, { codexPath: "codex-a", appServerUserAgent: "codex-cli/1.2.3" });
|
||||
const secondContext = sharedAppServerCacheContext(plugin, { codexPath: "codex-b", appServerUserAgent: "codex-cli/1.2.3" });
|
||||
const firstContext = sharedAppServerCacheContext(plugin, { codexPath: "codex-a" });
|
||||
const secondContext = sharedAppServerCacheContext(plugin, { codexPath: "codex-b" });
|
||||
let resolveFirst!: (threads: Thread[]) => void;
|
||||
const firstFetch = vi.fn(
|
||||
() =>
|
||||
|
|
@ -481,25 +476,9 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
|
|||
expect(sharedAppServerCache(plugin).cachedThreadList(firstContext)).toBeNull();
|
||||
});
|
||||
|
||||
it("keys shared thread list snapshots by published app-server identity", async () => {
|
||||
const plugin = await pluginWithLeaves([]);
|
||||
const host = chatHost(plugin);
|
||||
|
||||
host.appServerIdentity.publishAppServerIdentity("codex-cli/1.2.3");
|
||||
await host.sharedCache.refreshThreadList(() => Promise.resolve([thread("versioned")]));
|
||||
|
||||
expect(
|
||||
sharedAppServerCache(plugin).cachedThreadList(sharedAppServerCacheContext(plugin, { appServerUserAgent: "codex-cli/1.2.3" })),
|
||||
).toEqual([thread("versioned")]);
|
||||
|
||||
host.appServerIdentity.publishAppServerIdentity("codex-cli/9.9.9");
|
||||
|
||||
expect(host.sharedCache.cachedThreadList()).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the previous shared thread list when refresh fails", async () => {
|
||||
const plugin = await pluginWithLeaves([]);
|
||||
const context = sharedAppServerCacheContext(plugin, { appServerUserAgent: "codex-cli/1.2.3" });
|
||||
const context = sharedAppServerCacheContext(plugin);
|
||||
await sharedAppServerCache(plugin).refreshThreadList(context, () => Promise.resolve([thread("cached")]));
|
||||
|
||||
await expect(sharedAppServerCache(plugin).refreshThreadList(context, () => Promise.reject(new Error("boom")))).rejects.toThrow("boom");
|
||||
|
|
@ -651,9 +630,6 @@ function chatHostFixture(): CodexChatHost {
|
|||
cachedThreadList: vi.fn(() => null),
|
||||
cachedAppServerMetadata: vi.fn(() => null),
|
||||
},
|
||||
appServerIdentity: {
|
||||
publishAppServerIdentity: vi.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue