mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix duplicate Codex plan approval panels
This commit is contained in:
63
packages/app/e2e/codex-plan-approval.spec.ts
Normal file
63
packages/app/e2e/codex-plan-approval.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
import { allowPermission, waitForPermissionPrompt } from "./helpers/app";
|
||||
import { connectTerminalClient } from "./helpers/terminal-perf";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
|
||||
function getServerId(): string {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set.");
|
||||
}
|
||||
return serverId;
|
||||
}
|
||||
|
||||
test.describe("Codex plan approval", () => {
|
||||
test("shows a single actionable plan panel and removes it after implementation starts", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
const repo = await createTempGitRepo("codex-plan-approval-");
|
||||
const client = await connectTerminalClient();
|
||||
|
||||
try {
|
||||
const workspaceResult = await client.openProject(repo.path);
|
||||
if (!workspaceResult.workspace) {
|
||||
throw new Error(workspaceResult.error ?? `Failed to open project ${repo.path}`);
|
||||
}
|
||||
|
||||
const agent = await client.createAgent({
|
||||
provider: "codex",
|
||||
cwd: repo.path,
|
||||
title: "Codex plan approval e2e",
|
||||
model: "gpt-5.4-mini",
|
||||
thinkingOptionId: "low",
|
||||
featureValues: { plan_mode: true, fast_mode: true },
|
||||
initialPrompt:
|
||||
"Create a concise plan to add a README note. Present the plan only and wait for approval.",
|
||||
});
|
||||
|
||||
const agentUrl = `${buildHostWorkspaceRoute(
|
||||
getServerId(),
|
||||
repo.path,
|
||||
)}?open=${encodeURIComponent(`agent:${agent.id}`)}`;
|
||||
await page.goto(agentUrl);
|
||||
|
||||
await waitForPermissionPrompt(page, 120_000);
|
||||
|
||||
await expect(page.getByTestId("permission-plan-card")).toHaveCount(1);
|
||||
await expect(page.getByTestId("timeline-plan-card")).toHaveCount(0);
|
||||
|
||||
await allowPermission(page);
|
||||
|
||||
await expect(page.getByTestId("permission-plan-card")).toHaveCount(0, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByTestId("timeline-plan-card")).toHaveCount(0);
|
||||
} finally {
|
||||
await client.close();
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,10 @@ export interface TerminalPerfDaemonClient {
|
||||
cwd: string;
|
||||
title?: string;
|
||||
modeId?: string;
|
||||
model?: string;
|
||||
thinkingOptionId?: string;
|
||||
featureValues?: Record<string, unknown>;
|
||||
initialPrompt?: string;
|
||||
}): Promise<{ id: string; status: string }>;
|
||||
sendAgentMessage(agentId: string, text: string): Promise<void>;
|
||||
subscribeTerminal(
|
||||
|
||||
@@ -1129,6 +1129,7 @@ function PermissionRequestCard({
|
||||
description={description}
|
||||
text={planMarkdown}
|
||||
footer={footer}
|
||||
testID="permission-plan-card"
|
||||
disableOuterSpacing
|
||||
/>
|
||||
);
|
||||
@@ -1141,7 +1142,12 @@ function PermissionRequestCard({
|
||||
{description ? <Text style={permissionStyles.description}>{description}</Text> : null}
|
||||
|
||||
{planMarkdown ? (
|
||||
<PlanCard title="Proposed plan" text={planMarkdown} disableOuterSpacing />
|
||||
<PlanCard
|
||||
title="Proposed plan"
|
||||
text={planMarkdown}
|
||||
testID="permission-plan-card"
|
||||
disableOuterSpacing
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isPlanRequest ? (
|
||||
|
||||
@@ -2973,6 +2973,7 @@ export const ToolCall = memo(function ToolCall({
|
||||
<PlanCard
|
||||
title="Plan"
|
||||
text={effectiveDetail.text}
|
||||
testID="timeline-plan-card"
|
||||
disableOuterSpacing={disableOuterSpacing}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -199,12 +199,14 @@ export function PlanCard({
|
||||
text,
|
||||
footer,
|
||||
disableOuterSpacing = false,
|
||||
testID,
|
||||
}: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
text: string;
|
||||
footer?: ReactNode;
|
||||
disableOuterSpacing?: boolean;
|
||||
testID?: string;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
const markdownStyles = createMarkdownStyles(theme);
|
||||
@@ -231,7 +233,7 @@ export function PlanCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<View testID={testID} style={containerStyle}>
|
||||
<Text style={titleStyle}>{title}</Text>
|
||||
{description ? <Text style={descriptionStyle}>{description}</Text> : null}
|
||||
<Markdown style={markdownStyles} rules={markdownRules}>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user