Track MCP elicitations in panel reuse guards

This commit is contained in:
murashit 2026-06-21 15:55:02 +09:00
parent fb8f4eee91
commit 4b9575d6fb
4 changed files with 32 additions and 1 deletions

View file

@ -9,6 +9,7 @@ export interface ChatPanelSnapshot {
turnLifecycle: OpenCodexPanelTurnLifecycle;
pendingApprovals: number;
pendingUserInputs: number;
pendingMcpElicitations: number;
hasComposerDraft: boolean;
connected: boolean;
}

View file

@ -375,6 +375,7 @@ function isIdleEmptyPanelSnapshot(snapshot: ChatPanelSnapshot): boolean {
snapshot.turnLifecycle.kind === "idle" &&
snapshot.pendingApprovals === 0 &&
snapshot.pendingUserInputs === 0 &&
snapshot.pendingMcpElicitations === 0 &&
!snapshot.hasComposerDraft
);
}

View file

@ -22,6 +22,7 @@ function openPanelSnapshot(
turnLifecycle: OpenCodexPanelSnapshot["turnLifecycle"];
pendingApprovals: number;
pendingUserInputs: number;
pendingMcpElicitations: number;
hasComposerDraft: boolean;
connected: boolean;
lastFocused: boolean;
@ -34,6 +35,7 @@ function openPanelSnapshot(
turnLifecycle: { kind: "idle" },
pendingApprovals: 0,
pendingUserInputs: 0,
pendingMcpElicitations: 0,
hasComposerDraft: false,
connected: true,
...overrides,
@ -91,10 +93,11 @@ describe("threads view renderer decisions", () => {
openPanelSnapshot({ viewId: "running", threadId: "thread", turnLifecycle: { kind: "running", turnId: "turn" } }),
openPanelSnapshot({ viewId: "approval", threadId: "thread", pendingApprovals: 1 }),
openPanelSnapshot({ viewId: "input", threadId: "thread", pendingUserInputs: 1 }),
openPanelSnapshot({ viewId: "mcp", threadId: "thread", pendingMcpElicitations: 1 }),
],
new Map(),
)[0]?.live,
).toMatchObject({ status: "needs-input", label: "Needs input", viewId: "input", openPanels: 4 });
).toMatchObject({ status: "needs-input", label: "Needs input", viewId: "input", openPanels: 5 });
expect(
threadRows(

View file

@ -113,6 +113,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
turnLifecycle: { kind: "idle" },
pendingApprovals: 0,
pendingUserInputs: 0,
pendingMcpElicitations: 0,
hasComposerDraft: false,
connected: true,
});
@ -140,6 +141,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
turnLifecycle: { kind: "idle" },
pendingApprovals: 0,
pendingUserInputs: 0,
pendingMcpElicitations: 0,
hasComposerDraft: false,
connected: true,
});
@ -152,6 +154,7 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
turnLifecycle: { kind: "idle" },
pendingApprovals: 0,
pendingUserInputs: 0,
pendingMcpElicitations: 0,
hasComposerDraft: false,
connected: true,
});
@ -163,6 +166,28 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
expect(openEmptyThread).toHaveBeenCalledWith("thread-1");
});
it("does not reuse an idle panel with a pending MCP elicitation as empty", async () => {
const { CodexChatView } = await import("../src/features/chat/host/view");
const pendingLeaf = leaf();
pendingLeaf.view = chatView(CodexChatView, pendingLeaf);
const pendingView = pendingLeaf.view as CodexChatView;
vi.spyOn(pendingView.surface, "openPanelSnapshot").mockReturnValue(
panelSnapshot({ viewId: "pending-view", threadId: null, pendingMcpElicitations: 1 }),
);
const openPendingThread = vi.spyOn(pendingView.surface, "openThread").mockResolvedValue(undefined);
const emptyLeaf = leaf();
emptyLeaf.view = chatView(CodexChatView, emptyLeaf);
const emptyView = emptyLeaf.view as CodexChatView;
vi.spyOn(emptyView.surface, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "empty-view", threadId: null }));
const openEmptyThread = vi.spyOn(emptyView.surface, "openThread").mockResolvedValue(undefined);
const plugin = await pluginWithLeaves([pendingLeaf, emptyLeaf]);
await panels(plugin).openThreadInAvailableView("thread-1");
expect(openPendingThread).not.toHaveBeenCalled();
expect(openEmptyThread).toHaveBeenCalledWith("thread-1");
});
it("opens picker Enter selections in the most recent panel when the thread is not already open", async () => {
const { CodexChatView } = await import("../src/features/chat/host/view");
const olderLeaf = leaf();
@ -740,6 +765,7 @@ function panelSnapshot(
turnLifecycle: { kind: "idle" },
pendingApprovals: 0,
pendingUserInputs: 0,
pendingMcpElicitations: 0,
hasComposerDraft: false,
connected: true,
...overrides,