Narrow chat host dependencies around session composition

This commit is contained in:
murashit 2026-06-14 20:47:23 +09:00
parent 597046ff73
commit 46fb2edb76
11 changed files with 229 additions and 123 deletions

View file

@ -4,19 +4,39 @@ import type { CodexPanelSettings } from "../../../../settings/model";
import type { ChatTurnDiffViewState } from "../../domain/turn-diff";
export interface CodexChatHost {
readonly settingsRef: PluginSettingsRef;
readonly workspace: WorkspacePanels;
readonly sharedCache: SharedAppServerCacheFacade;
readonly threadSurfaces: ThreadSurfaceBroadcaster;
readonly appServerIdentity: AppServerIdentityPublisher;
}
export interface PluginSettingsRef {
readonly settings: CodexPanelSettings;
readonly vaultPath: string;
}
export interface WorkspacePanels {
openThreadInNewView(threadId: string): Promise<unknown>;
focusThreadInOpenView(threadId: string): Promise<boolean>;
openTurnDiff(state: ChatTurnDiffViewState): Promise<void>;
}
export interface ThreadSurfaceBroadcaster {
notifyThreadArchived(threadId: string): void;
notifyThreadRenamed(threadId: string, name: string | null): void;
refreshThreadsViewLiveState(): void;
refreshSharedThreadListFromOpenSurface(): void;
applyThreadListSnapshot(threads: readonly Thread[]): void;
publishAppServerMetadata(metadata: SharedServerMetadata): void;
}
interface SharedAppServerCacheFacade {
refreshThreadList(fetchThreads: () => Promise<readonly Thread[]>): Promise<readonly Thread[]>;
cachedThreadList(): readonly Thread[] | null;
publishAppServerMetadata(metadata: SharedServerMetadata): void;
publishAppServerIdentity(userAgent: string | null): void;
cachedAppServerMetadata(): SharedServerMetadata | null;
}
interface AppServerIdentityPublisher {
publishAppServerIdentity(userAgent: string | null): void;
}

View file

