From 0423cf899ffd284008fdf46ba539565bc56f3631 Mon Sep 17 00:00:00 2001 From: murashit Date: Fri, 29 May 2026 22:33:49 +0900 Subject: [PATCH] Fix fork archive thread list refresh --- src/features/chat/thread-actions.ts | 12 +++++---- tests/features/chat/thread-actions.test.ts | 8 +++++- tests/main.test.ts | 29 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/features/chat/thread-actions.ts b/src/features/chat/thread-actions.ts index 932b59ba..ab35e150 100644 --- a/src/features/chat/thread-actions.ts +++ b/src/features/chat/thread-actions.ts @@ -42,10 +42,12 @@ export class ChatThreadActionController { } async archiveThread(threadId: string, saveMarkdown = this.host.settings().archiveExportEnabled): Promise { - await this.archiveThreadWithResult(threadId, saveMarkdown); + if (await this.archiveThreadOnServer(threadId, saveMarkdown)) { + this.host.notifyThreadArchived(threadId); + } } - private async archiveThreadWithResult(threadId: string, saveMarkdown = this.host.settings().archiveExportEnabled): Promise { + private async archiveThreadOnServer(threadId: string, saveMarkdown = this.host.settings().archiveExportEnabled): Promise { if (chatTurnBusy(this.state)) { this.host.addSystemMessage("Finish or interrupt the current turn before archiving threads."); return false; @@ -64,7 +66,6 @@ export class ChatThreadActionController { new Notice(`Saved archived thread to ${result.path}.`); } await client.archiveThread(threadId); - this.host.notifyThreadArchived(threadId); return true; } catch (error) { this.host.addSystemMessage(error instanceof Error ? error.message : String(error)); @@ -117,8 +118,9 @@ export class ChatThreadActionController { } if (archiveSource) { if (!openedForkPanel) return; - const archived = await this.archiveThreadWithResult(threadId); - if (archived) this.host.closePanel(); + if (!(await this.archiveThreadOnServer(threadId))) return; + this.host.closePanel(); + this.host.notifyThreadArchived(threadId); } } catch (error) { this.host.addSystemMessage(error instanceof Error ? error.message : String(error)); diff --git a/tests/features/chat/thread-actions.test.ts b/tests/features/chat/thread-actions.test.ts index c1fe7ba9..b8abb7fc 100644 --- a/tests/features/chat/thread-actions.test.ts +++ b/tests/features/chat/thread-actions.test.ts @@ -46,10 +46,11 @@ describe("ChatThreadActionController", () => { expect(client.rollbackThread).not.toHaveBeenCalled(); expect(client.archiveThread).toHaveBeenCalledWith("source"); expect(host.closePanel).not.toHaveBeenCalled(); + expect(host.notifyThreadArchived).not.toHaveBeenCalled(); expect(host.addSystemMessage).toHaveBeenCalledWith("archive failed"); }); - it("closes the source panel after fork and archive succeeds", async () => { + it("closes the source panel before notifying surfaces after fork and archive succeeds", async () => { const client = clientMock(); const host = hostMock({ client, displayItems: turnItems() }); const controller = new ChatThreadActionController(host); @@ -58,6 +59,11 @@ describe("ChatThreadActionController", () => { expect(client.archiveThread).toHaveBeenCalledWith("source"); expect(host.closePanel).toHaveBeenCalledOnce(); + expect(host.notifyThreadArchived).toHaveBeenCalledWith("source"); + const closeOrder = host.closePanel.mock.invocationCallOrder[0]; + const notifyOrder = host.notifyThreadArchived.mock.invocationCallOrder[0]; + if (closeOrder === undefined || notifyOrder === undefined) throw new Error("Expected close and archive notification calls."); + expect(closeOrder).toBeLessThan(notifyOrder); }); }); diff --git a/tests/main.test.ts b/tests/main.test.ts index 54206f1b..75fae042 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -362,6 +362,35 @@ describe("CodexPanelPlugin boot restored panel loading", () => { expect(disconnectedRefresh).not.toHaveBeenCalled(); expect(connectedRefresh).toHaveBeenCalledOnce(); }); + + it("refreshes shared thread lists from a remaining connected panel after the archived panel is detached", async () => { + const { CodexChatView } = await import("../src/features/chat/view"); + const sourceLeaf = leaf(); + sourceLeaf.view = chatView(CodexChatView, sourceLeaf); + const sourceView = sourceLeaf.view as CodexChatView; + vi.spyOn(sourceView, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "source", threadId: "thread-1", connected: true })); + const sourceRefresh = vi.spyOn(sourceView, "refreshSharedThreadList").mockResolvedValue(undefined); + + const remainingLeaf = leaf(); + remainingLeaf.view = chatView(CodexChatView, remainingLeaf); + const remainingView = remainingLeaf.view as CodexChatView; + vi.spyOn(remainingView, "openPanelSnapshot").mockReturnValue( + panelSnapshot({ viewId: "remaining", threadId: "forked-thread", connected: true }), + ); + const remainingRefresh = vi.spyOn(remainingView, "refreshSharedThreadList").mockResolvedValue(undefined); + + const leaves = [sourceLeaf, remainingLeaf]; + sourceLeaf.detach.mockImplementation(() => { + leaves.splice(leaves.indexOf(sourceLeaf), 1); + }); + const plugin = await pluginWithLeaves(leaves); + + sourceLeaf.detach(); + plugin.notifyThreadArchived("thread-1"); + + expect(sourceRefresh).not.toHaveBeenCalled(); + expect(remainingRefresh).toHaveBeenCalledOnce(); + }); }); async function pluginWithLeaves(leaves: ReturnType[]) {