Fix duplicate Codex plan approval panels

This commit is contained in:
Mohamed Boudra
2026-05-01 11:42:30 +07:00
parent eee4a772d7
commit fa0af97aa6
7 changed files with 142 additions and 2 deletions
@@ -770,6 +770,14 @@ describe("Codex app-server provider", () => {
turn: { status: "completed", error: null },
});
expect(
events.some(
(event) =>
event.type === "timeline" &&
event.item.type === "tool_call" &&
event.item.detail.type === "plan",
),
).toBe(false);
expect(events.at(-2)).toEqual({
type: "permission_requested",
provider: "codex",
@@ -804,6 +812,51 @@ describe("Codex app-server provider", () => {
});
});
test("does not emit Codex plan thread items as timeline cards while plan approval is pending", () => {
const session = createSession({
featureValues: { plan_mode: true, fast_mode: true },
});
const events: AgentStreamEvent[] = [];
session.subscribe((event) => events.push(event));
asInternals(session).handleNotification("turn/started", {
turn: { id: "turn-plan-thread-item" },
});
asInternals(session).handleNotification("item/completed", {
item: {
id: "plan-item-1",
type: "plan",
text: "- Inspect README\n- Add a short note",
},
});
asInternals(session).handleNotification("turn/completed", {
turn: { status: "completed", error: null },
});
expect(events).not.toContainEqual(
expect.objectContaining({
type: "timeline",
item: expect.objectContaining({
type: "tool_call",
detail: expect.objectContaining({ type: "plan" }),
}),
}),
);
expect(events.at(-2)).toEqual({
type: "permission_requested",
provider: "codex",
turnId: "test-turn",
request: expect.objectContaining({
provider: "codex",
name: "CodexPlanApproval",
kind: "plan",
input: {
plan: "- Inspect README\n- Add a short note",
},
}),
});
});
test("emits usage_updated on token usage updates and keeps usage on turn completion", () => {
const session = createSession();
const events: AgentStreamEvent[] = [];
@@ -3724,6 +3724,12 @@ class CodexAppServerAgentSession implements AgentSession {
});
if (timelineItem) {
this.rememberPlanResult(timelineItem);
// In plan mode, the same plan is rendered through the synthetic approval
// permission. Keep the remembered text for that card, but do not also
// emit a static timeline plan panel.
if (this.planModeEnabled) {
return;
}
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
}
}
@@ -3883,6 +3889,11 @@ class CodexAppServerAgentSession implements AgentSession {
if (timelineItem.type === "tool_call") {
if (timelineItem.detail.type === "plan") {
this.rememberPlanResult(timelineItem);
// Codex can surface plans both as turn/plan updates and as completed
// thread items. In plan mode, approval owns the visible plan card.
if (this.planModeEnabled) {
return;
}
}
this.warnOnIncompleteEditToolCall(timelineItem, "item_completed", parsed.item);
}