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;
|
cwd: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
modeId?: string;
|
modeId?: string;
|
||||||
|
model?: string;
|
||||||
|
thinkingOptionId?: string;
|
||||||
|
featureValues?: Record<string, unknown>;
|
||||||
|
initialPrompt?: string;
|
||||||
}): Promise<{ id: string; status: string }>;
|
}): Promise<{ id: string; status: string }>;
|
||||||
sendAgentMessage(agentId: string, text: string): Promise<void>;
|
sendAgentMessage(agentId: string, text: string): Promise<void>;
|
||||||
subscribeTerminal(
|
subscribeTerminal(
|
||||||
|
|||||||
@@ -1129,6 +1129,7 @@ function PermissionRequestCard({
|
|||||||
description={description}
|
description={description}
|
||||||
text={planMarkdown}
|
text={planMarkdown}
|
||||||
footer={footer}
|
footer={footer}
|
||||||
|
testID="permission-plan-card"
|
||||||
disableOuterSpacing
|
disableOuterSpacing
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1141,7 +1142,12 @@ function PermissionRequestCard({
|
|||||||
{description ? <Text style={permissionStyles.description}>{description}</Text> : null}
|
{description ? <Text style={permissionStyles.description}>{description}</Text> : null}
|
||||||
|
|
||||||
{planMarkdown ? (
|
{planMarkdown ? (
|
||||||
<PlanCard title="Proposed plan" text={planMarkdown} disableOuterSpacing />
|
<PlanCard
|
||||||
|
title="Proposed plan"
|
||||||
|
text={planMarkdown}
|
||||||
|
testID="permission-plan-card"
|
||||||
|
disableOuterSpacing
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!isPlanRequest ? (
|
{!isPlanRequest ? (
|
||||||
|
|||||||
@@ -2973,6 +2973,7 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
<PlanCard
|
<PlanCard
|
||||||
title="Plan"
|
title="Plan"
|
||||||
text={effectiveDetail.text}
|
text={effectiveDetail.text}
|
||||||
|
testID="timeline-plan-card"
|
||||||
disableOuterSpacing={disableOuterSpacing}
|
disableOuterSpacing={disableOuterSpacing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -199,12 +199,14 @@ export function PlanCard({
|
|||||||
text,
|
text,
|
||||||
footer,
|
footer,
|
||||||
disableOuterSpacing = false,
|
disableOuterSpacing = false,
|
||||||
|
testID,
|
||||||
}: {
|
}: {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
text: string;
|
text: string;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
disableOuterSpacing?: boolean;
|
disableOuterSpacing?: boolean;
|
||||||
|
testID?: string;
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const markdownStyles = createMarkdownStyles(theme);
|
const markdownStyles = createMarkdownStyles(theme);
|
||||||
@@ -231,7 +233,7 @@ export function PlanCard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={containerStyle}>
|
<View testID={testID} style={containerStyle}>
|
||||||
<Text style={titleStyle}>{title}</Text>
|
<Text style={titleStyle}>{title}</Text>
|
||||||
{description ? <Text style={descriptionStyle}>{description}</Text> : null}
|
{description ? <Text style={descriptionStyle}>{description}</Text> : null}
|
||||||
<Markdown style={markdownStyles} rules={markdownRules}>
|
<Markdown style={markdownStyles} rules={markdownRules}>
|
||||||
|
|||||||
@@ -770,6 +770,14 @@ describe("Codex app-server provider", () => {
|
|||||||
turn: { status: "completed", error: null },
|
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({
|
expect(events.at(-2)).toEqual({
|
||||||
type: "permission_requested",
|
type: "permission_requested",
|
||||||
provider: "codex",
|
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", () => {
|
test("emits usage_updated on token usage updates and keeps usage on turn completion", () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const events: AgentStreamEvent[] = [];
|
const events: AgentStreamEvent[] = [];
|
||||||
|
|||||||
@@ -3724,6 +3724,12 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
});
|
});
|
||||||
if (timelineItem) {
|
if (timelineItem) {
|
||||||
this.rememberPlanResult(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 });
|
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3883,6 +3889,11 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
if (timelineItem.type === "tool_call") {
|
if (timelineItem.type === "tool_call") {
|
||||||
if (timelineItem.detail.type === "plan") {
|
if (timelineItem.detail.type === "plan") {
|
||||||
this.rememberPlanResult(timelineItem);
|
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);
|
this.warnOnIncompleteEditToolCall(timelineItem, "item_completed", parsed.item);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user