Rename app-server metadata surfaces

This commit is contained in:
murashit 2026-05-28 17:40:53 +09:00
parent 10b8a8473b
commit c1af92c6e9
14 changed files with 117 additions and 117 deletions

View file

@ -2,7 +2,7 @@
Codex Panel is a desktop-only Obsidian plugin that brings Codex into a panel in the right sidebar. It is built as an Obsidian frontend for `codex app-server`: the plugin starts Codex locally, communicates with it over stdio, and uses the current vault root as the Codex working directory.
Codex stays in charge of models, reasoning effort, sandbox modes, approval policies, MCP servers, hooks, skills, network access, providers, and thread history. Codex Panel adds the Obsidian surface around that Codex App Server session instead of replacing Codex configuration or policy.
Codex stays in charge of models, reasoning effort, sandbox modes, approval policies, MCP servers, hooks, skills, network access, providers, and thread history. Codex Panel adds the Obsidian surface around that Codex App Server connection instead of replacing Codex configuration or policy.
That boundary is also the privacy and security boundary: Codex Panel does not store API keys or make its own network requests. Data sent from the panel is handled by the configured Codex CLI according to your Codex configuration.

View file

@ -17,7 +17,7 @@ The source tree is organized by responsibility rather than by the original singl
- `src/main.ts` registers Obsidian views, commands, settings, and lifecycle hooks.
- `src/app-server/` owns app-server transport, connection lifecycle, compatibility helpers, and RPC facades.
- `src/features/chat/` owns the main Codex chat surface: Obsidian `ItemView` classes, panel orchestration, request/session controllers, composer behavior, display models, chat-only UI renderers, approvals, user input, thread actions, and panel-specific state.
- `src/features/chat/` owns the main Codex chat surface: Obsidian `ItemView` classes, panel orchestration, request/app-server controllers, composer behavior, display models, chat-only UI renderers, approvals, user input, thread actions, and panel-specific state.
- `src/features/threads-view/` owns the dedicated Obsidian thread list view, including app-server thread list rendering and live open-panel snapshot aggregation.
- `src/features/selection-rewrite/` owns the Markdown editor selection rewrite command, popover, prompt/output handling, and short-lived rewrite session runner.
- `src/runtime/` owns Codex runtime configuration projection, model metadata, collaboration mode, and compact labels used by views.

View file

