diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index 51412ebd..2da22a30 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -85,6 +85,7 @@ export interface CodexChatHost { refreshOpenThreadLists(): void; refreshThreadsViewLiveState(): void; refreshThreadsViewThreadList(): void; + publishThreadList(threads: Thread[]): void; } interface RestoredThreadState { @@ -295,6 +296,13 @@ export class CodexChatView extends ItemView { void this.refreshThreads(); } + applyThreadListSnapshot(threads: Thread[]): void { + this.state.listedThreads = threads; + this.state.threadsLoaded = true; + this.refreshTabHeader(); + this.render(); + } + openPanelSnapshot(): OpenCodexPanelSnapshot { return { viewId: this.viewId, @@ -424,6 +432,7 @@ export class CodexChatView extends ItemView { if (this.isStaleConnectionGeneration(generation)) return; await this.session.refreshThreadList(); if (this.isStaleConnectionGeneration(generation)) return; + this.publishThreadListSnapshot(); this.scheduleDeferredDiagnostics(); this.refreshTabHeader(); this.setStatus("Connected."); @@ -464,6 +473,7 @@ export class CodexChatView extends ItemView { if (!this.client) return; try { await this.session.refreshThreadList(); + this.publishThreadListSnapshot(); await this.session.refreshSessionMetadata(); this.refreshTabHeader(); this.render(); @@ -554,6 +564,10 @@ export class CodexChatView extends ItemView { this.requestWorkspaceLayoutSave(); } + private publishThreadListSnapshot(): void { + this.plugin.publishThreadList(this.state.listedThreads); + } + private requestWorkspaceLayoutSave(): void { void this.app.workspace.requestSaveLayout(); } diff --git a/src/features/threads-view/view.ts b/src/features/threads-view/view.ts index bc63c237..63c292c7 100644 --- a/src/features/threads-view/view.ts +++ b/src/features/threads-view/view.ts @@ -19,6 +19,8 @@ export interface CodexThreadsHost { getOpenPanelSnapshots(): OpenCodexPanelSnapshot[]; notifyThreadArchived(threadId: string): void; notifyThreadRenamed(threadId: string, name: string): void; + publishThreadList(threads: Thread[]): void; + cachedThreadList(): Thread[] | null; } export class CodexThreadsView extends ItemView { @@ -74,6 +76,10 @@ export class CodexThreadsView extends ItemView { } override async onOpen(): Promise { + const cachedThreads = this.plugin.cachedThreadList(); + if (cachedThreads) { + this.threads = cachedThreads; + } this.render(); void this.refresh(); } @@ -106,6 +112,7 @@ export class CodexThreadsView extends ItemView { const response = await this.client.listThreads(this.plugin.vaultPath); if (this.isStaleRefresh(connectionGeneration, refreshGeneration)) return; this.threads = response.data; + this.plugin.publishThreadList(response.data); this.status = response.data.length === 0 ? "No threads" : null; } catch (error) { if (error instanceof StaleConnectionError) return; @@ -183,6 +190,12 @@ export class CodexThreadsView extends ItemView { this.scheduleRender(); } + applyThreadListSnapshot(threads: Thread[]): void { + this.threads = threads; + this.status = threads.length === 0 ? "No threads" : null; + this.render(); + } + private scheduleRefresh(): void { if (this.refreshTimer !== null) return; this.refreshTimer = this.containerEl.win.setTimeout(() => { diff --git a/src/main.ts b/src/main.ts index 64fc9577..a1b3b00f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { CodexChatView } from "./features/chat/view"; import { CodexChatTurnDiffView } from "./features/chat/chat-turn-diff-view"; import type { OpenCodexPanelSnapshot } from "./features/chat/panel-snapshot"; import { CodexThreadsView } from "./features/threads-view/view"; +import type { Thread } from "./generated/app-server/v2/Thread"; import { DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormalizedData, type CodexPanelSettings } from "./settings/model"; import { CodexPanelSettingTab } from "./settings/tab"; import { persistedChatTurnDiffViewState, type ChatTurnDiffViewState } from "./features/chat/ui/turn-diff"; @@ -37,6 +38,7 @@ export default class CodexPanelPlugin extends Plugin { vaultPath = ""; private bootRestoredPanelLoadCancelled = false; private readonly bootRestoredPanelLoadTimers = new Set(); + private latestThreadList: Thread[] | null = null; override async onload(): Promise { this.bootRestoredPanelLoadCancelled = false; @@ -187,6 +189,24 @@ export default class CodexPanelPlugin extends Plugin { } } + publishThreadList(threads: Thread[]): void { + this.latestThreadList = threads; + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_PANEL)) { + if (leaf.view instanceof CodexChatView) { + leaf.view.applyThreadListSnapshot(threads); + } + } + for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_THREADS)) { + if (leaf.view instanceof CodexThreadsView) { + leaf.view.applyThreadListSnapshot(threads); + } + } + } + + cachedThreadList(): Thread[] | null { + return this.latestThreadList; + } + refreshThreadsViewLiveState(): void { for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_THREADS)) { if (leaf.view instanceof CodexThreadsView) { diff --git a/tests/features/chat/view-connection.test.ts b/tests/features/chat/view-connection.test.ts index f1ab3d63..879e9da9 100644 --- a/tests/features/chat/view-connection.test.ts +++ b/tests/features/chat/view-connection.test.ts @@ -83,6 +83,22 @@ describe("CodexChatView connection lifecycle", () => { expect(client.listThreads).toHaveBeenCalledTimes(1); }); + it("publishes refreshed thread lists after connecting", async () => { + const publishThreadList = vi.fn(); + const threads = [threadFixture("thread-1")]; + const client = connectedClient({ + listThreads: vi.fn().mockResolvedValue({ data: threads }), + }); + connectionMock.state.client = client; + const view = await chatView({ + host: chatHost({ publishThreadList }), + }); + + await view.connect(); + + expect(publishThreadList).toHaveBeenCalledWith(threads); + }); + it("ignores stale connection work after the view closes", async () => { let resolveConfig!: (value: unknown) => void; const client = connectedClient({ @@ -603,6 +619,7 @@ function chatHost(overrides: Partial = {}): CodexChatHost { refreshOpenThreadLists: vi.fn(), refreshThreadsViewLiveState: vi.fn(), refreshThreadsViewThreadList: vi.fn(), + publishThreadList: vi.fn(), ...overrides, }; } diff --git a/tests/features/threads-view/view.test.ts b/tests/features/threads-view/view.test.ts index beab15b9..9d8ffd37 100644 --- a/tests/features/threads-view/view.test.ts +++ b/tests/features/threads-view/view.test.ts @@ -196,6 +196,41 @@ describe("CodexThreadsView", () => { }); }); + it("publishes refreshed thread lists to other thread surfaces", async () => { + const threads = [threadFixture({ id: "thread", preview: "Thread preview" })]; + connectionMock.state.client = clientFixture({ + listThreads: vi.fn().mockResolvedValue({ data: threads }), + }); + const host = threadsHost({ + publishThreadList: vi.fn(), + }); + const view = await threadsView(host); + + await view.refresh(); + + expect(host.publishThreadList).toHaveBeenCalledWith(threads); + }); + + it("renders cached thread lists before refreshing", async () => { + connectionMock.state.client = clientFixture({ + listThreads: vi.fn( + () => + new Promise(() => { + // Keep the app-server refresh pending so the cached snapshot remains visible for this assertion. + }), + ), + }); + const view = await threadsView( + threadsHost({ + cachedThreadList: vi.fn(() => [threadFixture({ id: "cached", preview: "Cached thread" })]), + }), + ); + + await view.onOpen(); + + expect(view.containerEl.textContent).toContain("Cached thread"); + }); + it("notifies open panels after archiving a thread", async () => { const archiveThread = vi.fn().mockResolvedValue({}); connectionMock.state.client = clientFixture({ @@ -294,6 +329,8 @@ function threadsHost(overrides: Record = {}) { notifyThreadArchived: vi.fn(), notifyThreadRenamed: vi.fn(), refreshOpenThreadLists: vi.fn(), + publishThreadList: vi.fn(), + cachedThreadList: vi.fn(() => null), ...overrides, }; } diff --git a/tests/main.test.ts b/tests/main.test.ts index 6a188b72..7ad7a597 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -243,6 +243,7 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) { refreshOpenThreadLists: vi.fn(), refreshThreadsViewLiveState: vi.fn(), refreshThreadsViewThreadList: vi.fn(), + publishThreadList: vi.fn(), }, ); }