Allow task reassignment and color review results

Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
John Tennant
2026-09-09 14:40:42 -04:00
parent 26a26e8d10
commit 29966872ce
5 changed files with 103 additions and 22 deletions

View File

@@ -21,16 +21,16 @@ test("assignment preserves unrelated frontmatter, comments and body", () => {
});
});
test("retry retains operation and refuses another assignee", () => {
test("retry retains operation and reassignment replaces notification state", () => {
const pending = assignmentCanvas(canvas, pubkey, "operation", "pending");
assert.equal(
taskAssignee(assignmentCanvas(pending, pubkey, "operation", "sent"))
.notification.status,
"sent",
);
assert.throws(
() => assignmentCanvas(pending, "b".repeat(64), "new", "pending"),
/already assigned/,
assert.deepEqual(
taskAssignee(assignmentCanvas(pending, "b".repeat(64), "new", "pending")),
{ pubkey: "b".repeat(64), notification: { id: "new", status: "pending" } },
);
assert.throws(
() => assignmentCanvas(canvas, "bad", "new", "pending"),

View File

@@ -32,8 +32,6 @@ export function assignmentCanvas(
): string {
if (!/^[a-f0-9]{64}$/i.test(pubkey)) throw new Error("Invalid agent pubkey");
const existing = taskAssignee(content);
if (existing && existing.pubkey !== pubkey)
throw new Error("Task is already assigned");
const { doc, body } = document(content);
if (!existing) doc.set("assignee", doc.createNode({}));
doc.setIn(["assignee", "pubkey"], pubkey);

View File

@@ -38,7 +38,11 @@ export function AssignTask({
setPending(true);
setError("");
try {
const operationId = assignee?.notification?.id ?? crypto.randomUUID();
const operationId =
assignee?.pubkey === pubkey &&
assignee.notification?.status === "pending"
? assignee.notification.id
: crypto.randomUUID();
const result = await invokeTauri<{
assigned: boolean;
notified: boolean;
@@ -72,13 +76,20 @@ export function AssignTask({
return (
<div className="ml-auto flex items-center gap-2 text-xs">
{assignee ? (
{assignee && !choosing ? (
<>
<span title={assignee.pubkey}>
<button
type="button"
title="Change assignee"
aria-label="Change assignee"
disabled={pending}
onClick={() => setChoosing(true)}
className="rounded px-2 py-1 hover:bg-accent disabled:opacity-50"
>
Assigned to{" "}
{agents.data?.find((agent) => agent.pubkey === assignee.pubkey)
?.name ?? "agent"}
</span>
</button>
{assignee.notification?.status === "pending" && (
<button
type="button"
@@ -92,6 +103,11 @@ export function AssignTask({
</>
) : choosing ? (
<>
{assignee && (
<span className="text-muted-foreground">
Reassigning does not stop the previous agent.
</span>
)}
<select
aria-label="Assign task to agent"
disabled={pending}
@@ -103,7 +119,11 @@ export function AssignTask({
{agents.isLoading ? "Loading agents…" : "Choose agent…"}
</option>
{agents.data?.map((agent) => (
<option key={agent.pubkey} value={agent.pubkey}>
<option
key={agent.pubkey}
value={agent.pubkey}
disabled={agent.pubkey === assignee?.pubkey}
>
{agent.name}
</option>
))}

View File

@@ -1,6 +1,13 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { ClipboardCheck } from "lucide-react";
import {
CircleCheck,
CircleHelp,
CircleX,
ClipboardCheck,
Clock3,
TriangleAlert,
} from "lucide-react";
import type { ChannelBackedTask } from "../lib/channelBackedTask";
import { useTaskBranchStatus } from "../useTaskBranchStatus";
import { invokeTauri } from "@/shared/api/tauri";
@@ -80,6 +87,29 @@ export function TaskReviewModal({
<p>No attributable local runs found for this branch.</p>
)}
{[...latest].map(([name, { run, review }]) => {
const count =
review.finding_count ?? review.verdict?.findings?.length;
const failed = review.invocation_ok === false;
const clear = review.invocation_ok === true && count === 0;
const ResultIcon = failed
? CircleX
: count && count > 0
? TriangleAlert
: clear
? CircleCheck
: CircleHelp;
const resultColor = failed
? "text-red-600 dark:text-red-400"
: count && count > 0
? "text-amber-600 dark:text-amber-400"
: clear
? "text-green-600 dark:text-green-400"
: "text-muted-foreground";
const resultLabel = failed
? "Run failed"
: count !== undefined
? `${count} ${count === 1 ? "finding" : "findings"}`
: "Result unknown";
const freshness =
name === "pr-template"
? typeof run.pr_metadata?.title !== "string" ||
@@ -98,15 +128,16 @@ export function TaskReviewModal({
return (
<details key={name} className="rounded-lg border p-3 text-sm">
<summary className="cursor-pointer">
<ResultIcon
aria-hidden="true"
data-testid={`review-result-${name}`}
className={`mr-2 inline-block size-4 align-text-bottom ${resultColor}`}
/>
<span className="font-medium">{name}</span>
<span className="ml-2 text-muted-foreground">
{review.invocation_ok === false
? "Run failed"
: review.finding_count !== undefined ||
review.verdict?.findings
? `${review.finding_count ?? review.verdict?.findings?.length} findings`
: "Result unknown"}{" "}
· {freshness}
<span className={`ml-2 ${resultColor}`}>{resultLabel}</span>
<span className="mt-1 flex items-center gap-1 pl-6 text-xs text-muted-foreground">
<Clock3 aria-hidden="true" className="size-3" />
{freshness}
</span>
</summary>
<div className="mt-3 space-y-2">

View File

@@ -87,7 +87,19 @@ test("review modal shows latest intent results and conservative freshness", asyn
await expect(dialog).toBeVisible();
await expect(
dialog.locator("summary").filter({ hasText: "builderbot" }),
).toHaveText("builderbot1 findings · Same commit");
).toContainText("1 finding");
await expect(
dialog.locator("summary").filter({ hasText: "builderbot" }),
).toContainText("Same commit");
await expect(dialog.getByTestId("review-result-builderbot")).toHaveClass(
/text-amber-600/,
);
await expect(dialog.getByTestId("review-result-pr-template")).toHaveClass(
/text-green-600/,
);
await expect(dialog.getByTestId("review-result-dead-code")).toHaveClass(
/text-red-600/,
);
await expect(
dialog.locator("summary").filter({ hasText: "pr-template" }),
).toContainText("PR metadata changed");
@@ -120,7 +132,10 @@ test("assign saves the task and retains notification retry after navigating away
}) => {
await installMockBridge(page, {
canvasContent: TASK_CANVAS,
managedAgents: [{ pubkey: "a".repeat(64), name: "Sol", status: "stopped" }],
managedAgents: [
{ pubkey: "a".repeat(64), name: "Sol", status: "stopped" },
{ pubkey: "b".repeat(64), name: "Luna", status: "stopped" },
],
});
await page.goto("/");
await expect(page.getByTestId("channel-engineering")).toBeVisible();
@@ -134,16 +149,19 @@ test("assign saves the task and retains notification retry after navigating away
) => Promise<unknown>;
};
__ASSIGN_CALLS__: number;
__ASSIGN_OPERATIONS__: string[];
};
const original = w.__TAURI_INTERNALS__.invoke.bind(w.__TAURI_INTERNALS__);
let content: string | undefined;
let channel: string | undefined;
w.__ASSIGN_CALLS__ = 0;
w.__ASSIGN_OPERATIONS__ = [];
w.__TAURI_INTERNALS__.invoke = async (command, payload, options) => {
if (command === "get_canvas" && content && payload.channelId === channel)
return { content };
if (command === "assign_task_channel") {
w.__ASSIGN_CALLS__++;
w.__ASSIGN_OPERATIONS__.push(payload.operationId);
channel = payload.channelId;
content =
w.__ASSIGN_CALLS__ === 1
@@ -185,6 +203,20 @@ test("assign saves the task and retains notification retry after navigating away
.__ASSIGN_CALLS__,
),
).toBe(2);
await page.getByRole("button", { name: "Change assignee" }).click();
await page
.getByRole("combobox", { name: "Assign task to agent" })
.selectOption("b".repeat(64));
await expect(page.getByTestId("task-overview")).toContainText(
"Assigned to Luna",
);
const operations = await page.evaluate(
() =>
(window as typeof window & { __ASSIGN_OPERATIONS__: string[] })
.__ASSIGN_OPERATIONS__,
);
expect(operations[0]).toBe(operations[1]);
expect(operations[2]).not.toBe(operations[0]);
});
test("loading a corrected canvas refreshes the sidebar task metadata", async ({