Deduplicate auto-review results

This commit is contained in:
murashit 2026-05-13 08:17:05 +09:00
parent 4bb631c309
commit aa3ffa47aa
2 changed files with 91 additions and 2 deletions

View file

@ -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";
}

View file

@ -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<ServerNotification, { method: "guardianWarning" }>);
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<ServerNotification, { method: "item/autoApprovalReview/completed" }>);
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<ServerNotification, { method: "item/autoApprovalReview/completed" }>);
controller.handleNotification({
method: "guardianWarning",
params: { threadId: "thread-active", message: "Auto-review approved: npm test" },
} satisfies Extract<ServerNotification, { method: "guardianWarning" }>);
expect(state.displayItems).toHaveLength(1);
expect(state.displayItems[0]).toMatchObject({ id: "review-review-1" });
});
});
function supportedApprovalRequests(): ServerRequest[] {