Remove plugin coordinator compatibility facade

This commit is contained in:
murashit 2026-06-07 16:03:00 +09:00
parent 2e76635430
commit 4a6f1a106b
6 changed files with 160 additions and 149 deletions

View file

@ -7,13 +7,11 @@ export interface CodexChatHost {
readonly settings: CodexPanelSettings;
readonly vaultPath: string;
openThreadInNewView(threadId: string): Promise<unknown>;
openThreadInAvailableView(threadId: string): Promise<void>;
focusThreadInOpenView(threadId: string): Promise<boolean>;
openTurnDiff(state: ChatTurnDiffViewState): Promise<void>;
notifyThreadArchived(threadId: string): void;
notifyThreadRenamed(threadId: string, name: string | null): void;
refreshThreadsViewLiveState(): void;
refreshOpenViews(): void;
refreshSharedThreadListFromOpenSurface(): void;
applyThreadListSnapshot(threads: readonly Thread[]): void;
refreshThreadList(fetchThreads: () => Promise<readonly Thread[]>): Promise<readonly Thread[]>;

View file

@ -3,16 +3,13 @@ import { Plugin } from "obsidian";
import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants";
import { registerSelectionRewriteCommand } from "./features/selection-rewrite/command";
import { CodexChatView } from "./features/chat/view";
import type { CodexChatHost } from "./features/chat/chat-host";
import { CodexChatTurnDiffView } from "./features/chat/chat-turn-diff-view";
import { openThreadPicker } from "./features/thread-picker/modal";
import type { OpenCodexPanelSnapshot } from "./runtime/open-panel-snapshot";
import { CodexThreadsView } from "./features/threads-view/view";
import type { Thread } from "./generated/app-server/v2/Thread";
import type { Model } from "./generated/app-server/v2/Model";
import type { SharedAppServerMetadata } from "./runtime/shared-app-server-state";
import { openThreadPicker, type ThreadPickerHost } from "./features/thread-picker/modal";
import { CodexThreadsView, type CodexThreadsHost } from "./features/threads-view/view";
import { SharedAppServerCache } from "./runtime/shared-app-server-cache";
import { DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormalizedData, type CodexPanelSettings } from "./settings/model";
import { CodexPanelSettingTab } from "./settings/tab";
import { CodexPanelSettingTab, type CodexPanelSettingTabHost } from "./settings/tab";
import { persistedChatTurnDiffViewState, type ChatTurnDiffViewState } from "./features/chat/ui/turn-diff";
import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator";
import { ThreadSurfaceCoordinator } from "./workspace/thread-surface-coordinator";
@ -24,14 +21,14 @@ export default class CodexPanelPlugin extends Plugin {
private readonly panels = new WorkspacePanelCoordinator({
app: this.app,
refreshThreadsViewLiveState: () => {
this.refreshThreadsViewLiveState();
this.threadSurfaces.refreshThreadsViewLiveState();
},
});
private readonly threadSurfaces = new ThreadSurfaceCoordinator({
app: this.app,
panels: this.panels,
refreshThreadSurfaces: () => {
this.refreshSharedThreadListFromOpenSurface();
this.threadSurfaces.refreshSharedThreadListFromOpenSurface();
},
});
@ -40,9 +37,9 @@ export default class CodexPanelPlugin extends Plugin {
this.vaultPath = getVaultPath(this.app);
await this.loadSettings();
this.registerView(VIEW_TYPE_CODEX_PANEL, (leaf) => new CodexChatView(leaf, this));
this.registerView(VIEW_TYPE_CODEX_PANEL, (leaf) => new CodexChatView(leaf, this.chatHost()));
this.registerView(VIEW_TYPE_CODEX_TURN_DIFF, (leaf) => new CodexChatTurnDiffView(leaf));
this.registerView(VIEW_TYPE_CODEX_THREADS, (leaf) => new CodexThreadsView(leaf, this));
this.registerView(VIEW_TYPE_CODEX_THREADS, (leaf) => new CodexThreadsView(leaf, this.threadsHost()));
this.registerEvent(
this.app.workspace.on("active-leaf-change", (leaf) => {
this.panels.recordLastFocusedPanel(leaf);
@ -50,19 +47,19 @@ export default class CodexPanelPlugin extends Plugin {
);
this.addRibbonIcon("bot-message-square", "Open panel", () => {
void this.activateView();
void this.panels.activateView();
});
this.addCommand({
id: "open-panel",
name: "Open panel",
callback: () => void this.activateView(),
callback: () => void this.panels.activateView(),
});
this.addCommand({
id: "open-new-panel",
name: "Open new panel",
callback: () => void this.activateNewView(),
callback: () => void this.panels.activateNewView(),
});
this.addCommand({
@ -74,21 +71,21 @@ export default class CodexPanelPlugin extends Plugin {
this.addCommand({
id: "open-thread",
name: "Open thread...",
callback: () => void openThreadPicker(this),
callback: () => void openThreadPicker(this.threadPickerHost()),
});
this.addCommand({
id: "new-chat",
name: "Start new chat",
callback: async () => {
const view = await this.activateView();
const view = await this.panels.activateView();
await view.startNewThread();
},
});
registerSelectionRewriteCommand(this);
this.addSettingTab(new CodexPanelSettingTab(this.app, this));
this.addSettingTab(new CodexPanelSettingTab(this.app, this, this.settingTabHost()));
this.panels.scheduleBootRestoredPanelLoads();
}
@ -97,39 +94,7 @@ export default class CodexPanelPlugin extends Plugin {
this.panels.cancelBootRestoredPanelLoads();
}
async activateView(): Promise<CodexChatView> {
return this.panels.activateView();
}
async activateNewView(options: { connect?: boolean } = {}): Promise<CodexChatView> {
return this.panels.activateNewView(options);
}
async openThreadInNewView(threadId: string): Promise<CodexChatView> {
return this.panels.openThreadInNewView(threadId);
}
async openNewPanel(): Promise<void> {
await this.panels.openNewPanel();
}
async openThreadInAvailableView(threadId: string): Promise<void> {
await this.panels.openThreadInAvailableView(threadId);
}
async openThreadInCurrentView(threadId: string): Promise<void> {
await this.panels.openThreadInCurrentView(threadId);
}
async focusThreadInOpenView(threadId: string): Promise<boolean> {
return this.panels.focusThreadInOpenView(threadId);
}
async openThreadInIdleEmptyView(threadId: string): Promise<boolean> {
return this.panels.openThreadInIdleEmptyView(threadId);
}
async activateThreadsView(): Promise<CodexThreadsView> {
private async activateThreadsView(): Promise<CodexThreadsView> {
const leaf = await this.app.workspace.ensureSideLeaf(VIEW_TYPE_CODEX_THREADS, "left", {
active: true,
reveal: true,
@ -139,7 +104,7 @@ export default class CodexPanelPlugin extends Plugin {
return view;
}
async openTurnDiff(state: ChatTurnDiffViewState): Promise<void> {
private async openTurnDiff(state: ChatTurnDiffViewState): Promise<void> {
const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_TURN_DIFF).at(0);
const leaf = existing ?? this.app.workspace.getLeaf("tab");
await leaf.setViewState({ type: VIEW_TYPE_CODEX_TURN_DIFF, active: true, state: { ...persistedChatTurnDiffViewState(state) } });
@ -150,67 +115,6 @@ export default class CodexPanelPlugin extends Plugin {
await this.app.workspace.revealLeaf(leaf);
}
refreshOpenViews(): void {
this.threadSurfaces.refreshOpenViews();
}
refreshSharedThreadListFromOpenSurface(): void {
this.threadSurfaces.refreshSharedThreadListFromOpenSurface();
}
refreshThreadList(fetchThreads: () => Promise<readonly Thread[]>): Promise<readonly Thread[]> {
return this.sharedAppServerCache.refreshThreadList(fetchThreads, (threads) => {
this.threadSurfaces.applyThreadListSnapshot(threads);
});
}
applyThreadListSnapshot(threads: readonly Thread[]): void {
this.sharedAppServerCache.applyThreadListSnapshot(threads);
this.threadSurfaces.applyThreadListSnapshot(threads);
}
cachedThreadList(): readonly Thread[] | null {
return this.sharedAppServerCache.cachedThreadList();
}
publishAppServerMetadata(metadata: SharedAppServerMetadata): void {
this.sharedAppServerCache.applyAppServerMetadataSnapshot(metadata);
this.threadSurfaces.publishAppServerMetadata(metadata);
}
publishModels(models: readonly Model[]): void {
this.sharedAppServerCache.applyModelsSnapshot(models);
this.threadSurfaces.publishModels(models);
}
cachedAppServerMetadata(): SharedAppServerMetadata | null {
return this.sharedAppServerCache.cachedAppServerMetadata();
}
cachedModels(): Model[] {
return this.sharedAppServerCache.cachedModels();
}
refreshThreadsViewLiveState(): void {
this.threadSurfaces.refreshThreadsViewLiveState();
}
notifyThreadArchived(threadId: string, options: { closeOpenPanels?: boolean } = {}): void {
this.threadSurfaces.notifyThreadArchived(threadId, options);
}
notifyThreadRenamed(threadId: string, name: string | null): void {
this.threadSurfaces.notifyThreadRenamed(threadId, name);
}
getOpenPanelSnapshots(): OpenCodexPanelSnapshot[] {
return this.panels.getOpenPanelSnapshots();
}
async focusOpenPanel(viewId: string, threadId: string | null = null): Promise<boolean> {
return this.panels.focusOpenPanel(viewId, threadId);
}
async loadSettings(): Promise<void> {
const data: unknown = await this.loadData();
this.settings = normalizeSettings(data);
@ -222,4 +126,95 @@ export default class CodexPanelPlugin extends Plugin {
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
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);
},
notifyThreadRenamed: (threadId, name) => {
this.threadSurfaces.notifyThreadRenamed(threadId, name);
},
refreshThreadsViewLiveState: () => {
this.threadSurfaces.refreshThreadsViewLiveState();
},
refreshSharedThreadListFromOpenSurface: () => {
this.threadSurfaces.refreshSharedThreadListFromOpenSurface();
},
applyThreadListSnapshot: (threads) => {
this.sharedAppServerCache.applyThreadListSnapshot(threads);
this.threadSurfaces.applyThreadListSnapshot(threads);
},
refreshThreadList: (fetchThreads) =>
this.sharedAppServerCache.refreshThreadList(fetchThreads, (threads) => {
this.threadSurfaces.applyThreadListSnapshot(threads);
}),
cachedThreadList: () => this.sharedAppServerCache.cachedThreadList(),
publishAppServerMetadata: (metadata) => {
this.sharedAppServerCache.applyAppServerMetadataSnapshot(metadata);
this.threadSurfaces.publishAppServerMetadata(metadata);
},
cachedAppServerMetadata: () => this.sharedAppServerCache.cachedAppServerMetadata(),
};
}
private threadsHost(): CodexThreadsHost {
return {
settings: this.settings,
vaultPath: this.vaultPath,
openNewPanel: () => this.panels.openNewPanel(),
openThreadInAvailableView: (threadId) => this.panels.openThreadInAvailableView(threadId),
getOpenPanelSnapshots: () => this.panels.getOpenPanelSnapshots(),
notifyThreadArchived: (threadId, options) => {
this.threadSurfaces.notifyThreadArchived(threadId, options);
},
notifyThreadRenamed: (threadId, name) => {
this.threadSurfaces.notifyThreadRenamed(threadId, name);
},
refreshThreadList: (fetchThreads) =>
this.sharedAppServerCache.refreshThreadList(fetchThreads, (threads) => {
this.threadSurfaces.applyThreadListSnapshot(threads);
}),
cachedThreadList: () => this.sharedAppServerCache.cachedThreadList(),
};
}
private threadPickerHost(): ThreadPickerHost {
return {
app: this.app,
settings: this.settings,
vaultPath: this.vaultPath,
cachedThreadList: () => this.sharedAppServerCache.cachedThreadList(),
refreshThreadList: (fetchThreads) =>
this.sharedAppServerCache.refreshThreadList(fetchThreads, (threads) => {
this.threadSurfaces.applyThreadListSnapshot(threads);
}),
openThreadInCurrentView: (threadId) => this.panels.openThreadInCurrentView(threadId),
openThreadInAvailableView: (threadId) => this.panels.openThreadInAvailableView(threadId),
};
}
private settingTabHost(): CodexPanelSettingTabHost {
return {
settings: this.settings,
vaultPath: this.vaultPath,
saveSettings: () => this.saveSettings(),
refreshOpenViews: () => {
this.threadSurfaces.refreshOpenViews();
},
refreshSharedThreadListFromOpenSurface: () => {
this.threadSurfaces.refreshSharedThreadListFromOpenSurface();
},
cachedModels: () => this.sharedAppServerCache.cachedModels(),
publishModels: (models) => {
this.sharedAppServerCache.applyModelsSnapshot(models);
this.threadSurfaces.publishModels(models);
},
};
}
}

View file

@ -48,9 +48,10 @@ export class CodexPanelSettingTab extends PluginSettingTab {
constructor(
app: App,
owner: Plugin,
private readonly plugin: CodexPanelSettingTabHost,
) {
super(app, plugin);
super(app, owner);
this.models = plugin.cachedModels();
}
@ -528,7 +529,7 @@ export class CodexPanelSettingTab extends PluginSettingTab {
}
}
export interface CodexPanelSettingTabHost extends Plugin {
export interface CodexPanelSettingTabHost {
settings: CodexPanelSettings;
vaultPath: string;
saveSettings(): Promise<void>;

View file

@ -1191,12 +1191,10 @@ function chatHost(overrides: Partial<CodexChatHost> = {}): CodexChatHost {
},
vaultPath: "/vault",
openThreadInNewView: vi.fn(),
openThreadInAvailableView: vi.fn().mockResolvedValue(undefined),
focusThreadInOpenView: vi.fn().mockResolvedValue(false),
openTurnDiff: vi.fn(),
notifyThreadArchived: vi.fn(),
notifyThreadRenamed: vi.fn(),
refreshOpenViews: vi.fn(),
refreshSharedThreadListFromOpenSurface: vi.fn(),
refreshThreadsViewLiveState: vi.fn(),
applyThreadListSnapshot: vi.fn(),

View file

@ -5,12 +5,28 @@ import { FileSystemAdapter } from "obsidian";
import { VIEW_TYPE_CODEX_PANEL } from "../src/constants";
import { DEFAULT_SETTINGS } from "../src/settings/model";
import type CodexPanelPlugin from "../src/main";
import type { CodexChatView } from "../src/features/chat/view";
import type { Thread } from "../src/generated/app-server/v2/Thread";
import type { SharedAppServerCache } from "../src/runtime/shared-app-server-cache";
import type { WorkspacePanelCoordinator } from "../src/workspace/panel-coordinator";
import type { ThreadSurfaceCoordinator } from "../src/workspace/thread-surface-coordinator";
import { installObsidianDomShims } from "./support/dom";
installObsidianDomShims();
function panels(plugin: CodexPanelPlugin): WorkspacePanelCoordinator {
return (plugin as unknown as { panels: WorkspacePanelCoordinator }).panels;
}
function threadSurfaces(plugin: CodexPanelPlugin): ThreadSurfaceCoordinator {
return (plugin as unknown as { threadSurfaces: ThreadSurfaceCoordinator }).threadSurfaces;
}
function sharedAppServerCache(plugin: CodexPanelPlugin): SharedAppServerCache {
return (plugin as unknown as { sharedAppServerCache: SharedAppServerCache }).sharedAppServerCache;
}
describe("CodexPanelPlugin boot restored panel loading", () => {
beforeEach(() => {
vi.useRealTimers();
@ -56,7 +72,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
restoredLeaf.view = chatView(CodexChatView, restoredLeaf);
});
await plugin.openThreadInAvailableView("thread-1");
await panels(plugin).openThreadInAvailableView("thread-1");
expect(restoredLeaf.loadIfDeferred).toHaveBeenCalledTimes(1);
expect(restoredLeaf.view).toBeInstanceOf(CodexChatView);
@ -84,7 +100,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const openEmptyThread = vi.spyOn(emptyView, "openThread").mockResolvedValue(undefined);
const plugin = await pluginWithLeaves([openLeaf, emptyLeaf]);
await plugin.openThreadInAvailableView("thread-1");
await panels(plugin).openThreadInAvailableView("thread-1");
expect((plugin.app.workspace.revealLeaf as ReturnType<typeof vi.fn>).mock.calls).toContainEqual([openLeaf]);
expect(openEmptyThread).not.toHaveBeenCalled();
@ -121,7 +137,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const openEmptyThread = vi.spyOn(emptyView, "openThread").mockResolvedValue(undefined);
const plugin = await pluginWithLeaves([busyLeaf, emptyLeaf]);
await plugin.openThreadInAvailableView("thread-1");
await panels(plugin).openThreadInAvailableView("thread-1");
expect(openEmptyThread).toHaveBeenCalledWith("thread-1");
});
@ -139,7 +155,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves([olderLeaf, currentLeaf]);
(plugin.app.workspace.getMostRecentLeaf as ReturnType<typeof vi.fn>).mockReturnValue(currentLeaf);
await plugin.openThreadInCurrentView("thread-1");
await panels(plugin).openThreadInCurrentView("thread-1");
expect(openCurrentThread).toHaveBeenCalledWith("thread-1");
expect(openOlderThread).not.toHaveBeenCalled();
@ -159,7 +175,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
(plugin.app.workspace.getActiveViewOfType as ReturnType<typeof vi.fn>).mockReturnValue(activeView);
(plugin.app.workspace.getMostRecentLeaf as ReturnType<typeof vi.fn>).mockReturnValue(fallbackLeaf);
await plugin.openThreadInCurrentView("thread-1");
await panels(plugin).openThreadInCurrentView("thread-1");
expect(openActiveThread).toHaveBeenCalledWith("thread-1");
expect(openFallbackThread).not.toHaveBeenCalled();
@ -179,7 +195,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves([openLeaf, currentLeaf]);
(plugin.app.workspace.getMostRecentLeaf as ReturnType<typeof vi.fn>).mockReturnValue(currentLeaf);
await plugin.openThreadInCurrentView("thread-1");
await panels(plugin).openThreadInCurrentView("thread-1");
expect(focusOpenThread).toHaveBeenCalledWith("thread-1");
expect(openCurrentThread).not.toHaveBeenCalled();
@ -197,7 +213,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
restoredLeaf.view = view;
});
await plugin.openThreadInCurrentView("selected-thread");
await panels(plugin).openThreadInCurrentView("selected-thread");
expect(restoredLeaf.loadIfDeferred).toHaveBeenCalledOnce();
expect(openThread).toHaveBeenCalledWith("selected-thread");
@ -217,7 +233,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const focusComposer = vi.spyOn(view, "focusComposer").mockImplementation(() => undefined);
const openThread = vi.spyOn(view, "openThread").mockResolvedValue(undefined);
await plugin.openThreadInNewView("thread-1");
await panels(plugin).openThreadInNewView("thread-1");
expect(connect).not.toHaveBeenCalled();
expect(focusComposer).toHaveBeenCalledOnce();
@ -239,7 +255,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves([firstLeaf, activeLeaf]);
(plugin.app.workspace.getActiveViewOfType as ReturnType<typeof vi.fn>).mockReturnValue(activeView);
await expect(plugin.activateView()).resolves.toBe(activeView);
await expect(panels(plugin).activateView()).resolves.toBe(activeView);
expect((plugin.app.workspace.revealLeaf as ReturnType<typeof vi.fn>).mock.calls).toContainEqual([activeLeaf]);
expect(connectActive).toHaveBeenCalledOnce();
@ -267,7 +283,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
if (name === "active-leaf-change") activeLeafHandlers.push(handler);
return {};
});
const refreshLiveState = vi.spyOn(plugin, "refreshThreadsViewLiveState").mockImplementation(() => undefined);
const refreshLiveState = vi.spyOn(threadSurfaces(plugin), "refreshThreadsViewLiveState").mockImplementation(() => undefined);
await plugin.onload();
const activeLeafHandler = activeLeafHandlers.at(0);
@ -275,7 +291,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
activeLeafHandler(secondLeaf);
expect(refreshLiveState).toHaveBeenCalledOnce();
expect(plugin.getOpenPanelSnapshots()).toMatchObject([
expect(panels(plugin).getOpenPanelSnapshots()).toMatchObject([
{ viewId: "first", lastFocused: false },
{ viewId: "second", lastFocused: true },
]);
@ -295,7 +311,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves([firstLeaf, activeLeaf]);
(plugin.app.workspace.getActiveViewOfType as ReturnType<typeof vi.fn>).mockReturnValue(activeView);
expect(plugin.getOpenPanelSnapshots()).toMatchObject([
expect(panels(plugin).getOpenPanelSnapshots()).toMatchObject([
{ viewId: "first", lastFocused: false },
{ viewId: "active", lastFocused: true },
]);
@ -316,7 +332,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves([recentLeaf, otherLeaf]);
(plugin.app.workspace.getMostRecentLeaf as ReturnType<typeof vi.fn>).mockReturnValue(recentLeaf);
expect(plugin.getOpenPanelSnapshots()).toMatchObject([
expect(panels(plugin).getOpenPanelSnapshots()).toMatchObject([
{ viewId: "recent", lastFocused: true },
{ viewId: "other", lastFocused: false },
]);
@ -335,7 +351,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const focusComposer = vi.spyOn(view, "focusComposer").mockImplementation(() => undefined);
const openThread = vi.spyOn(view, "openThread").mockResolvedValue(undefined);
await plugin.openNewPanel();
await panels(plugin).openNewPanel();
expect(connect).toHaveBeenCalledOnce();
expect(focusComposer).toHaveBeenCalledOnce();
@ -344,9 +360,11 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
it("refreshes shared thread lists after archive lifecycle notifications", async () => {
const plugin = await pluginWithLeaves([]);
const refreshSharedThreadList = vi.spyOn(plugin, "refreshSharedThreadListFromOpenSurface").mockImplementation(() => undefined);
const refreshSharedThreadList = vi
.spyOn(threadSurfaces(plugin), "refreshSharedThreadListFromOpenSurface")
.mockImplementation(() => undefined);
plugin.notifyThreadArchived("thread-1");
threadSurfaces(plugin).notifyThreadArchived("thread-1");
expect(refreshSharedThreadList).toHaveBeenCalledOnce();
});
@ -361,15 +379,15 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
otherLeaf.view = chatView(CodexChatView, otherLeaf);
vi.spyOn(otherLeaf.view as CodexChatView, "openPanelSnapshot").mockReturnValue(panelSnapshot({ threadId: "thread-2" }));
const plugin = await pluginWithLeaves([restoredMatchingLeaf, matchingLeaf, otherLeaf]);
vi.spyOn(plugin, "refreshSharedThreadListFromOpenSurface").mockImplementation(() => undefined);
vi.spyOn(threadSurfaces(plugin), "refreshSharedThreadListFromOpenSurface").mockImplementation(() => undefined);
plugin.notifyThreadArchived("thread-1");
threadSurfaces(plugin).notifyThreadArchived("thread-1");
expect(restoredMatchingLeaf.detach).not.toHaveBeenCalled();
expect(matchingLeaf.detach).not.toHaveBeenCalled();
expect(otherLeaf.detach).not.toHaveBeenCalled();
plugin.notifyThreadArchived("thread-1", { closeOpenPanels: true });
threadSurfaces(plugin).notifyThreadArchived("thread-1", { closeOpenPanels: true });
expect(restoredMatchingLeaf.detach).toHaveBeenCalledOnce();
expect(matchingLeaf.detach).toHaveBeenCalledOnce();
@ -378,9 +396,11 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
it("refreshes shared thread lists after rename lifecycle notifications", async () => {
const plugin = await pluginWithLeaves([]);
const refreshSharedThreadList = vi.spyOn(plugin, "refreshSharedThreadListFromOpenSurface").mockImplementation(() => undefined);
const refreshSharedThreadList = vi
.spyOn(threadSurfaces(plugin), "refreshSharedThreadListFromOpenSurface")
.mockImplementation(() => undefined);
plugin.notifyThreadRenamed("thread-1", "Renamed thread");
threadSurfaces(plugin).notifyThreadRenamed("thread-1", "Renamed thread");
expect(refreshSharedThreadList).toHaveBeenCalledOnce();
});
@ -396,8 +416,8 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
);
const secondFetch = vi.fn().mockResolvedValue([thread("second")]);
const first = plugin.refreshThreadList(fetchThreads);
const second = plugin.refreshThreadList(secondFetch);
const first = sharedAppServerCache(plugin).refreshThreadList(fetchThreads);
const second = sharedAppServerCache(plugin).refreshThreadList(secondFetch);
expect(fetchThreads).toHaveBeenCalledOnce();
expect(secondFetch).not.toHaveBeenCalled();
@ -405,16 +425,16 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
await expect(first).resolves.toEqual([thread("first")]);
await expect(second).resolves.toEqual([thread("first")]);
expect(plugin.cachedThreadList()).toEqual([thread("first")]);
expect(sharedAppServerCache(plugin).cachedThreadList()).toEqual([thread("first")]);
});
it("keeps the previous shared thread list when refresh fails", async () => {
const plugin = await pluginWithLeaves([]);
await plugin.refreshThreadList(() => Promise.resolve([thread("cached")]));
await sharedAppServerCache(plugin).refreshThreadList(() => Promise.resolve([thread("cached")]));
await expect(plugin.refreshThreadList(() => Promise.reject(new Error("boom")))).rejects.toThrow("boom");
await expect(sharedAppServerCache(plugin).refreshThreadList(() => Promise.reject(new Error("boom")))).rejects.toThrow("boom");
expect(plugin.cachedThreadList()).toEqual([thread("cached")]);
expect(sharedAppServerCache(plugin).cachedThreadList()).toEqual([thread("cached")]);
});
it("refreshes shared thread lists from a connected chat panel", async () => {
@ -433,7 +453,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves([disconnectedLeaf, connectedLeaf]);
plugin.refreshSharedThreadListFromOpenSurface();
threadSurfaces(plugin).refreshSharedThreadListFromOpenSurface();
expect(disconnectedRefresh).not.toHaveBeenCalled();
expect(connectedRefresh).toHaveBeenCalledOnce();
@ -462,7 +482,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
const plugin = await pluginWithLeaves(leaves);
sourceLeaf.detach();
plugin.notifyThreadArchived("thread-1");
threadSurfaces(plugin).notifyThreadArchived("thread-1");
expect(sourceRefresh).not.toHaveBeenCalled();
expect(remainingRefresh).toHaveBeenCalledOnce();
@ -535,12 +555,10 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) {
settings: { ...DEFAULT_SETTINGS, codexPath: "codex", sendShortcut: "enter" },
vaultPath: "/vault",
openThreadInNewView: vi.fn(),
openThreadInAvailableView: vi.fn(),
focusThreadInOpenView: vi.fn(),
openTurnDiff: vi.fn(),
notifyThreadArchived: vi.fn(),
notifyThreadRenamed: vi.fn(),
refreshOpenViews: vi.fn(),
refreshSharedThreadListFromOpenSurface: vi.fn(),
refreshThreadsViewLiveState: vi.fn(),
applyThreadListSnapshot: vi.fn(),

View file

@ -368,6 +368,7 @@ function newSettingsTab(
} = {},
): CodexPanelSettingTab {
return new CodexPanelSettingTab(
{} as never,
{} as never,
{
settings: {