@ -4,7 +4,7 @@ import { createGoalActions } from "./goal-actions";
import { createSelectionActions } from "./selection-actions";
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle";
import type { ChatStateStore } from "../state/reducer";
import type { CodexChatHost } from "../ports/chat-host";
import type { PluginSettingsRef, ThreadSurfaceBroadcaster, WorkspacePanels } from "../ports/chat-host";
import { createThreadNamingParts } from "./naming-parts";
import { createThreadManagementActions } from "./thread-management-actions";
import { createThreadLifecycleParts } from "./lifecycle-parts";
@ -13,7 +13,9 @@ interface ThreadPartsContext {
obsidian: {
archiveAdapter: () => ArchiveExportAdapter;
};
plugin: CodexChatHost;
settingsRef: PluginSettingsRef;
workspace: Pick<WorkspacePanels, "focusThreadInOpenView" | "openThreadInNewView">;
threadSurfaces: ThreadSurfaceBroadcaster;
state: {
stateStore: ChatStateStore;
};
@ -54,12 +56,27 @@ interface ThreadPartsContext {
}
export function createThreadParts(context: ThreadPartsContext) {
const { obsidian, plugin, state, thread, status, notify, liveState, scroll, client, composer, lifecycle } = context;
const {
obsidian,
settingsRef,
workspace,
threadSurfaces,
state,
thread,
status,
notify,
liveState,
scroll,
client,
composer,
lifecycle,
} = context;
const stateStore = state.stateStore;
const currentClient = client.getClient;
const naming = createThreadNamingParts({
plugin,
settingsRef,
threadSurfaces,
stateStore,
client: {
currentClient,
@ -73,7 +90,9 @@ export function createThreadParts(context: ThreadPartsContext) {
const managementActions = createThreadManagementActions({
obsidian,
plugin,
settingsRef,
workspace,
threadSurfaces,
stateStore,
client: {
currentClient,
@ -99,7 +118,7 @@ export function createThreadParts(context: ThreadPartsContext) {
refreshLiveState: liveState.refresh,
});
const threadLifecycle = createThreadLifecycleParts({
plugin,
settingsRef,
stateStore,
client: {
currentClient,
@ -134,7 +153,7 @@ export function createThreadParts(context: ThreadPartsContext) {
}
interface ThreadSelectionActionsContext {
plugin: CodexChatHost;
workspace: Pick<WorkspacePanels, "focusThreadInOpenView">;
state: {
stateStore: ChatStateStore;
};
@ -152,13 +171,13 @@ export function createThreadSelectionActions(
closeForThreadSelection: () => void;
},
) {
const { plugin, thread, status } = context;
const { workspace, thread, status } = context;
const stateStore = context.state.stateStore;
return createSelectionActions({
stateStore,
closeForThreadSelection: refs.closeForThreadSelection,
focusThreadInOpenView: (threadId) => plugin.focusThreadInOpenView(threadId),
focusThreadInOpenView: workspace.focusThreadInOpenView,
resumeThread: thread.resumeThread,
addSystemMessage: status.addSystemMessage,
});

View file

@ -2,7 +2,7 @@ import type { AppServerClient } from "../../../../app-server/connection/client";
import { recoverRolloutTokenUsage } from "../../../../app-server/services/rollout-token-usage";
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle";
import type { ChatStateStore } from "../state/reducer";
import type { CodexChatHost } from "../ports/chat-host";
import type { PluginSettingsRef } from "../ports/chat-host";
import type { GoalActions } from "./goal-actions";
import { HistoryController } from "./history-controller";
import { createIdentitySync } from "./identity-sync";
@ -10,7 +10,7 @@ import { ResumeController } from "./resume-controller";
import { RestorationController } from "./restoration-controller";
export interface ThreadLifecyclePartsContext {
plugin: CodexChatHost;
settingsRef: PluginSettingsRef;
stateStore: ChatStateStore;
client: {
currentClient: () => AppServerClient | null;
@ -50,7 +50,7 @@ export interface ThreadLifecycleParts {
}
export function createThreadLifecycleParts(context: ThreadLifecyclePartsContext): ThreadLifecycleParts {
const { plugin, stateStore, client, lifecycle, thread, status, liveState, scroll, goals, resetThreadTurnPresence } = context;
const { settingsRef, stateStore, client, lifecycle, thread, status, liveState, scroll, goals, resetThreadTurnPresence } = context;
const { deferredTasks, resumeWork } = lifecycle;
const history = new HistoryController({
stateStore,
@ -75,7 +75,7 @@ export function createThreadLifecycleParts(context: ThreadLifecyclePartsContext)
});
const resume = new ResumeController({
stateStore,
vaultPath: plugin.vaultPath,
vaultPath: settingsRef.vaultPath,
resumeWork,
history,
restoration,

View file

@ -1,11 +1,12 @@
import type { AppServerClient } from "../../../../app-server/connection/client";
import type { ChatStateStore } from "../state/reducer";
import type { CodexChatHost } from "../ports/chat-host";
import type { PluginSettingsRef, ThreadSurfaceBroadcaster } from "../ports/chat-host";
import { AutoTitleController } from "./auto-title-controller";
import { ThreadRenameEditorController } from "./rename-editor-controller";
export interface ThreadNamingPartsContext {
plugin: CodexChatHost;
settingsRef: PluginSettingsRef;
threadSurfaces: ThreadSurfaceBroadcaster;
stateStore: ChatStateStore;
client: {
currentClient: () => AppServerClient | null;
@ -22,22 +23,26 @@ export interface ThreadNamingParts {
}
export function createThreadNamingParts(context: ThreadNamingPartsContext): ThreadNamingParts {
const { plugin, stateStore, client, status } = context;
const { settingsRef, threadSurfaces, stateStore, client, status } = context;
const rename = new ThreadRenameEditorController({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
vaultPath: settingsRef.vaultPath,
settings: () => settingsRef.settings,
ensureConnected: client.ensureConnected,
currentClient: client.currentClient,
addSystemMessage: status.addSystemMessage,
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),
notifyThreadRenamed: (threadId, name) => {
threadSurfaces.notifyThreadRenamed(threadId, name);
},
});
const autoTitle = new AutoTitleController({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
vaultPath: settingsRef.vaultPath,
settings: () => settingsRef.settings,
currentClient: client.currentClient,
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),
notifyThreadRenamed: (threadId, name) => {
threadSurfaces.notifyThreadRenamed(threadId, name);
},
});
return {

View file

@ -4,7 +4,7 @@ import { inheritedForkThreadName, normalizeExplicitThreadName } from "../../../.
import type { CodexPanelSettings } from "../../../../settings/model";
import type { ArchiveExportAdapter } from "../../../thread-export/archive-markdown";
import { exportArchivedThreadMarkdown } from "../../../thread-export/archive-markdown";
import type { CodexChatHost } from "../ports/chat-host";
import type { PluginSettingsRef, ThreadSurfaceBroadcaster, WorkspacePanels } from "../ports/chat-host";
import {
archivedSourceOpenForkFailedMessage,
finishBeforeArchivingThreadsMessage,
@ -58,7 +58,9 @@ export interface ThreadManagementActionsContext {
obsidian: {
archiveAdapter: () => ArchiveExportAdapter;
};
plugin: CodexChatHost;
settingsRef: PluginSettingsRef;
workspace: Pick<WorkspacePanels, "openThreadInNewView">;
threadSurfaces: Pick<ThreadSurfaceBroadcaster, "notifyThreadArchived" | "notifyThreadRenamed" | "refreshSharedThreadListFromOpenSurface">;
stateStore: ChatStateStore;
client: {
currentClient: () => AppServerClient | null;
@ -92,11 +94,11 @@ type ConnectedRenameThreadHost = Pick<
>;
export function createThreadManagementActions(context: ThreadManagementActionsContext): ThreadManagementActions {
const { obsidian, plugin, stateStore, client, status, notify, thread, composer } = context;
const { obsidian, settingsRef, workspace, threadSurfaces, stateStore, client, status, notify, thread, composer } = context;
const host: ThreadManagementActionsHost = {
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
vaultPath: settingsRef.vaultPath,
settings: () => settingsRef.settings,
archiveAdapter: obsidian.archiveAdapter,
ensureConnected: client.ensureConnected,
currentClient: client.currentClient,
@ -104,16 +106,16 @@ export function createThreadManagementActions(context: ThreadManagementActionsCo
showNotice: notify.showNotice,
setStatus: status.set,
setComposerText: composer.setText,
openThreadInNewView: (threadId) => plugin.openThreadInNewView(threadId),
openThreadInNewView: workspace.openThreadInNewView,
openThreadInCurrentPanel: thread.selectThread,
notifyThreadArchived: plugin.notifyThreadArchived.bind(plugin),
notifyThreadArchived: threadSurfaces.notifyThreadArchived,
notifyThreadRenamed: (threadId, name) => {
plugin.notifyThreadRenamed(threadId, name);
threadSurfaces.notifyThreadRenamed(threadId, name);
},
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
refreshThreads: thread.refreshThreads,
refreshSharedThreadListFromOpenSurface: () => {
plugin.refreshSharedThreadListFromOpenSurface();
threadSurfaces.refreshSharedThreadListFromOpenSurface();
},
};

View file

@ -5,14 +5,14 @@ import { currentModel, runtimeConfigOrDefault } from "../domain/runtime/effectiv
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
import { activeTurnId, type ChatStateStore } from "../application/state/reducer";
import type { ChatPanelComposerShellState } from "../panel/shell-state";
import type { CodexChatHost } from "../application/ports/chat-host";
import type { PluginSettingsRef } from "../application/ports/chat-host";
import type { ChatRuntimeSettingsActions } from "../application/runtime/settings-actions";
import { ChatComposerController } from "../panel/composer-controller";
import type { ChatPanelComposerProjection } from "../panel/surface/model";
export interface ConversationComposerContext {
app: App;
plugin: CodexChatHost;
settingsRef: PluginSettingsRef;
stateStore: ChatStateStore;
viewId: string;
surface: {
@ -36,14 +36,14 @@ export function createConversationComposer(
context: ConversationComposerContext,
refs: ConversationComposerRefs,
): ConversationComposerParts {
const { app, plugin, stateStore, viewId, surface, liveState } = context;
const { app, settingsRef, stateStore, viewId, surface, liveState } = context;
const scrollBridge = new MessageStreamScrollBridge();
const controller = new ChatComposerController({
app,
stateStore,
viewId,
sendShortcut: () => plugin.settings.sendShortcut,
scrollThreadFromComposerEdges: () => plugin.settings.scrollThreadFromComposerEdges,
sendShortcut: () => settingsRef.settings.sendShortcut,
scrollThreadFromComposerEdges: () => settingsRef.settings.scrollThreadFromComposerEdges,
canInterrupt: (state) => {
return state.turn.lifecycle.kind !== "idle" && Boolean(state.activeThread.id && activeTurnId(state));
},

View file

@ -10,7 +10,7 @@ import type { HistoryController } from "../application/threads/history-controlle
import type { ChatInboundController } from "../app-server/inbound/controller";
import type { MessageStreamItem, MessageStreamNoticeSection } from "../domain/message-stream/items";
import type { ChatPanelComposerShellState } from "../panel/shell-state";
import type { CodexChatHost } from "../application/ports/chat-host";
import type { PluginSettingsRef, WorkspacePanels } from "../application/ports/chat-host";
import { MessageStreamPresenter } from "../panel/surface/message-stream-presenter";
import type { ChatMessageScrollIntentState } from "../panel/surface/message-stream-scroll-intent";
import type { ChatPanelComposerProjection } from "../panel/surface/model";
@ -23,7 +23,8 @@ interface ConversationPartsContext {
owner: Component;
viewId: string;
};
plugin: CodexChatHost;
settingsRef: PluginSettingsRef;
workspace: WorkspacePanels;
state: {
stateStore: ChatStateStore;
};
@ -78,7 +79,7 @@ export function createConversationParts(
history: HistoryController;
},
) {
const { plugin, state, surface, runtime, thread, liveState, status, lifecycle, client, scroll } = context;
const { settingsRef, workspace, state, surface, runtime, thread, liveState, status, lifecycle, client, scroll } = context;
const { app, owner, viewId } = context.obsidian;
const stateStore = state.stateStore;
const currentClient = client.getClient;
@ -86,7 +87,7 @@ export function createConversationParts(
const composer = createConversationComposer(
{
app,
plugin,
settingsRef,
stateStore,
viewId,
surface: {
@ -112,7 +113,7 @@ export function createConversationParts(
const turnActions = createConversationTurnActions(
{
vaultPath: plugin.vaultPath,
vaultPath: settingsRef.vaultPath,
stateStore,
client: {
currentClient,
@ -150,7 +151,7 @@ export function createConversationParts(
store: stateStore,
},
workspace: {
vaultPath: plugin.vaultPath,
vaultPath: settingsRef.vaultPath,
},
scroll: {
consumeIntent: () => lifecycle.messageScrollIntent.consumeIntent(),
@ -166,7 +167,7 @@ export function createConversationParts(
rollbackThread: (threadId) => void refs.threadActions.rollbackThread(threadId),
forkThreadFromTurn: (threadId, turnId, archiveSource) => void refs.threadActions.forkThreadFromTurn(threadId, turnId, archiveSource),
implementPlan: (item: MessageStreamItem) => void turnActions.planImplementation.implement(item),
openTurnDiff: (state) => void plugin.openTurnDiff(state),
openTurnDiff: (state) => void workspace.openTurnDiff(state),
},
requests: {
pendingSignature: surface.pendingRequestsSignature,

View file

@ -340,8 +340,8 @@ export class ChatPanelSession {
private createSessionParts(): ChatPanelSessionParts {
const connectionHandlers = this.createConnectionHandlers();
const connection = new ConnectionManager(
() => this.environment.plugin.settings.codexPath,
this.environment.plugin.vaultPath,
() => this.environment.plugin.settingsRef.settings.codexPath,
this.environment.plugin.settingsRef.vaultPath,
connectionHandlers.handlers,
);
const connectionControllerRef = createChatPanelSessionDeferredRef<ChatConnectionController>("chat connection controller");
@ -372,7 +372,9 @@ export class ChatPanelSession {
obsidian: {
archiveAdapter: this.environment.obsidian.archiveAdapter,
},
plugin: this.environment.plugin,
settingsRef: this.environment.plugin.settingsRef,
workspace: this.environment.plugin.workspace,
threadSurfaces: this.environment.plugin.threadSurfaces,
state: {
stateStore: this.stateStore,
},
@ -427,7 +429,7 @@ export class ChatPanelSession {
});
const selection = createThreadSelectionActions(
{
plugin: this.environment.plugin,
workspace: this.environment.plugin.workspace,
state: {
stateStore: this.stateStore,
},
@ -492,8 +494,8 @@ export class ChatPanelSession {
);
const surface = createChatPanelSurface(
{
settings: this.environment.plugin.settings,
vaultPath: this.environment.plugin.vaultPath,
settings: this.environment.plugin.settingsRef.settings,
vaultPath: this.environment.plugin.settingsRef.vaultPath,
stateStore: this.stateStore,
restoredThreadPlaceholder: () => restoration.placeholder(),
},
@ -513,7 +515,8 @@ export class ChatPanelSession {
owner: this.environment.obsidian.owner,
viewId: this.environment.obsidian.viewId,
},
plugin: this.environment.plugin,
settingsRef: this.environment.plugin.settingsRef,
workspace: this.environment.plugin.workspace,
state: {
stateStore: this.stateStore,
},
@ -623,28 +626,28 @@ export class ChatPanelSession {
}): ChatPanelSessionServerParts {
const serverMetadata = createChatServerMetadataActions({
stateStore: this.stateStore,
vaultPath: this.environment.plugin.vaultPath,
vaultPath: this.environment.plugin.settingsRef.vaultPath,
currentClient: sessionPorts.currentClient,
publishAppServerMetadata: (metadata) => {
this.environment.plugin.publishAppServerMetadata(metadata);
this.environment.plugin.threadSurfaces.publishAppServerMetadata(metadata);
},
});
const serverDiagnostics = createChatServerDiagnosticsActions({
stateStore: this.stateStore,
vaultPath: this.environment.plugin.vaultPath,
vaultPath: this.environment.plugin.settingsRef.vaultPath,
currentClient: sessionPorts.currentClient,
publishAppServerMetadata: (metadata) => {
this.environment.plugin.publishAppServerMetadata(metadata);
this.environment.plugin.threadSurfaces.publishAppServerMetadata(metadata);
},
serverMetadataSnapshot: () => serverMetadata.serverMetadataSnapshot(),
});
const serverThreads = createChatServerThreadActions({
stateStore: this.stateStore,
vaultPath: this.environment.plugin.vaultPath,
vaultPath: this.environment.plugin.settingsRef.vaultPath,
currentClient: sessionPorts.currentClient,
runtimeSnapshotForState: runtimeSnapshotForChatState,
publishThreadList: (threads) => {
this.environment.plugin.applyThreadListSnapshot(threads);
this.environment.plugin.threadSurfaces.applyThreadListSnapshot(threads);
},
syncThreadGoal: (threadId) => {
void goals.syncThreadGoal(threadId);
@ -667,8 +670,12 @@ export class ChatPanelSession {
maybeNameThread: (threadId, turnId, completedSummary) => {
autoTitle.maybeAutoTitleThread(threadId, turnId, completedSummary);
},
notifyThreadArchived: this.environment.plugin.notifyThreadArchived.bind(this.environment.plugin),
notifyThreadRenamed: this.environment.plugin.notifyThreadRenamed.bind(this.environment.plugin),
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);
},
@ -707,9 +714,9 @@ export class ChatPanelSession {
setStatus: sideEffects.status.set,
addSystemMessage: sideEffects.status.addSystemMessage,
publishAppServerIdentity: (userAgent) => {
this.environment.plugin.publishAppServerIdentity(userAgent);
this.environment.plugin.appServerIdentity.publishAppServerIdentity(userAgent);
},
configuredCommand: () => this.environment.plugin.settings.codexPath,
configuredCommand: () => this.environment.plugin.settingsRef.settings.codexPath,
refreshLiveState: () => {
this.refreshLiveState();
},
@ -783,9 +790,9 @@ export class ChatPanelSession {
}
private applyCachedAppServerState(): void {
const threads = this.environment.plugin.cachedThreadList();
const threads = this.environment.plugin.sharedCache.cachedThreadList();
if (threads) this.parts.serverActions.threads.applyThreadList(threads);
const metadata = this.environment.plugin.cachedAppServerMetadata();
const metadata = this.environment.plugin.sharedCache.cachedAppServerMetadata();
if (metadata) this.parts.serverActions.metadata.applyAppServerMetadata(metadata);
}
@ -794,7 +801,7 @@ export class ChatPanelSession {
if (!root) return;
renderChatPanelShell(root, {
stateStore: this.stateStore,
showToolbar: this.environment.plugin.settings.showToolbar,
showToolbar: this.environment.plugin.settingsRef.settings.showToolbar,
parts: {
toolbar: {
surface: this.parts.surface.toolbar,
@ -828,7 +835,7 @@ export class ChatPanelSession {
}
private async loadSharedThreadList(): Promise<void> {
const threads = await this.environment.plugin.refreshThreadList(() => this.parts.serverActions.threads.loadThreadList());
const threads = await this.environment.plugin.sharedCache.refreshThreadList(() => this.parts.serverActions.threads.loadThreadList());
this.parts.serverActions.threads.applyThreadList(threads);
}
@ -842,7 +849,7 @@ export class ChatPanelSession {
}
private refreshLiveState(): void {
this.environment.plugin.refreshThreadsViewLiveState();
this.environment.plugin.threadSurfaces.refreshThreadsViewLiveState();
}
private deferLiveStateRefresh(): void {
@ -909,7 +916,7 @@ export class ChatPanelSession {
return connectionDiagnosticsModel({
state: this.state,
connected: this.parts.connection.manager.isConnected(),
configuredCommand: this.environment.plugin.settings.codexPath,
configuredCommand: this.environment.plugin.settingsRef.settings.codexPath,
}).map((section) => ({
title: section.title,
auditFacts: section.rows.map((row) => ({ key: row.label, value: row.value })),

View file

@ -165,35 +165,42 @@ export default class CodexPanelPlugin extends Plugin {
private chatHost(): CodexChatHost {
return {
settings: this.settings,
vaultPath: this.vaultPath,
openThreadInNewView: (threadId) => this.panels.openThreadInNewView(threadId),
focusThreadInOpenView: (threadId) => this.panels.focusThreadInOpenView(threadId),
openTurnDiff: (state) => this.openTurnDiff(state),
notifyThreadArchived: (threadId) => {
this.threadSurfaces.notifyThreadArchived(threadId);
settingsRef: this,
workspace: {
openThreadInNewView: (threadId) => this.panels.openThreadInNewView(threadId),
focusThreadInOpenView: (threadId) => this.panels.focusThreadInOpenView(threadId),
openTurnDiff: (state) => this.openTurnDiff(state),
},
notifyThreadRenamed: (threadId, name) => {
this.threadSurfaces.notifyThreadRenamed(threadId, name);
threadSurfaces: {
notifyThreadArchived: (threadId) => {
this.threadSurfaces.notifyThreadArchived(threadId);
},
notifyThreadRenamed: (threadId, name) => {
this.threadSurfaces.notifyThreadRenamed(threadId, name);
},
refreshThreadsViewLiveState: () => {
this.threadSurfaces.refreshThreadsViewLiveState();
},
refreshSharedThreadListFromOpenSurface: () => {
this.threadSurfaces.refreshSharedThreadListFromOpenSurface();
},
applyThreadListSnapshot: (threads) => {
this.applyThreadListSnapshot(threads);
},
publishAppServerMetadata: (metadata) => {
this.publishAppServerMetadata(metadata);
},
},
refreshThreadsViewLiveState: () => {
this.threadSurfaces.refreshThreadsViewLiveState();
sharedCache: {
refreshThreadList: (fetchThreads) => this.refreshThreadList(fetchThreads),
cachedThreadList: () => this.cachedThreadList(),
cachedAppServerMetadata: () => this.sharedAppServerCache.cachedAppServerMetadata(this.sharedAppServerCacheContext()),
},
refreshSharedThreadListFromOpenSurface: () => {
this.threadSurfaces.refreshSharedThreadListFromOpenSurface();
appServerIdentity: {
publishAppServerIdentity: (userAgent) => {
this.publishAppServerIdentity(userAgent);
},
},
applyThreadListSnapshot: (threads) => {
this.applyThreadListSnapshot(threads);
},
refreshThreadList: (fetchThreads) => this.refreshThreadList(fetchThreads),
cachedThreadList: () => this.cachedThreadList(),
publishAppServerMetadata: (metadata) => {
this.publishAppServerMetadata(metadata);
},
publishAppServerIdentity: (userAgent) => {
this.publishAppServerIdentity(userAgent);
},
cachedAppServerMetadata: () => this.sharedAppServerCache.cachedAppServerMetadata(this.sharedAppServerCacheContext()),
};
}

View file

@ -1242,30 +1242,61 @@ async function flushAsyncTicks(): Promise<void> {
}
}
function chatHost(overrides: Partial<CodexChatHost> = {}): CodexChatHost {
interface ChatHostFixtureOverrides {
settings?: Partial<typeof DEFAULT_SETTINGS>;
vaultPath?: string;
openThreadInNewView?: CodexChatHost["workspace"]["openThreadInNewView"];
focusThreadInOpenView?: CodexChatHost["workspace"]["focusThreadInOpenView"];
openTurnDiff?: CodexChatHost["workspace"]["openTurnDiff"];
notifyThreadArchived?: CodexChatHost["threadSurfaces"]["notifyThreadArchived"];
notifyThreadRenamed?: CodexChatHost["threadSurfaces"]["notifyThreadRenamed"];
refreshSharedThreadListFromOpenSurface?: CodexChatHost["threadSurfaces"]["refreshSharedThreadListFromOpenSurface"];
refreshThreadsViewLiveState?: CodexChatHost["threadSurfaces"]["refreshThreadsViewLiveState"];
applyThreadListSnapshot?: CodexChatHost["threadSurfaces"]["applyThreadListSnapshot"];
publishAppServerMetadata?: CodexChatHost["threadSurfaces"]["publishAppServerMetadata"];
refreshThreadList?: CodexChatHost["sharedCache"]["refreshThreadList"];
cachedThreadList?: CodexChatHost["sharedCache"]["cachedThreadList"];
cachedAppServerMetadata?: CodexChatHost["sharedCache"]["cachedAppServerMetadata"];
publishAppServerIdentity?: CodexChatHost["appServerIdentity"]["publishAppServerIdentity"];
}
function chatHost(overrides: ChatHostFixtureOverrides = {}): CodexChatHost {
const settings = {
...DEFAULT_SETTINGS,
codexPath: "codex",
sendShortcut: "enter" as const,
...overrides.settings,
};
return {
settings: {
...DEFAULT_SETTINGS,
codexPath: "codex",
sendShortcut: "enter",
settingsRef: {
settings,
vaultPath: overrides.vaultPath ?? "/vault",
},
workspace: {
openThreadInNewView: overrides.openThreadInNewView ?? vi.fn(),
focusThreadInOpenView: overrides.focusThreadInOpenView ?? vi.fn().mockResolvedValue(false),
openTurnDiff: overrides.openTurnDiff ?? vi.fn(),
},
threadSurfaces: {
notifyThreadArchived: overrides.notifyThreadArchived ?? vi.fn(),
notifyThreadRenamed: overrides.notifyThreadRenamed ?? vi.fn(),
refreshSharedThreadListFromOpenSurface: overrides.refreshSharedThreadListFromOpenSurface ?? vi.fn(),
refreshThreadsViewLiveState: overrides.refreshThreadsViewLiveState ?? vi.fn(),
applyThreadListSnapshot: overrides.applyThreadListSnapshot ?? vi.fn(),
publishAppServerMetadata: overrides.publishAppServerMetadata ?? vi.fn(),
},
sharedCache: {
refreshThreadList:
overrides.refreshThreadList ??
(vi.fn(
(fetchThreads: () => Promise<unknown>) => fetchThreads() as Promise<never[]>,
) as CodexChatHost["sharedCache"]["refreshThreadList"]),
cachedThreadList: overrides.cachedThreadList ?? vi.fn(() => null),
cachedAppServerMetadata: overrides.cachedAppServerMetadata ?? vi.fn(() => null),
},
appServerIdentity: {
publishAppServerIdentity: overrides.publishAppServerIdentity ?? vi.fn(),
},
vaultPath: "/vault",
openThreadInNewView: vi.fn(),
focusThreadInOpenView: vi.fn().mockResolvedValue(false),
openTurnDiff: vi.fn(),
notifyThreadArchived: vi.fn(),
notifyThreadRenamed: vi.fn(),
refreshSharedThreadListFromOpenSurface: vi.fn(),
refreshThreadsViewLiveState: vi.fn(),
applyThreadListSnapshot: vi.fn(),
refreshThreadList: vi.fn(
(fetchThreads: () => Promise<unknown>) => fetchThreads() as Promise<never[]>,
) as CodexChatHost["refreshThreadList"],
cachedThreadList: vi.fn(() => null),
publishAppServerMetadata: vi.fn(),
publishAppServerIdentity: vi.fn(),
cachedAppServerMetadata: vi.fn(() => null),
...overrides,
};
}

View file

@ -478,16 +478,16 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves([]);
const host = chatHost(plugin);
host.publishAppServerIdentity("codex-cli/1.2.3");
await host.refreshThreadList(() => Promise.resolve([thread("versioned")]));
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.publishAppServerIdentity("codex-cli/9.9.9");
host.appServerIdentity.publishAppServerIdentity("codex-cli/9.9.9");
expect(host.cachedThreadList()).toBeNull();
expect(host.sharedCache.cachedThreadList()).toBeNull();
});
it("keeps the previous shared thread list when refresh fails", async () => {
@ -616,24 +616,38 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) {
},
containerEl,
} as never,
{
chatHostFixture(),
);
}
function chatHostFixture(): CodexChatHost {
return {
settingsRef: {
settings: { ...DEFAULT_SETTINGS, codexPath: "codex", sendShortcut: "enter" },
vaultPath: "/vault",
},
workspace: {
openThreadInNewView: vi.fn(),
focusThreadInOpenView: vi.fn(),
openTurnDiff: vi.fn(),
},
threadSurfaces: {
notifyThreadArchived: vi.fn(),
notifyThreadRenamed: vi.fn(),
refreshSharedThreadListFromOpenSurface: vi.fn(),
refreshThreadsViewLiveState: vi.fn(),
applyThreadListSnapshot: vi.fn(),
publishAppServerMetadata: vi.fn(),
},
sharedCache: {
refreshThreadList: vi.fn((fetchThreads: () => Promise<unknown>) => fetchThreads() as Promise<never[]>),
cachedThreadList: vi.fn(() => null),
publishAppServerMetadata: vi.fn(),
publishAppServerIdentity: vi.fn(),
cachedAppServerMetadata: vi.fn(() => null),
},
);
appServerIdentity: {
publishAppServerIdentity: vi.fn(),
},
};
}
function thread(id: string): Thread {