mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
refactor(runtime): simplify disposable runtime boundaries
This commit is contained in:
parent
1f5aa50862
commit
ffcc814ba5
19 changed files with 153 additions and 118 deletions
|
|
@ -81,15 +81,12 @@ export class CodexExecutionRuntime implements AppServerClientAccess {
|
|||
});
|
||||
}
|
||||
|
||||
chatHost(): CodexChatHost {
|
||||
private chatHost(): CodexChatHost {
|
||||
this.assertActive();
|
||||
return {
|
||||
appServerClientAccess: this,
|
||||
appServerContext: this.context,
|
||||
settingsRef: {
|
||||
settings: this.chatSettings(),
|
||||
vaultPath: this.context.vaultPath,
|
||||
},
|
||||
settings: this.chatSettings(),
|
||||
workspace: this.options.workspace,
|
||||
appServerQueries: this.resourceStore,
|
||||
threadCatalog: this.threadCatalog,
|
||||
|
|
@ -100,7 +97,7 @@ export class CodexExecutionRuntime implements AppServerClientAccess {
|
|||
};
|
||||
}
|
||||
|
||||
threadsHost(): ThreadsViewHost {
|
||||
private threadsHost(): ThreadsViewHost {
|
||||
this.assertActive();
|
||||
return {
|
||||
settings: this.threadsSettings(),
|
||||
|
|
@ -362,21 +359,15 @@ export class CodexExecutionRuntime implements AppServerClientAccess {
|
|||
archiveExportFilenameTemplate: this.options.settings().archiveExportFilenameTemplate,
|
||||
archiveExportTags: this.options.settings().archiveExportTags,
|
||||
}),
|
||||
codexPath: () => this.context.codexPath,
|
||||
scrollThreadFromComposerEdges: () => this.options.settings().scrollThreadFromComposerEdges,
|
||||
sendShortcut: () => this.options.settings().sendShortcut,
|
||||
showToolbar: () => this.options.settings().showToolbar,
|
||||
threadNamingEffort: () => this.options.settings().threadNamingEffort,
|
||||
threadNamingModel: () => this.options.settings().threadNamingModel,
|
||||
};
|
||||
}
|
||||
|
||||
private threadsSettings(): ThreadsViewSettingsAccess {
|
||||
return {
|
||||
archiveExportEnabled: () => this.options.settings().archiveExportEnabled,
|
||||
codexPath: () => this.context.codexPath,
|
||||
threadNamingModel: () => this.options.settings().threadNamingModel,
|
||||
threadNamingEffort: () => this.options.settings().threadNamingEffort,
|
||||
archiveExportSettings: () => ({
|
||||
archiveExportFolderTemplate: this.options.settings().archiveExportFolderTemplate,
|
||||
archiveExportFilenameTemplate: this.options.settings().archiveExportFilenameTemplate,
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ export function createChatComposerController(
|
|||
contextReferenceProvider: new VaultComposerContextReferenceProvider(environment.obsidian.app),
|
||||
attachmentHandler: createVaultComposerAttachmentHandler({
|
||||
app: environment.obsidian.app,
|
||||
attachmentFolder: () => environment.plugin.settingsRef.settings.attachmentFolder(),
|
||||
attachmentFolder: () => environment.plugin.settings.attachmentFolder(),
|
||||
}),
|
||||
sourcePath: () => environment.obsidian.app.workspace.getActiveFile()?.path ?? "",
|
||||
stateStore,
|
||||
viewId: environment.obsidian.viewId,
|
||||
referenceActiveNoteOnSend: () => environment.plugin.settingsRef.settings.referenceActiveNoteOnSend(),
|
||||
sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut(),
|
||||
scrollThreadFromComposerEdges: () => environment.plugin.settingsRef.settings.scrollThreadFromComposerEdges(),
|
||||
referenceActiveNoteOnSend: () => environment.plugin.settings.referenceActiveNoteOnSend(),
|
||||
sendShortcut: () => environment.plugin.settings.sendShortcut(),
|
||||
scrollThreadFromComposerEdges: () => environment.plugin.settings.scrollThreadFromComposerEdges(),
|
||||
canInterrupt: (model) => {
|
||||
return model.turnBusy && Boolean(model.activeThreadId && model.activeTurnId);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export function createConnectionBundle(
|
|||
},
|
||||
setStatus: status.set,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
configuredCommand: () => environment.plugin.settingsRef.settings.codexPath(),
|
||||
configuredCommand: () => environment.plugin.appServerContext.codexPath,
|
||||
isStaleConnectionError: (error) => error instanceof StaleConnectionError,
|
||||
isStaleResourceContextError: isStaleAppServerResourceContextError,
|
||||
notifyConnectionFailed: () => {
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ export function createRuntimeBundle(
|
|||
projection: createChatPanelRuntimeProjection({
|
||||
state: () => host.stateStore.getState(),
|
||||
connected: () => input.connection.isConnected(),
|
||||
configuredCommand: () => host.environment.plugin.settingsRef.settings.codexPath(),
|
||||
vaultPath: () => host.environment.plugin.settingsRef.vaultPath,
|
||||
configuredCommand: () => host.environment.plugin.appServerContext.codexPath,
|
||||
vaultPath: () => host.environment.plugin.appServerContext.vaultPath,
|
||||
nowMs: () => Date.now(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -89,13 +89,13 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
|
|||
nowMs: () => Date.now(),
|
||||
},
|
||||
settings: {
|
||||
vaultPath: () => environment.plugin.settingsRef.vaultPath,
|
||||
configuredCommand: () => environment.plugin.settingsRef.settings.codexPath(),
|
||||
archiveExportEnabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled(),
|
||||
vaultPath: () => environment.plugin.appServerContext.vaultPath,
|
||||
configuredCommand: () => environment.plugin.appServerContext.codexPath,
|
||||
archiveExportEnabled: () => environment.plugin.settings.archiveExportEnabled(),
|
||||
},
|
||||
};
|
||||
const goalSurface: ChatPanelGoalSurface = {
|
||||
sendShortcut: () => environment.plugin.settingsRef.settings.sendShortcut(),
|
||||
sendShortcut: () => environment.plugin.settings.sendShortcut(),
|
||||
actions: goals,
|
||||
};
|
||||
const threadStreamContext = createChatThreadStreamSurfaceContext({
|
||||
|
|
@ -103,7 +103,7 @@ export function createShellBundle(host: ChatPanelShellBundleHost, input: ChatPan
|
|||
app: environment.obsidian.app,
|
||||
owner: environment.obsidian.owner,
|
||||
stateStore,
|
||||
vaultPath: environment.plugin.settingsRef.vaultPath,
|
||||
vaultPath: environment.plugin.appServerContext.vaultPath,
|
||||
loadOlderTurns: () => void history.loadOlder(),
|
||||
actions: {
|
||||
rollbackThread: (threadId) => void threadActions.rollbackThread(threadId),
|
||||
|
|
|
|||
|
|
@ -125,9 +125,9 @@ export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPan
|
|||
transport: threadOperationsTransport,
|
||||
nameMutations: environment.plugin.threadNameMutations,
|
||||
archiveExport: {
|
||||
settings: () => environment.plugin.settingsRef.settings.archiveExportSettings(),
|
||||
enabled: () => environment.plugin.settingsRef.settings.archiveExportEnabled(),
|
||||
vaultPath: environment.plugin.settingsRef.vaultPath,
|
||||
settings: () => environment.plugin.settings.archiveExportSettings(),
|
||||
enabled: () => environment.plugin.settings.archiveExportEnabled(),
|
||||
vaultPath: environment.plugin.appServerContext.vaultPath,
|
||||
vaultConfigDir: environment.obsidian.app.vault.configDir,
|
||||
},
|
||||
archiveDestination: environment.obsidian.archiveDestination,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { App, Component, EventRef } from "obsidian";
|
|||
import type { AppServerClientAccess } from "../../../app-server/connection/client-access";
|
||||
import type { AppServerQueryContext } from "../../../app-server/query/keys";
|
||||
import type { ObservedResultListener } from "../../../app-server/query/observed-result";
|
||||
import type { ModelMetadata, ReasoningEffort } from "../../../domain/catalog/metadata";
|
||||
import type { ModelMetadata } from "../../../domain/catalog/metadata";
|
||||
import type { SendShortcut } from "../../../domain/input/send-shortcut";
|
||||
import type { SharedServerMetadata, SharedServerMetadataResource } from "../../../domain/server/metadata";
|
||||
import type { ArchiveExportSettings } from "../../../domain/threads/archive-markdown";
|
||||
|
|
@ -18,7 +18,7 @@ import type { ThreadGoalOperationCoordinator } from "../application/threads/goal
|
|||
export interface CodexChatHost {
|
||||
readonly appServerClientAccess: AppServerClientAccess;
|
||||
readonly appServerContext: Readonly<AppServerQueryContext>;
|
||||
readonly settingsRef: ChatPanelSettingsRef;
|
||||
readonly settings: ChatPanelSettingsAccess;
|
||||
readonly workspace: WorkspacePanels;
|
||||
readonly appServerQueries: ChatAppServerQueries;
|
||||
readonly threadCatalog: ChatThreadCatalog;
|
||||
|
|
@ -28,22 +28,14 @@ export interface CodexChatHost {
|
|||
readonly runtimeSettingsCommitQueue: KeyedOperationQueue<string>;
|
||||
}
|
||||
|
||||
interface ChatPanelSettingsRef {
|
||||
readonly settings: ChatPanelSettingsAccess;
|
||||
readonly vaultPath: string;
|
||||
}
|
||||
|
||||
export interface ChatPanelSettingsAccess {
|
||||
referenceActiveNoteOnSend(): boolean;
|
||||
attachmentFolder(): string;
|
||||
archiveExportEnabled(): boolean;
|
||||
archiveExportSettings(): ArchiveExportSettings;
|
||||
codexPath(): string;
|
||||
scrollThreadFromComposerEdges(): boolean;
|
||||
sendShortcut(): SendShortcut;
|
||||
showToolbar(): boolean;
|
||||
threadNamingEffort(): ReasoningEffort | null;
|
||||
threadNamingModel(): string | null;
|
||||
}
|
||||
|
||||
export interface WorkspacePanels {
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
if (!root) return;
|
||||
renderChatPanelShell(root, {
|
||||
stateStore: this.stateStore,
|
||||
showToolbar: this.environment.plugin.settingsRef.settings.showToolbar(),
|
||||
showToolbar: this.environment.plugin.settings.showToolbar(),
|
||||
parts: this.runtime.shell.parts,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Notice } from "obsidian";
|
|||
import type { ObservedPaginatedResult } from "../../app-server/query/observed-result";
|
||||
import { observedInitialError, observedInitialLoading } from "../../app-server/query/observed-result";
|
||||
import { isStaleAppServerResourceContextError } from "../../app-server/query/resource-store";
|
||||
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import type { ArchiveExportSettings } from "../../domain/threads/archive-markdown";
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
import type { ThreadRenameLifecycleEvent } from "../../domain/threads/rename-lifecycle";
|
||||
|
|
@ -33,9 +32,6 @@ type ThreadsViewThreadCatalog = ThreadCatalogPaginatedActiveReader & ThreadCatal
|
|||
|
||||
export interface ThreadsViewSettingsAccess {
|
||||
archiveExportEnabled(): boolean;
|
||||
codexPath(): string;
|
||||
threadNamingModel(): string | null;
|
||||
threadNamingEffort(): ReasoningEffort | null;
|
||||
archiveExportSettings(): ArchiveExportSettings;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import type { App } from "obsidian";
|
||||
import type { AppServerClient } from "./app-server/connection/client";
|
||||
import type { AppServerClientAccessOptions } from "./app-server/connection/client-access";
|
||||
import { VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants";
|
||||
import { CodexExecutionRuntime } from "./execution-runtime";
|
||||
import type {
|
||||
|
|
@ -8,12 +6,10 @@ import type {
|
|||
ChatSharedThreadSurface,
|
||||
ChatViewLifecycleSurface,
|
||||
ChatViewRuntimeOwner,
|
||||
CodexChatHost,
|
||||
} from "./features/chat/host/contracts";
|
||||
import type { SelectionRewriteCommandController } from "./features/selection-rewrite/command.obsidian";
|
||||
import type { SelectionRewriteTransport } from "./features/selection-rewrite/transport";
|
||||
import type { ThreadCatalogEvent } from "./features/threads/catalog/thread-catalog";
|
||||
import type { ThreadsViewHost } from "./features/threads-view/session";
|
||||
import type { ThreadsViewPanelActivity } from "./features/threads-view/state";
|
||||
import { CodexThreadsView, type ThreadsRuntimeView, type ThreadsViewRuntimeOwner } from "./features/threads-view/view.obsidian";
|
||||
import { persistedTurnDiffViewState, type TurnDiffViewState } from "./features/turn-diff/model";
|
||||
|
|
@ -124,18 +120,6 @@ export class CodexPanelRuntime implements ChatViewRuntimeOwner, ThreadsViewRunti
|
|||
};
|
||||
}
|
||||
|
||||
withClient<T>(operation: (client: AppServerClient) => Promise<T>, options?: AppServerClientAccessOptions): Promise<T> {
|
||||
return this.currentExecutionRuntime().withClient(operation, options);
|
||||
}
|
||||
|
||||
chatHost(): CodexChatHost {
|
||||
return this.currentExecutionRuntime().chatHost();
|
||||
}
|
||||
|
||||
threadsHost(): ThreadsViewHost {
|
||||
return this.currentExecutionRuntime().threadsHost();
|
||||
}
|
||||
|
||||
settingTabHost(): CodexPanelSettingTabHost {
|
||||
return {
|
||||
settings: this.options.settingsRef.settings,
|
||||
|
|
@ -163,7 +147,10 @@ export class CodexPanelRuntime implements ChatViewRuntimeOwner, ThreadsViewRunti
|
|||
} else {
|
||||
Object.assign(this.options.settingsRef.settings, settings);
|
||||
}
|
||||
if (appServerContextReplaced || previousSettings.showToolbar !== settings.showToolbar) this.refreshOpenViews();
|
||||
if (previousSettings.showToolbar !== settings.showToolbar || previousSettings.archiveExportEnabled !== settings.archiveExportEnabled) {
|
||||
this.refreshChatViewSettings();
|
||||
}
|
||||
if (previousSettings.archiveExportEnabled !== settings.archiveExportEnabled) this.refreshThreadsViewSettings();
|
||||
return { replacementDynamicData };
|
||||
}
|
||||
|
||||
|
|
@ -178,11 +165,14 @@ export class CodexPanelRuntime implements ChatViewRuntimeOwner, ThreadsViewRunti
|
|||
await this.options.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
private refreshOpenViews(): void {
|
||||
private refreshChatViewSettings(): void {
|
||||
for (const view of this.panels.panelViews()) {
|
||||
const surface: ChatViewLifecycleSurface = view.surface;
|
||||
surface.refreshSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private refreshThreadsViewSettings(): void {
|
||||
for (const view of this.threadsViews()) view.refreshSettings();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,11 +82,7 @@ function connectionBundleFixture(overrides: { readServerDiagnostics?: ReturnType
|
|||
refreshActive: vi.fn().mockResolvedValue(undefined),
|
||||
apply: vi.fn(),
|
||||
},
|
||||
settingsRef: {
|
||||
settings: {
|
||||
codexPath: () => "codex",
|
||||
},
|
||||
},
|
||||
appServerContext: { codexPath: "codex", vaultPath: "/vault" },
|
||||
},
|
||||
},
|
||||
stateStore,
|
||||
|
|
|
|||
|
|
@ -217,7 +217,8 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
workspace?: Partial<ChatPanelEnvironment["plugin"]["workspace"]>;
|
||||
threadCatalog?: Partial<ChatPanelEnvironment["plugin"]["threadCatalog"]>;
|
||||
appServerQueries?: Partial<ChatPanelEnvironment["plugin"]["appServerQueries"]>;
|
||||
settingsRef?: Partial<ChatPanelEnvironment["plugin"]["settingsRef"]>;
|
||||
settings?: ChatPanelEnvironment["plugin"]["settings"];
|
||||
appServerContext?: ChatPanelEnvironment["plugin"]["appServerContext"];
|
||||
};
|
||||
view?: Partial<ChatPanelEnvironment["view"]>;
|
||||
}
|
||||
|
|
@ -225,7 +226,6 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
function chatPanelEnvironmentFixture(overrides: PartialChatPanelEnvironment = {}): ChatPanelEnvironment {
|
||||
const threadCatalog = threadCatalogFixture(overrides.plugin?.threadCatalog);
|
||||
const appServerQueries = appServerQueriesFixture(overrides.plugin?.appServerQueries);
|
||||
const settingsRef = overrides.plugin?.settingsRef;
|
||||
const settingsSource: CodexPanelSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
codexPath: "codex",
|
||||
|
|
@ -269,15 +269,12 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
appServerClientAccess: {
|
||||
withClient: vi.fn(() => Promise.reject(new Error("Unexpected fallback app-server client request."))),
|
||||
},
|
||||
appServerContext: { codexPath: "codex", vaultPath: "/vault" },
|
||||
appServerContext: overrides.plugin?.appServerContext ?? { codexPath: "codex", vaultPath: "/vault" },
|
||||
threadTitleTransport: {
|
||||
persistedContext: vi.fn().mockResolvedValue(null),
|
||||
generateTitle: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
settingsRef: {
|
||||
settings: settingsRef?.settings ?? chatPanelSettingsAccess(settingsSource),
|
||||
vaultPath: settingsRef?.vaultPath ?? "/vault",
|
||||
},
|
||||
settings: overrides.plugin?.settings ?? chatPanelSettingsAccess(settingsSource),
|
||||
workspace: {
|
||||
openThreadInNewView: vi.fn().mockResolvedValue(undefined),
|
||||
threadHasPendingOrRunningPanel: vi.fn(() => false),
|
||||
|
|
|
|||
|
|
@ -564,10 +564,7 @@ export function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexCha
|
|||
},
|
||||
threadGoalOperations: createThreadGoalOperationCoordinator(),
|
||||
runtimeSettingsCommitQueue: createKeyedOperationQueue(),
|
||||
settingsRef: {
|
||||
settings: chatPanelSettingsAccess(settings),
|
||||
vaultPath,
|
||||
},
|
||||
settings: chatPanelSettingsAccess(settings),
|
||||
workspace: {
|
||||
openThreadInNewView: overrides.openThreadInNewView ?? vi.fn(),
|
||||
openThreadFromPanel: overrides.openThreadFromPanel ?? vi.fn(),
|
||||
|
|
|
|||
|
|
@ -11,11 +11,8 @@ export function chatPanelSettingsAccess(settings: CodexPanelSettings): ChatPanel
|
|||
archiveExportFilenameTemplate: settings.archiveExportFilenameTemplate,
|
||||
archiveExportTags: settings.archiveExportTags,
|
||||
}),
|
||||
codexPath: () => settings.codexPath,
|
||||
scrollThreadFromComposerEdges: () => settings.scrollThreadFromComposerEdges,
|
||||
sendShortcut: () => settings.sendShortcut,
|
||||
showToolbar: () => settings.showToolbar,
|
||||
threadNamingEffort: () => settings.threadNamingEffort,
|
||||
threadNamingModel: () => settings.threadNamingModel,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,9 +649,6 @@ function threadsHost(overrides: Record<string, unknown> = {}) {
|
|||
return {
|
||||
settings: {
|
||||
archiveExportEnabled: () => DEFAULT_SETTINGS.archiveExportEnabled,
|
||||
codexPath: () => "codex",
|
||||
threadNamingModel: () => DEFAULT_SETTINGS.threadNamingModel,
|
||||
threadNamingEffort: () => DEFAULT_SETTINGS.threadNamingEffort,
|
||||
archiveExportSettings: () => ({
|
||||
archiveExportFolderTemplate: DEFAULT_SETTINGS.archiveExportFolderTemplate,
|
||||
archiveExportFilenameTemplate: DEFAULT_SETTINGS.archiveExportFilenameTemplate,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
|
||||
import { VIEW_TYPE_CODEX_PANEL } from "../src/constants";
|
||||
import type { Thread } from "../src/domain/threads/model";
|
||||
import type { ChatRuntimeView, CodexChatHost } from "../src/features/chat/host/contracts";
|
||||
import type { CodexChatView } from "../src/features/chat/host/view.obsidian";
|
||||
import type CodexPanelPlugin from "../src/main";
|
||||
import { deferred } from "./support/async";
|
||||
|
|
@ -30,7 +31,30 @@ vi.mock("../src/app-server/connection/short-lived-client", () => ({
|
|||
}));
|
||||
|
||||
function threadCatalog(plugin: CodexPanelPlugin) {
|
||||
return plugin.runtime.chatHost().threadCatalog;
|
||||
return currentChatHost(plugin).threadCatalog;
|
||||
}
|
||||
|
||||
const capturedChatHosts = new WeakMap<CodexPanelPlugin, { current: CodexChatHost | null }>();
|
||||
|
||||
function currentChatHost(plugin: CodexPanelPlugin): CodexChatHost {
|
||||
let capture = capturedChatHosts.get(plugin);
|
||||
if (!capture) {
|
||||
const createdCapture = { current: null as CodexChatHost | null };
|
||||
const view: ChatRuntimeView = {
|
||||
attachRuntime: (host) => {
|
||||
createdCapture.current = host;
|
||||
},
|
||||
activateRuntime: () => undefined,
|
||||
detachRuntime: () => {
|
||||
createdCapture.current = null;
|
||||
},
|
||||
};
|
||||
capturedChatHosts.set(plugin, createdCapture);
|
||||
capture = createdCapture;
|
||||
plugin.runtime.attachChatView(view);
|
||||
}
|
||||
if (!capture.current) throw new Error("Expected a captured chat runtime host.");
|
||||
return capture.current;
|
||||
}
|
||||
|
||||
describe("CodexPanelPlugin runtime integration", () => {
|
||||
|
|
@ -174,9 +198,12 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
const plugin = await pluginWithLeaves([]);
|
||||
plugin.settings.codexPath = "codex";
|
||||
|
||||
const result = await plugin.runtime.withClient((client) => client.request("config/read", { cwd: "/vault", includeLayers: true }), {
|
||||
serverRequests: { kind: "reject", message: "Settings refresh does not handle server requests." },
|
||||
});
|
||||
const result = await currentChatHost(plugin).appServerClientAccess.withClient(
|
||||
(client) => client.request("config/read", { cwd: "/vault", includeLayers: true }),
|
||||
{
|
||||
serverRequests: { kind: "reject", message: "Settings refresh does not handle server requests." },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(withShortLivedAppServerClientMock).toHaveBeenCalledWith(
|
||||
|
|
@ -197,7 +224,7 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
const plugin = await pluginWithLeaves([]);
|
||||
await publishCodexPath(plugin, "codex-a");
|
||||
|
||||
const operation = plugin.runtime.withClient(() => Promise.resolve("unused"), {
|
||||
const operation = currentChatHost(plugin).appServerClientAccess.withClient(() => Promise.resolve("unused"), {
|
||||
serverRequests: { kind: "reject", message: "test" },
|
||||
});
|
||||
await publishCodexPath(plugin, "codex-b");
|
||||
|
|
@ -212,7 +239,7 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
const plugin = await pluginWithLeaves([]);
|
||||
await publishCodexPath(plugin, "codex-a");
|
||||
|
||||
const operation = plugin.runtime.withClient(() => Promise.resolve("unused"), {
|
||||
const operation = currentChatHost(plugin).appServerClientAccess.withClient(() => Promise.resolve("unused"), {
|
||||
serverRequests: { kind: "reject", message: "test" },
|
||||
});
|
||||
await publishCodexPath(plugin, "codex-b");
|
||||
|
|
@ -220,7 +247,7 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
result.resolve("stale-a");
|
||||
|
||||
await expect(operation).rejects.toThrow("Codex app-server resource context changed while loading.");
|
||||
expect(plugin.runtime.chatHost().appServerContext).toEqual({ codexPath: "codex-a", vaultPath: "/vault" });
|
||||
expect(currentChatHost(plugin).appServerContext).toEqual({ codexPath: "codex-a", vaultPath: "/vault" });
|
||||
});
|
||||
|
||||
it("does not start a short-lived operation when its app-server context changes while connecting", async () => {
|
||||
|
|
@ -234,7 +261,7 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
await publishCodexPath(plugin, "codex-a");
|
||||
const callback = vi.fn(() => Promise.resolve("unused"));
|
||||
|
||||
const operation = plugin.runtime.withClient(callback, {
|
||||
const operation = currentChatHost(plugin).appServerClientAccess.withClient(callback, {
|
||||
serverRequests: { kind: "reject", message: "test" },
|
||||
});
|
||||
await publishCodexPath(plugin, "codex-b");
|
||||
|
|
@ -247,7 +274,7 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
|
||||
it("publishes a persisted context and its replacement runtime atomically", async () => {
|
||||
const plugin = await pluginWithLeaves([]);
|
||||
const firstContext = plugin.runtime.chatHost().appServerContext;
|
||||
const firstContext = currentChatHost(plugin).appServerContext;
|
||||
const save = deferred<void>();
|
||||
const saveSettings = vi.spyOn(plugin, "saveSettings").mockReturnValue(save.promise);
|
||||
|
||||
|
|
@ -256,14 +283,14 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
|
||||
expect(saveSettings).toHaveBeenCalledWith(expect.objectContaining({ codexPath: "codex-next" }));
|
||||
expect(plugin.settings.codexPath).toBe(firstContext.codexPath);
|
||||
expect(plugin.runtime.chatHost().appServerContext).toBe(firstContext);
|
||||
expect(currentChatHost(plugin).appServerContext).toBe(firstContext);
|
||||
|
||||
save.resolve(undefined);
|
||||
await publication;
|
||||
|
||||
expect(plugin.settings.codexPath).toBe("codex-next");
|
||||
expect(plugin.runtime.chatHost().appServerContext).toEqual({ codexPath: "codex-next", vaultPath: "/vault" });
|
||||
expect(plugin.runtime.chatHost().appServerContext).not.toBe(firstContext);
|
||||
expect(currentChatHost(plugin).appServerContext).toEqual({ codexPath: "codex-next", vaultPath: "/vault" });
|
||||
expect(currentChatHost(plugin).appServerContext).not.toBe(firstContext);
|
||||
});
|
||||
|
||||
it("detaches and reattaches every open Chat view to the new execution runtime", async () => {
|
||||
|
|
@ -280,8 +307,8 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
expect(detachRuntime).toHaveBeenCalledOnce();
|
||||
expect(attachRuntime).toHaveBeenCalledOnce();
|
||||
expect(activateRuntime).toHaveBeenCalledOnce();
|
||||
expect(attachRuntime.mock.calls[0]?.[0].settingsRef.settings.codexPath()).toBe("codex-next");
|
||||
expect(attachRuntime.mock.calls[0]?.[0].settingsRef.vaultPath).toBe("/vault");
|
||||
expect(attachRuntime.mock.calls[0]?.[0].appServerContext.codexPath).toBe("codex-next");
|
||||
expect(attachRuntime.mock.calls[0]?.[0].appServerContext.vaultPath).toBe("/vault");
|
||||
});
|
||||
|
||||
it("detaches and reattaches every open Threads view to the new execution runtime", async () => {
|
||||
|
|
@ -299,7 +326,6 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
expect(attachRuntime).toHaveBeenCalledOnce();
|
||||
expect(activateRuntime).toHaveBeenCalledOnce();
|
||||
expect(attachRuntime.mock.calls[0]?.[0].vaultPath).toBe("/vault");
|
||||
expect(attachRuntime.mock.calls[0]?.[0].settings.codexPath()).toBe("codex-next");
|
||||
});
|
||||
|
||||
it("constructs every replacement session before activating any of them", async () => {
|
||||
|
|
@ -366,7 +392,7 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
expect(broken.detachRuntime).toHaveBeenCalledOnce();
|
||||
expect(healthy.detachRuntime).toHaveBeenCalledOnce();
|
||||
expect(healthy.attachRuntime).toHaveBeenCalledOnce();
|
||||
expect(plugin.runtime.chatHost().appServerContext.codexPath).toBe("codex-next");
|
||||
expect(currentChatHost(plugin).appServerContext.codexPath).toBe("codex-next");
|
||||
});
|
||||
|
||||
it("keeps activating healthy views when one replacement attachment fails", async () => {
|
||||
|
|
@ -394,7 +420,7 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
expect(broken.detachRuntime).toHaveBeenCalledTimes(2);
|
||||
expect(broken.activateRuntime).toHaveBeenCalledOnce();
|
||||
expect(healthy.activateRuntime).toHaveBeenCalledOnce();
|
||||
expect(plugin.runtime.chatHost().appServerContext.codexPath).toBe("codex-next");
|
||||
expect(currentChatHost(plugin).appServerContext.codexPath).toBe("codex-next");
|
||||
});
|
||||
|
||||
it("cancels selection rewrites before publishing a new app-server context", async () => {
|
||||
|
|
@ -406,4 +432,54 @@ describe("CodexPanelPlugin runtime integration", () => {
|
|||
|
||||
expect(closeAll).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("refreshes only chat settings when toolbar visibility changes", async () => {
|
||||
const { CodexChatView } = await import("../src/features/chat/host/view.obsidian");
|
||||
const chatLeaf = leaf();
|
||||
chatLeaf.view = chatView(CodexChatView, chatLeaf);
|
||||
const refreshChat = vi.spyOn((chatLeaf.view as CodexChatView).surface, "refreshSettings");
|
||||
const plugin = await pluginWithLeaves([chatLeaf]);
|
||||
|
||||
await plugin.runtime.settingTabHost().publishSettings({ ...plugin.settings, showToolbar: !plugin.settings.showToolbar });
|
||||
|
||||
expect(refreshChat).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("refreshes chat and Threads settings when the archive default changes", async () => {
|
||||
const { CodexChatView } = await import("../src/features/chat/host/view.obsidian");
|
||||
const { CodexThreadsView } = await import("../src/features/threads-view/view.obsidian");
|
||||
const chatLeaf = leaf();
|
||||
chatLeaf.view = chatView(CodexChatView, chatLeaf);
|
||||
const refreshChat = vi.spyOn((chatLeaf.view as CodexChatView).surface, "refreshSettings");
|
||||
const threadsView = Object.create(CodexThreadsView.prototype) as InstanceType<typeof CodexThreadsView>;
|
||||
const refreshThreads = vi.spyOn(threadsView, "refreshSettings");
|
||||
const threadsLeaf = leaf();
|
||||
threadsLeaf.view = threadsView;
|
||||
const plugin = await pluginWithLeaves([chatLeaf], { threadsLeaves: [threadsLeaf] });
|
||||
|
||||
await plugin.runtime
|
||||
.settingTabHost()
|
||||
.publishSettings({ ...plugin.settings, archiveExportEnabled: !plugin.settings.archiveExportEnabled });
|
||||
|
||||
expect(refreshChat).toHaveBeenCalledOnce();
|
||||
expect(refreshThreads).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not redundantly refresh workspace views after runtime replacement", async () => {
|
||||
const { CodexChatView } = await import("../src/features/chat/host/view.obsidian");
|
||||
const { CodexThreadsView } = await import("../src/features/threads-view/view.obsidian");
|
||||
const chatLeaf = leaf();
|
||||
chatLeaf.view = chatView(CodexChatView, chatLeaf);
|
||||
const refreshChat = vi.spyOn((chatLeaf.view as CodexChatView).surface, "refreshSettings");
|
||||
const threadsView = Object.create(CodexThreadsView.prototype) as InstanceType<typeof CodexThreadsView>;
|
||||
const refreshThreads = vi.spyOn(threadsView, "refreshSettings");
|
||||
const threadsLeaf = leaf();
|
||||
threadsLeaf.view = threadsView;
|
||||
const plugin = await pluginWithLeaves([chatLeaf], { threadsLeaves: [threadsLeaf] });
|
||||
|
||||
await publishCodexPath(plugin, "codex-next");
|
||||
|
||||
expect(refreshChat).not.toHaveBeenCalled();
|
||||
expect(refreshThreads).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ describe("settings tab", () => {
|
|||
|
||||
it("routes every declarative control through the existing settings publication boundary", async () => {
|
||||
const saveSettings = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshOpenViews = vi.fn();
|
||||
const tab = newSettingsTab({ saveSettings, refreshOpenViews });
|
||||
const refreshChatViews = vi.fn();
|
||||
const refreshThreadsViews = vi.fn();
|
||||
const tab = newSettingsTab({ saveSettings, refreshChatViews, refreshThreadsViews });
|
||||
|
||||
const changes = [
|
||||
["showToolbar", false],
|
||||
|
|
@ -91,7 +92,8 @@ describe("settings tab", () => {
|
|||
}
|
||||
|
||||
expect(saveSettings).toHaveBeenCalledTimes(changes.length);
|
||||
expect(refreshOpenViews).toHaveBeenCalledOnce();
|
||||
expect(refreshChatViews).toHaveBeenCalledTimes(2);
|
||||
expect(refreshThreadsViews).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("ignores invalid declarative control values and rejects unknown keys", async () => {
|
||||
|
|
@ -290,8 +292,8 @@ describe("settings tab", () => {
|
|||
|
||||
it("saves the toolbar visibility setting and refreshes open panels", async () => {
|
||||
const saveSettings = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshOpenViews = vi.fn();
|
||||
const tab = newSettingsTab({ saveSettings, refreshOpenViews });
|
||||
const refreshChatViews = vi.fn();
|
||||
const tab = newSettingsTab({ saveSettings, refreshChatViews });
|
||||
|
||||
tab.display();
|
||||
const toggle = inputForSetting(tab, "Show chat toolbar");
|
||||
|
|
@ -301,15 +303,15 @@ describe("settings tab", () => {
|
|||
await flushPromises();
|
||||
|
||||
expect(saveSettings).toHaveBeenCalledOnce();
|
||||
expect(refreshOpenViews).toHaveBeenCalledOnce();
|
||||
expect(refreshChatViews).toHaveBeenCalledOnce();
|
||||
expect(settingDesc(tab, "Show chat toolbar")).toContain("toolbar above chat panels");
|
||||
});
|
||||
|
||||
it("finishes a pending settings save without remounting a hidden tab", async () => {
|
||||
const save = deferred<void>();
|
||||
const saveSettings = vi.fn(() => save.promise);
|
||||
const refreshOpenViews = vi.fn();
|
||||
const tab = newSettingsTab({ saveSettings, refreshOpenViews });
|
||||
const refreshChatViews = vi.fn();
|
||||
const tab = newSettingsTab({ saveSettings, refreshChatViews });
|
||||
|
||||
tab.display();
|
||||
const toggle = inputForSetting(tab, "Show chat toolbar");
|
||||
|
|
@ -325,7 +327,7 @@ describe("settings tab", () => {
|
|||
await flushPromises();
|
||||
|
||||
expect(saveSettings).toHaveBeenCalledOnce();
|
||||
expect(refreshOpenViews).toHaveBeenCalledOnce();
|
||||
expect(refreshChatViews).toHaveBeenCalledOnce();
|
||||
expect(tab.containerEl.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
|
@ -556,7 +558,7 @@ describe("settings tab", () => {
|
|||
|
||||
it("clears dynamic sections when the Codex executable changes", async () => {
|
||||
const saveSettings = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshOpenViews = vi.fn();
|
||||
const refreshChatViews = vi.fn();
|
||||
const oldClient = settingsClient({
|
||||
models: [model("gpt-old")],
|
||||
hooks: [hook({ key: "hook-old", command: "old hook", currentHash: "oldhash" })],
|
||||
|
|
@ -572,7 +574,7 @@ describe("settings tab", () => {
|
|||
.fn()
|
||||
.mockResolvedValueOnce([panelThread({ id: "thread-old", preview: "Old archived", archived: true })])
|
||||
.mockResolvedValueOnce([panelThread({ id: "thread-new", preview: "New archived", archived: true })]);
|
||||
const tab = newSettingsTab({ saveSettings, refreshOpenViews, fetchModels, refreshModels, refreshArchived });
|
||||
const tab = newSettingsTab({ saveSettings, refreshChatViews, fetchModels, refreshModels, refreshArchived });
|
||||
|
||||
tab.display();
|
||||
await flushPromises();
|
||||
|
|
@ -587,13 +589,13 @@ describe("settings tab", () => {
|
|||
await flushPromises();
|
||||
|
||||
expect(saveSettings).not.toHaveBeenCalled();
|
||||
expect(refreshOpenViews).not.toHaveBeenCalled();
|
||||
expect(refreshChatViews).not.toHaveBeenCalled();
|
||||
|
||||
codexInput.dispatchEvent(new FocusEvent("blur"));
|
||||
await flushPromises();
|
||||
|
||||
expect(saveSettings).toHaveBeenCalledOnce();
|
||||
expect(refreshOpenViews).toHaveBeenCalledOnce();
|
||||
expect(refreshChatViews).not.toHaveBeenCalled();
|
||||
expect(withShortLivedAppServerClientMock).toHaveBeenCalledTimes(1);
|
||||
expect(tab.containerEl.textContent).not.toContain("gpt-old");
|
||||
expect(tab.containerEl.textContent).not.toContain("Old archived");
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ export interface SettingsTabHostOptions {
|
|||
fetchModels?: () => Promise<readonly ModelMetadata[]>;
|
||||
refreshModels?: () => Promise<readonly ModelMetadata[]>;
|
||||
observeModels?: SettingsDynamicDataAccess["observeModelsResult"];
|
||||
refreshOpenViews?: () => void;
|
||||
refreshChatViews?: () => void;
|
||||
refreshThreadsViews?: () => void;
|
||||
archivedThreads?: Thread[];
|
||||
archivedSnapshot?: Thread[] | null;
|
||||
refreshArchived?: () => Promise<readonly Thread[]>;
|
||||
|
|
@ -261,7 +262,13 @@ export function settingsTabHost(options: SettingsTabHostOptions = {}): CodexPane
|
|||
if (appServerContextReplaced && !options.dynamicData) {
|
||||
dynamicData = createDynamicData();
|
||||
}
|
||||
if (appServerContextReplaced || previousSettings.showToolbar !== nextSettings.showToolbar) options.refreshOpenViews?.();
|
||||
if (
|
||||
previousSettings.showToolbar !== nextSettings.showToolbar ||
|
||||
previousSettings.archiveExportEnabled !== nextSettings.archiveExportEnabled
|
||||
) {
|
||||
options.refreshChatViews?.();
|
||||
}
|
||||
if (previousSettings.archiveExportEnabled !== nextSettings.archiveExportEnabled) options.refreshThreadsViews?.();
|
||||
return { replacementDynamicData: appServerContextReplaced ? dynamicData : null };
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -136,10 +136,7 @@ function chatHostFixture(): CodexChatHost {
|
|||
},
|
||||
threadGoalOperations: createThreadGoalOperationCoordinator(),
|
||||
runtimeSettingsCommitQueue: createKeyedOperationQueue(),
|
||||
settingsRef: {
|
||||
settings: chatPanelSettingsAccess(settings),
|
||||
vaultPath: "/vault",
|
||||
},
|
||||
settings: chatPanelSettingsAccess(settings),
|
||||
workspace: {
|
||||
openThreadInNewView: vi.fn(),
|
||||
openThreadFromPanel: vi.fn(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue