From aa3ffa47aa494bec008992a9b71b82fc6c14c148 Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 13 May 2026 08:17:05 +0900 Subject: [PATCH] Deduplicate auto-review results --- src/panel/controller.ts | 30 ++++++++++++++-- tests/panel-controller.test.ts | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/panel/controller.ts b/src/panel/controller.ts index 5552ea5c..557b7c11 100644 --- a/src/panel/controller.ts +++ b/src/panel/controller.ts @@ -117,7 +117,8 @@ export class PanelController { "mcp progress", ); } else if (method === "item/autoApprovalReview/started" || method === "item/autoApprovalReview/completed") { - this.state.displayItems = upsertDisplayItem(this.state.displayItems, createAutoReviewResultItem(params)); + const reviewItem = createAutoReviewResultItem(params); + this.state.displayItems = upsertDisplayItem(removeUnstructuredAutoReviewWarnings(this.state.displayItems), reviewItem); } else if (method === "thread/started") { if (params.thread) { if (!this.state.activeThreadId || this.state.activeThreadId === params.thread.id) { @@ -143,7 +144,10 @@ export class PanelController { } else if (method === "thread/compacted") { this.addSystemMessage("Context compacted."); } else if (method === "guardianWarning") { - this.state.displayItems.push(createReviewResultItem(params.message)); + const item = createReviewResultItem(params.message); + if (!isUnstructuredAutoReviewWarning(item) || !hasStructuredAutoReviewResult(this.state.displayItems, this.state.activeTurnId)) { + this.state.displayItems = upsertDisplayItem(this.state.displayItems, item); + } } else if (method === "model/rerouted" || method === "deprecationNotice") { this.addSystemMessage(`${method}: ${jsonPreview(params)}`); } else if (method === "mcpServer/startupStatus/updated") { @@ -381,6 +385,28 @@ function approvalTurnId(approval: PendingApproval): string | undefined { return typeof params.turnId === "string" ? params.turnId : undefined; } +function removeUnstructuredAutoReviewWarnings(items: DisplayItem[]): DisplayItem[] { + return items.filter((item) => !isUnstructuredAutoReviewWarning(item)); +} + +function hasStructuredAutoReviewResult(items: DisplayItem[], activeTurnId: string | null): boolean { + return items.some( + (item) => + item.kind === "reviewResult" && + Boolean(item.turnId) && + (!activeTurnId || item.turnId === activeTurnId) && + isAutoReviewText(item.text), + ); +} + +function isUnstructuredAutoReviewWarning(item: DisplayItem): boolean { + return item.kind === "reviewResult" && !item.turnId && isAutoReviewText(item.text); +} + +function isAutoReviewText(text: string): boolean { + return /^Auto-review\b/i.test(text.trim()); +} + function isUserMessage(item: DisplayItem): item is MessageDisplayItem & { role: "user" } { return item.kind === "message" && item.role === "user"; } diff --git a/tests/panel-controller.test.ts b/tests/panel-controller.test.ts index 52beb470..30029eb9 100644 --- a/tests/panel-controller.test.ts +++ b/tests/panel-controller.test.ts @@ -603,6 +603,69 @@ describe("PanelController", () => { rows: expect.arrayContaining([{ key: "status", value: "approved" }]), }); }); + + it("replaces guardian auto-review warnings when structured auto-review notifications arrive", () => { + const state = createPanelState(); + state.activeThreadId = "thread-active"; + state.activeTurnId = "turn-active"; + const controller = controllerForState(state); + + controller.handleNotification({ + method: "guardianWarning", + params: { threadId: "thread-active", message: "Auto-review approved: npm test" }, + } satisfies Extract); + controller.handleNotification({ + method: "item/autoApprovalReview/completed", + params: { + threadId: "thread-active", + turnId: "turn-active", + startedAtMs: 1, + completedAtMs: 2, + reviewId: "review-1", + targetItemId: "cmd-1", + decisionSource: "agent", + review: { status: "approved", riskLevel: "low", userAuthorization: "medium", rationale: "Allowed by policy." }, + action: { type: "command", source: "shell", command: "npm test", cwd: "/vault" }, + }, + } satisfies Extract); + + expect(state.displayItems).toHaveLength(1); + expect(state.displayItems[0]).toMatchObject({ + id: "review-review-1", + kind: "reviewResult", + text: "Auto-review approved: npm test", + turnId: "turn-active", + }); + }); + + it("ignores guardian auto-review warnings after structured auto-review notifications", () => { + const state = createPanelState(); + state.activeThreadId = "thread-active"; + state.activeTurnId = "turn-active"; + const controller = controllerForState(state); + + controller.handleNotification({ + method: "item/autoApprovalReview/completed", + params: { + threadId: "thread-active", + turnId: "turn-active", + startedAtMs: 1, + completedAtMs: 2, + reviewId: "review-1", + targetItemId: "cmd-1", + decisionSource: "agent", + review: { status: "approved", riskLevel: "low", userAuthorization: "medium", rationale: "Allowed by policy." }, + action: { type: "command", source: "shell", command: "npm test", cwd: "/vault" }, + }, + } satisfies Extract); + controller.handleNotification({ + method: "guardianWarning", + params: { threadId: "thread-active", message: "Auto-review approved: npm test" }, + } satisfies Extract); + + expect(state.displayItems).toHaveLength(1); + expect(state.displayItems[0]).toMatchObject({ id: "review-review-1" }); + }); }); function supportedApprovalRequests(): ServerRequest[] {