@ -1,6 +1,6 @@
import { AppServerClient } from "./client";
export async function withAppServerSession<T>(
export async function withAppServerConnection<T>(
codexPath: string,
cwd: string,
operation: (client: AppServerClient) => Promise<T>,

View file

@ -11,11 +11,11 @@ import type { McpServerStatus } from "../../generated/app-server/v2/McpServerSta
import type { Model } from "../../generated/app-server/v2/Model";
import type { RateLimitSnapshot } from "../../generated/app-server/v2/RateLimitSnapshot";
import type { SkillMetadata } from "../../generated/app-server/v2/SkillMetadata";
import type { SharedSessionMetadata } from "../../runtime/shared-app-server-state";
import type { SharedAppServerMetadata } from "../../runtime/shared-app-server-state";
import { requestedOrConfiguredServiceTier, type RuntimeSnapshot } from "../../runtime/state";
import type { ChatAction, ChatState, ChatStateStore } from "./chat-state";
export interface ChatSessionControllerHost {
export interface ChatAppServerControllerHost {
stateStore: ChatStateStore;
vaultPath: string;
currentClient: () => AppServerClient | null;
@ -24,11 +24,11 @@ export interface ChatSessionControllerHost {
}
export interface RefreshCapabilityDiagnosticsOptions {
cachedSessionMetadata?: boolean;
cachedAppServerMetadata?: boolean;
}
export class ChatSessionController {
constructor(private readonly host: ChatSessionControllerHost) {}
export class ChatAppServerController {
constructor(private readonly host: ChatAppServerControllerHost) {}
private get state(): ChatState {
return this.host.stateStore.getState();
@ -49,7 +49,7 @@ export class ChatSessionController {
return response.data;
}
sessionMetadataSnapshot(): SharedSessionMetadata {
appServerMetadataSnapshot(): SharedAppServerMetadata {
return {
effectiveConfig: this.state.effectiveConfig,
availableModels: this.state.availableModels,
@ -59,7 +59,7 @@ export class ChatSessionController {
};
}
applySessionMetadata(metadata: SharedSessionMetadata): void {
applyAppServerMetadata(metadata: SharedAppServerMetadata): void {
this.dispatch({
type: "thread/list-applied",
effectiveConfig: metadata.effectiveConfig,
@ -70,7 +70,7 @@ export class ChatSessionController {
});
}
async loadSessionMetadata(): Promise<SharedSessionMetadata | null> {
async loadAppServerMetadata(): Promise<SharedAppServerMetadata | null> {
const client = this.host.currentClient();
if (!client) return null;
const effectiveConfig = await client.readEffectiveConfig(this.host.vaultPath);
@ -88,9 +88,9 @@ export class ChatSessionController {
};
}
async refreshSessionMetadata(): Promise<SharedSessionMetadata | null> {
const metadata = await this.loadSessionMetadata();
if (metadata) this.applySessionMetadata(metadata);
async refreshAppServerMetadata(): Promise<SharedAppServerMetadata | null> {
const metadata = await this.loadAppServerMetadata();
if (metadata) this.applyAppServerMetadata(metadata);
return metadata;
}
@ -187,7 +187,7 @@ export class ChatSessionController {
if (!client) return;
const probes: Promise<void>[] = [];
if (!options.cachedSessionMetadata) {
if (!options.cachedAppServerMetadata) {
probes.push(
this.probeCapability(
"model/list",

View file

@ -42,7 +42,7 @@ import { createApprovalResultItem, createUserInputResultItem } from "./request-s
export interface ChatControllerActions {
refreshThreads: () => void;
refreshSkills: (forceReload?: boolean) => void;
publishSessionMetadata: () => void;
publishAppServerMetadata: () => void;
maybeNameThread: (threadId: string, turn: Turn) => void;
notifyThreadArchived: (threadId: string) => void;
notifyThreadRenamed: (threadId: string, name: string | null) => void;
@ -297,12 +297,12 @@ export class ChatController {
this.dispatch({ type: "thread/token-usage-set", tokenUsage: params.tokenUsage });
} else if (method === "account/rateLimits/updated") {
this.dispatch({ type: "thread/list-applied", rateLimit: params.rateLimits });
this.actions.publishSessionMetadata();
this.actions.publishAppServerMetadata();
} else if (method === "skills/changed") {
this.actions.refreshSkills(true);
} else if (method === "mcpServer/startupStatus/updated") {
this.handleMcpStartupStatus(params);
this.actions.publishSessionMetadata();
this.actions.publishAppServerMetadata();
}
}

View file

@ -44,7 +44,7 @@ import { compactContextLabel, modelOverrideMessage, reasoningEffortOverrideMessa
import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult } from "./slash-commands";
import type { ThreadReferenceInput } from "./slash-commands";
import { mcpStatusLines } from "./mcp-status";
import { ChatSessionController } from "./chat-session-controller";
import { ChatAppServerController } from "./chat-app-server-controller";
import { statusValue, usageLimitStatusLines } from "./status-lines";
import { ThreadHistoryLoader } from "./thread-history";
import { ThreadRenameController } from "./thread-rename";
@ -76,7 +76,7 @@ import { renderChatPanelShell, unmountChatPanelShell, type ChatPanelSlotSnapshot
import type { ChatTurnDiffViewState } from "./ui/turn-diff";
import { ChatMessageRenderer, type ChatMessageScrollIntent } from "./chat-message-renderer";
import type { OpenCodexPanelSnapshot } from "../../runtime/open-panel-snapshot";
import type { SharedSessionMetadata } from "../../runtime/shared-app-server-state";
import type { SharedAppServerMetadata } from "../../runtime/shared-app-server-state";
import { ChatThreadActionController } from "./thread-actions";
import { unmountReactRoot } from "../../shared/ui/react-root";
@ -93,8 +93,8 @@ export interface CodexChatHost {
refreshSharedThreadListFromOpenSurface(): void;
refreshThreadList(fetchThreads: () => Promise<readonly Thread[]>): Promise<readonly Thread[]>;
cachedThreadList(): readonly Thread[] | null;
publishSessionMetadata(metadata: SharedSessionMetadata): void;
cachedSessionMetadata(): SharedSessionMetadata | null;
publishAppServerMetadata(metadata: SharedAppServerMetadata): void;
cachedAppServerMetadata(): SharedAppServerMetadata | null;
}
interface RestoredThreadState {
@ -107,7 +107,7 @@ export class CodexChatView extends ItemView {
private client: AppServerClient | null = null;
private readonly connection: ConnectionManager;
private readonly controller: ChatController;
private readonly session: ChatSessionController;
private readonly appServer: ChatAppServerController;
private readonly history: ThreadHistoryLoader;
private readonly threadActions: ChatThreadActionController;
private readonly threadRename: ThreadRenameController;
@ -130,7 +130,7 @@ export class CodexChatView extends ItemView {
private closing = false;
private nextMessageScrollIntent: ChatMessageScrollIntent = "auto";
private lastPendingRequestFocusSignature = "";
private scheduledSessionWarmupTimer: number | null = null;
private scheduledAppServerWarmupTimer: number | null = null;
constructor(
leaf: WorkspaceLeaf,
@ -204,8 +204,8 @@ export class CodexChatView extends ItemView {
void this.refreshThreads();
},
refreshSkills: (forceReload) => void this.refreshSkills(forceReload),
publishSessionMetadata: () => {
this.publishSessionMetadataSnapshot();
publishAppServerMetadata: () => {
this.publishAppServerMetadataSnapshot();
},
maybeNameThread: (threadId, turn) => {
this.threadRename.maybeAutoNameThread(threadId, turn);
@ -217,13 +217,13 @@ export class CodexChatView extends ItemView {
this.plugin.notifyThreadRenamed(threadId, name);
},
recordMcpStartupStatus: (name, status, message) => {
this.session.recordMcpStartupStatus(name, status, message);
this.appServer.recordMcpStartupStatus(name, status, message);
this.scheduleRender();
},
respondToServerRequest: (requestId, result) => this.respondToServerRequest(requestId, result),
rejectServerRequest: (requestId, code, message) => this.rejectServerRequest(requestId, code, message),
});
this.session = new ChatSessionController({
this.appServer = new ChatAppServerController({
stateStore: this.chatState,
vaultPath: this.plugin.vaultPath,
currentClient: () => this.connection.currentClient(),
@ -353,7 +353,7 @@ export class CodexChatView extends ItemView {
this.invalidateResumeWork();
this.restoredThread = null;
this.clearDeferredRestoredThreadHydration();
this.scheduleDeferredSessionWarmup();
this.scheduleDeferredAppServerWarmup();
return;
}
@ -369,13 +369,13 @@ export class CodexChatView extends ItemView {
}
applyThreadListSnapshot(threads: readonly Thread[]): void {
this.session.applyThreadList(threads);
this.appServer.applyThreadList(threads);
this.refreshTabHeader();
this.render();
}
applySessionMetadataSnapshot(metadata: SharedSessionMetadata): void {
this.session.applySessionMetadata(metadata);
applyAppServerMetadataSnapshot(metadata: SharedAppServerMetadata): void {
this.appServer.applyAppServerMetadata(metadata);
this.render();
}
@ -457,7 +457,7 @@ export class CodexChatView extends ItemView {
);
this.applyCachedSharedAppServerState();
this.render();
this.scheduleDeferredSessionWarmup();
this.scheduleDeferredAppServerWarmup();
this.scheduleDeferredRestoredThreadHydration();
}
@ -468,7 +468,7 @@ export class CodexChatView extends ItemView {
this.invalidateResumeWork();
this.connectingPromise = null;
this.clearDeferredRestoredThreadHydration();
this.clearDeferredSessionWarmup();
this.clearDeferredAppServerWarmup();
if (this.scheduledRenderTimer !== null) {
this.containerEl.win.clearTimeout(this.scheduledRenderTimer);
this.scheduledRenderTimer = null;
@ -522,9 +522,9 @@ export class CodexChatView extends ItemView {
if (this.isStaleConnectionGeneration(generation)) return;
this.client = this.connection.currentClient();
if (!this.client) throw new Error("Codex app-server connection did not initialize.");
const metadata = await this.session.refreshSessionMetadata();
const metadata = await this.appServer.refreshAppServerMetadata();
if (this.isStaleConnectionGeneration(generation)) return;
if (metadata) this.plugin.publishSessionMetadata(metadata);
if (metadata) this.plugin.publishAppServerMetadata(metadata);
await this.loadSharedThreadList();
if (this.isStaleConnectionGeneration(generation)) return;
this.scheduleDeferredDiagnostics();
@ -568,8 +568,8 @@ export class CodexChatView extends ItemView {
if (!this.client) return;
try {
await this.loadSharedThreadList();
const metadata = await this.session.refreshSessionMetadata();
if (metadata) this.plugin.publishSessionMetadata(metadata);
const metadata = await this.appServer.refreshAppServerMetadata();
if (metadata) this.plugin.publishAppServerMetadata(metadata);
this.refreshTabHeader();
this.render();
} catch (error) {
@ -582,16 +582,16 @@ export class CodexChatView extends ItemView {
await this.ensureConnected();
if (!this.client) return;
this.clearDeferredDiagnostics();
await this.session.refreshCapabilityDiagnostics();
this.publishSessionMetadataSnapshot();
await this.appServer.refreshCapabilityDiagnostics();
this.publishAppServerMetadataSnapshot();
this.render();
}
private async refreshSkills(forceReload = false): Promise<void> {
this.client = this.connection.currentClient();
if (!this.client) return;
await this.session.refreshSkills(forceReload);
this.publishSessionMetadataSnapshot();
await this.appServer.refreshSkills(forceReload);
this.publishAppServerMetadataSnapshot();
this.render();
}
@ -660,19 +660,19 @@ export class CodexChatView extends ItemView {
}
private async loadSharedThreadList(): Promise<void> {
const threads = await this.plugin.refreshThreadList(() => this.session.loadThreadList());
this.session.applyThreadList(threads);
const threads = await this.plugin.refreshThreadList(() => this.appServer.loadThreadList());
this.appServer.applyThreadList(threads);
}
private publishSessionMetadataSnapshot(): void {
this.plugin.publishSessionMetadata(this.session.sessionMetadataSnapshot());
private publishAppServerMetadataSnapshot(): void {
this.plugin.publishAppServerMetadata(this.appServer.appServerMetadataSnapshot());
}
private applyCachedSharedAppServerState(): void {
const threads = this.plugin.cachedThreadList();
if (threads) this.session.applyThreadList(threads);
const metadata = this.plugin.cachedSessionMetadata();
if (metadata) this.session.applySessionMetadata(metadata);
if (threads) this.appServer.applyThreadList(threads);
const metadata = this.plugin.cachedAppServerMetadata();
if (metadata) this.appServer.applyAppServerMetadata(metadata);
}
private requestWorkspaceLayoutSave(): void {
@ -712,7 +712,7 @@ export class CodexChatView extends ItemView {
let optimisticUserId: string | null = null;
try {
if (!this.state.activeThreadId) {
const threadResponse = await this.session.startThread();
const threadResponse = await this.appServer.startThread();
if (!threadResponse) return;
this.notifyActiveThreadIdentityChanged();
this.threadRename.resetThreadTurnPresence(false);
@ -1151,19 +1151,19 @@ export class CodexChatView extends ItemView {
this.scheduledRestoredThreadHydrationTimer = null;
}
private scheduleDeferredSessionWarmup(): void {
if (!this.opened || this.connection.isConnected() || this.scheduledSessionWarmupTimer !== null) return;
this.scheduledSessionWarmupTimer = this.containerEl.win.setTimeout(() => {
this.scheduledSessionWarmupTimer = null;
private scheduleDeferredAppServerWarmup(): void {
if (!this.opened || this.connection.isConnected() || this.scheduledAppServerWarmupTimer !== null) return;
this.scheduledAppServerWarmupTimer = this.containerEl.win.setTimeout(() => {
this.scheduledAppServerWarmupTimer = null;
if (!this.opened || this.closing || this.connection.isConnected()) return;
void this.ensureConnected();
}, 0);
}
private clearDeferredSessionWarmup(): void {
if (this.scheduledSessionWarmupTimer === null) return;
this.containerEl.win.clearTimeout(this.scheduledSessionWarmupTimer);
this.scheduledSessionWarmupTimer = null;
private clearDeferredAppServerWarmup(): void {
if (this.scheduledAppServerWarmupTimer === null) return;
this.containerEl.win.clearTimeout(this.scheduledAppServerWarmupTimer);
this.scheduledAppServerWarmupTimer = null;
}
private activeThreadTitle(): string | null {
@ -1523,8 +1523,8 @@ export class CodexChatView extends ItemView {
private async refreshDeferredDiagnostics(): Promise<void> {
if (!this.connection.isConnected()) return;
await this.session.refreshCapabilityDiagnostics({ cachedSessionMetadata: true });
this.publishSessionMetadataSnapshot();
await this.appServer.refreshCapabilityDiagnostics({ cachedAppServerMetadata: true });
this.publishAppServerMetadataSnapshot();
this.render();
}

View file

@ -11,11 +11,11 @@ import type { Thread } from "./generated/app-server/v2/Thread";
import type { Model } from "./generated/app-server/v2/Model";
import {
applySharedModels,
applySharedSessionMetadata,
applySharedAppServerMetadata,
applySharedThreadList,
createSharedAppServerState,
type SharedAppServerState,
type SharedSessionMetadata,
type SharedAppServerMetadata,
} from "./runtime/shared-app-server-state";
import { DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormalizedData, type CodexPanelSettings } from "./settings/model";
import { CodexPanelSettingTab } from "./settings/tab";
@ -262,11 +262,11 @@ export default class CodexPanelPlugin extends Plugin {
return this.sharedAppServerState.threads;
}
publishSessionMetadata(metadata: SharedSessionMetadata): void {
this.sharedAppServerState = applySharedSessionMetadata(this.sharedAppServerState, metadata);
publishAppServerMetadata(metadata: SharedAppServerMetadata): void {
this.sharedAppServerState = applySharedAppServerMetadata(this.sharedAppServerState, metadata);
for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_PANEL)) {
if (leaf.view instanceof CodexChatView) {
leaf.view.applySessionMetadataSnapshot(metadata);
leaf.view.applyAppServerMetadataSnapshot(metadata);
}
}
}
@ -280,8 +280,8 @@ export default class CodexPanelPlugin extends Plugin {
}
}
cachedSessionMetadata(): SharedSessionMetadata | null {
if (!this.sharedAppServerState.sessionMetadataLoaded) return null;
cachedAppServerMetadata(): SharedAppServerMetadata | null {
if (!this.sharedAppServerState.appServerMetadataLoaded) return null;
return {
effectiveConfig: this.sharedAppServerState.effectiveConfig,
availableModels: this.sharedAppServerState.availableModels,

View file

@ -6,7 +6,7 @@ import type { RateLimitSnapshot } from "../generated/app-server/v2/RateLimitSnap
import type { SkillMetadata } from "../generated/app-server/v2/SkillMetadata";
import type { Thread } from "../generated/app-server/v2/Thread";
export interface SharedSessionMetadata {
export interface SharedAppServerMetadata {
effectiveConfig: ConfigReadResponse | null;
availableModels: readonly Model[];
availableSkills: readonly SkillMetadata[];
@ -14,15 +14,15 @@ export interface SharedSessionMetadata {
appServerDiagnostics: AppServerDiagnostics;
}
export interface SharedAppServerState extends SharedSessionMetadata {
export interface SharedAppServerState extends SharedAppServerMetadata {
threads: readonly Thread[] | null;
sessionMetadataLoaded: boolean;
appServerMetadataLoaded: boolean;
}
export function createSharedAppServerState(): SharedAppServerState {
return {
threads: null,
sessionMetadataLoaded: false,
appServerMetadataLoaded: false,
effectiveConfig: null,
availableModels: [],
availableSkills: [],
@ -38,11 +38,11 @@ export function applySharedThreadList(state: SharedAppServerState, threads: read
};
}
export function applySharedSessionMetadata(state: SharedAppServerState, metadata: SharedSessionMetadata): SharedAppServerState {
export function applySharedAppServerMetadata(state: SharedAppServerState, metadata: SharedAppServerMetadata): SharedAppServerState {
return {
...state,
...metadata,
sessionMetadataLoaded: true,
appServerMetadataLoaded: true,
};
}

View file

@ -1,7 +1,7 @@
import { type App, Notice, type Plugin, PluginSettingTab, Setting } from "obsidian";
import type { AppServerClient } from "../app-server/client";
import { withAppServerSession } from "../app-server/session-client";
import { withAppServerConnection } from "../app-server/connection-client";
import { DEFAULT_CODEX_PATH } from "../constants";
import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort";
import type { HookMetadata } from "../generated/app-server/v2/HookMetadata";
@ -231,7 +231,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
let failedCount = 0;
try {
const result = await this.withSettingsSession((client) => loadSettingsData(client, this.plugin.vaultPath));
const result = await this.withSettingsConnection((client) => loadSettingsData(client, this.plugin.vaultPath));
if (result.models.ok) {
this.models = result.models.data;
@ -284,7 +284,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
this.hooksStatus = "";
this.display();
try {
const hooks = await this.withSettingsSession((client) => loadHookData(client, this.plugin.vaultPath));
const hooks = await this.withSettingsConnection((client) => loadHookData(client, this.plugin.vaultPath));
this.hooks = hooks.hooks;
this.hookWarnings = hooks.warnings;
this.hookErrors = hooks.errors;
@ -304,7 +304,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
this.hooksStatus = "";
this.display();
try {
await this.withSettingsSession((client) => client.trustHook(hook));
await this.withSettingsConnection((client) => client.trustHook(hook));
this.hooksStatus = "Trusted hook definition.";
await this.loadHooks();
} catch (error) {
@ -320,7 +320,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
this.hooksStatus = "";
this.display();
try {
await this.withSettingsSession((client) => client.setHookEnabled(hook, enabled));
await this.withSettingsConnection((client) => client.setHookEnabled(hook, enabled));
this.hooksStatus = enabled ? "Enabled hook." : "Disabled hook.";
await this.loadHooks();
} catch (error) {
@ -357,7 +357,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
this.archivedThreadsStatus = "";
this.display();
try {
const response = await this.withSettingsSession((client) => client.unarchiveThread(threadId));
const response = await this.withSettingsConnection((client) => client.unarchiveThread(threadId));
this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId);
this.archivedThreadsLoaded = true;
this.archivedThreadsStatus = `Restored "${archivedThreadDisplayTitle(response.thread)}".`;
@ -371,8 +371,8 @@ export class CodexPanelSettingTab extends PluginSettingTab {
}
}
private async withSettingsSession<T>(operation: (client: AppServerClient) => Promise<T>): Promise<T> {
return withAppServerSession(this.plugin.settings.codexPath, this.plugin.vaultPath, operation);
private async withSettingsConnection<T>(operation: (client: AppServerClient) => Promise<T>): Promise<T> {
return withAppServerConnection(this.plugin.settings.codexPath, this.plugin.vaultPath, operation);
}
private modelOptions(): Model[] {

View file

@ -1,14 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import type { AppServerClient } from "../../../src/app-server/client";
import { ChatSessionController } from "../../../src/features/chat/chat-session-controller";
import { ChatAppServerController } from "../../../src/features/chat/chat-app-server-controller";
import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state";
import type { Model } from "../../../src/generated/app-server/v2/Model";
import type { RateLimitSnapshot } from "../../../src/generated/app-server/v2/RateLimitSnapshot";
import type { SkillMetadata } from "../../../src/generated/app-server/v2/SkillMetadata";
describe("ChatSessionController", () => {
it("reuses cached session metadata for deferred diagnostics", async () => {
describe("ChatAppServerController", () => {
it("reuses cached app-server metadata for deferred diagnostics", async () => {
const state = createChatState();
const stateStore = createChatStateStore(state);
@ -27,7 +27,7 @@ describe("ChatSessionController", () => {
readModelProviderCapabilities: vi.fn().mockResolvedValue({}),
} as unknown as AppServerClient;
const controller = new ChatSessionController({
const controller = new ChatAppServerController({
stateStore,
vaultPath: "/vault",
currentClient: () => client,
@ -35,12 +35,12 @@ describe("ChatSessionController", () => {
forceMessagesToBottom: () => undefined,
});
await controller.refreshSessionMetadata();
await controller.refreshAppServerMetadata();
listModels.mockClear();
listSkills.mockClear();
readAccountRateLimits.mockClear();
await controller.refreshCapabilityDiagnostics({ cachedSessionMetadata: true });
await controller.refreshCapabilityDiagnostics({ cachedAppServerMetadata: true });
expect(listModels).not.toHaveBeenCalled();
expect(listSkills).not.toHaveBeenCalled();

View file

@ -24,7 +24,7 @@ function controllerForState(
return new ChatController(testStoreForState(state), {
refreshThreads: vi.fn(),
refreshSkills: vi.fn(),
publishSessionMetadata: vi.fn(),
publishAppServerMetadata: vi.fn(),
maybeNameThread: vi.fn(),
notifyThreadArchived: vi.fn(),
notifyThreadRenamed: vi.fn(),
@ -601,8 +601,8 @@ describe("ChatController", () => {
it("stores account rate limit updates outside thread scope", () => {
const state = createChatState();
const publishSessionMetadata = vi.fn();
const controller = controllerForState(state, { publishSessionMetadata });
const publishAppServerMetadata = vi.fn();
const controller = controllerForState(state, { publishAppServerMetadata });
controller.handleNotification({
method: "account/rateLimits/updated",
@ -623,14 +623,14 @@ describe("ChatController", () => {
limitId: "codex",
primary: { usedPercent: 64 },
});
expect(publishSessionMetadata).toHaveBeenCalledOnce();
expect(publishAppServerMetadata).toHaveBeenCalledOnce();
});
it("records MCP startup status for diagnostics without a chat system message", () => {
const state = createChatState();
const recordMcpStartupStatus = vi.fn();
const publishSessionMetadata = vi.fn();
const controller = controllerForState(state, { recordMcpStartupStatus, publishSessionMetadata });
const publishAppServerMetadata = vi.fn();
const controller = controllerForState(state, { recordMcpStartupStatus, publishAppServerMetadata });
controller.handleNotification({
method: "mcpServer/startupStatus/updated",
@ -642,7 +642,7 @@ describe("ChatController", () => {
} satisfies Extract<ServerNotification, { method: "mcpServer/startupStatus/updated" }>);
expect(recordMcpStartupStatus).toHaveBeenCalledWith("github", "failed", "missing token");
expect(publishSessionMetadata).toHaveBeenCalledOnce();
expect(publishAppServerMetadata).toHaveBeenCalledOnce();
expect(state.displayItems).toEqual([]);
});
});

View file

@ -103,16 +103,16 @@ describe("CodexChatView connection lifecycle", () => {
expect((view as unknown as { state: { listedThreads: unknown[] } }).state.listedThreads).toEqual(threads);
});
it("publishes session metadata after connecting", async () => {
const publishSessionMetadata = vi.fn();
it("publishes app-server metadata after connecting", async () => {
const publishAppServerMetadata = vi.fn();
connectionMock.state.client = connectedClient();
const view = await chatView({
host: chatHost({ publishSessionMetadata }),
host: chatHost({ publishAppServerMetadata }),
});
await view.connect();
expect(publishSessionMetadata).toHaveBeenCalledWith(
expect(publishAppServerMetadata).toHaveBeenCalledWith(
expect.objectContaining({
effectiveConfig: {},
availableModels: [],
@ -205,7 +205,7 @@ describe("CodexChatView connection lifecycle", () => {
expect(client.threadTurnsList).toHaveBeenCalledWith("thread-1", null, 20);
});
it("warms session metadata for an empty restored panel after the shell is open", async () => {
it("warms app-server metadata for an empty restored panel after the shell is open", async () => {
vi.useFakeTimers();
const client = connectedClient({
listThreads: vi.fn().mockResolvedValue({ data: [threadFixture("thread-1")] }),
@ -232,7 +232,7 @@ describe("CodexChatView connection lifecycle", () => {
const view = await chatView({
host: chatHost({
cachedThreadList: vi.fn(() => [cachedThread] as never[]),
cachedSessionMetadata: vi.fn(
cachedAppServerMetadata: vi.fn(
() =>
({
effectiveConfig: { config: { model: "gpt-cached" }, origins: {}, layers: [] },
@ -882,8 +882,8 @@ function chatHost(overrides: Partial<CodexChatHost> = {}): CodexChatHost {
(fetchThreads: () => Promise<unknown>) => fetchThreads() as Promise<never[]>,
) as CodexChatHost["refreshThreadList"],
cachedThreadList: vi.fn(() => null),
publishSessionMetadata: vi.fn(),
cachedSessionMetadata: vi.fn(() => null),
publishAppServerMetadata: vi.fn(),
cachedAppServerMetadata: vi.fn(() => null),
...overrides,
};
}

View file

@ -412,8 +412,8 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) {
refreshThreadsViewLiveState: vi.fn(),
refreshThreadList: vi.fn((fetchThreads: () => Promise<unknown>) => fetchThreads() as Promise<never[]>),
cachedThreadList: vi.fn(() => null),
publishSessionMetadata: vi.fn(),
cachedSessionMetadata: vi.fn(() => null),
publishAppServerMetadata: vi.fn(),
cachedAppServerMetadata: vi.fn(() => null),
},
);
}

View file

@ -15,12 +15,12 @@ const { JSDOM } = require("jsdom") as {
JSDOM: new (html: string) => { window: Window & typeof globalThis };
};
const { withAppServerSessionMock } = vi.hoisted(() => ({
withAppServerSessionMock: vi.fn(),
const { withAppServerConnectionMock } = vi.hoisted(() => ({
withAppServerConnectionMock: vi.fn(),
}));
vi.mock("../../src/app-server/session-client", () => ({
withAppServerSession: withAppServerSessionMock,
vi.mock("../../src/app-server/connection-client", () => ({
withAppServerConnection: withAppServerConnectionMock,
}));
describe("settings tab", () => {
@ -33,7 +33,7 @@ describe("settings tab", () => {
vi.stubGlobal("HTMLInputElement", dom.window.HTMLInputElement);
vi.stubGlobal("HTMLSelectElement", dom.window.HTMLSelectElement);
vi.stubGlobal("Event", dom.window.Event);
withAppServerSessionMock.mockReset();
withAppServerConnectionMock.mockReset();
notices.length = 0;
});
@ -68,7 +68,7 @@ describe("settings tab", () => {
it("auto-loads settings data once and keeps one global refresh button", async () => {
const client = settingsClient();
withAppServerSessionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
withAppServerConnectionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
operation(client),
);
const tab = newSettingsTab();
@ -76,7 +76,7 @@ describe("settings tab", () => {
tab.display();
await flushPromises();
expect(withAppServerSessionMock).toHaveBeenCalledTimes(1);
expect(withAppServerConnectionMock).toHaveBeenCalledTimes(1);
expect(client.listModels).toHaveBeenCalledTimes(1);
expect(client.listHooks).toHaveBeenCalledTimes(1);
expect(client.listThreads).toHaveBeenCalledWith("/vault", true);
@ -84,7 +84,7 @@ describe("settings tab", () => {
tab.display();
await flushPromises();
expect(withAppServerSessionMock).toHaveBeenCalledTimes(1);
expect(withAppServerConnectionMock).toHaveBeenCalledTimes(1);
expect(buttonTexts(tab)).toContain("Refresh Codex data");
expect(buttonTexts(tab)).not.toContain("Load models");
expect(buttonTexts(tab)).not.toContain("Load hooks");
@ -154,7 +154,7 @@ describe("settings tab", () => {
models: [model("gpt-5.5")],
threads: [thread({ id: "thread-new", preview: "New" })],
});
withAppServerSessionMock
withAppServerConnectionMock
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
operation(firstClient),
)
@ -168,7 +168,7 @@ describe("settings tab", () => {
clickButton(tab, "Refresh Codex data");
await flushPromises();
expect(withAppServerSessionMock).toHaveBeenCalledTimes(2);
expect(withAppServerConnectionMock).toHaveBeenCalledTimes(2);
expect(tab.containerEl.textContent).toContain("gpt-5.5");
expect(tab.containerEl.textContent).toContain("New");
expect(tab.containerEl.textContent).not.toContain("Old");
@ -177,7 +177,7 @@ describe("settings tab", () => {
it("uses cached models initially and publishes refreshed models", async () => {
const publishModels = vi.fn();
const client = settingsClient({ models: [model("gpt-5.5")] });
withAppServerSessionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
withAppServerConnectionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
operation(client),
);
const tab = newSettingsTab({ cachedModels: [model("gpt-cached")], publishModels });
@ -198,7 +198,7 @@ describe("settings tab", () => {
hooksError: new Error("hooks unavailable"),
threads: [thread({ preview: "Archived thread" })],
});
withAppServerSessionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
withAppServerConnectionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
operation(client),
);
const tab = newSettingsTab();
@ -217,7 +217,7 @@ describe("settings tab", () => {
hooks: [hook({ key: "hook-1", command: "node hook.js", currentHash: "abc123", trustStatus: "untrusted" })],
threads: [thread({ id: "thread-archived", preview: "Archived thread" })],
});
withAppServerSessionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
withAppServerConnectionMock.mockImplementation((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
operation(client),
);
const tab = newSettingsTab();