Sync thread list refreshes

This commit is contained in:
murashit 2026-05-26 09:07:52 +09:00
parent 406edc6f57
commit 94f5bd0730
6 changed files with 102 additions and 0 deletions

View file

@ -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();
}

View file

@ -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<void> {
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(() => {

View file

@ -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<number>();
private latestThreadList: Thread[] | null = null;
override async onload(): Promise<void> {
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) {

View file

@ -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> = {}): CodexChatHost {
refreshOpenThreadLists: vi.fn(),
refreshThreadsViewLiveState: vi.fn(),
refreshThreadsViewThreadList: vi.fn(),
publishThreadList: vi.fn(),
...overrides,
};
}

View file

@ -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<string, unknown> = {}) {
notifyThreadArchived: vi.fn(),
notifyThreadRenamed: vi.fn(),
refreshOpenThreadLists: vi.fn(),
publishThreadList: vi.fn(),
cachedThreadList: vi.fn(() => null),
...overrides,
};
}

View file

@ -243,6 +243,7 @@ function chatView(CodexChatViewCtor: typeof CodexChatView, leaf: TestLeaf) {
refreshOpenThreadLists: vi.fn(),
refreshThreadsViewLiveState: vi.fn(),
refreshThreadsViewThreadList: vi.fn(),
publishThreadList: vi.fn(),
},
);
}