Fix fork archive thread list refresh

This commit is contained in:
murashit 2026-05-29 22:33:49 +09:00
parent 52a7ed02a5
commit 0423cf899f
3 changed files with 43 additions and 6 deletions

View file

@ -42,10 +42,12 @@ export class ChatThreadActionController {
}
async archiveThread(threadId: string, saveMarkdown = this.host.settings().archiveExportEnabled): Promise<void> {
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<boolean> {
private async archiveThreadOnServer(threadId: string, saveMarkdown = this.host.settings().archiveExportEnabled): Promise<boolean> {
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));

View file

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

View file

@ -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<typeof leaf>[]) {