diff --git a/desktop/src/features/workstream-board/lib/workstreamAgentStatuses.test.mjs b/desktop/src/features/workstream-board/lib/workstreamAgentStatuses.test.mjs
new file mode 100644
index 0000000000..b61d69b2c1
--- /dev/null
+++ b/desktop/src/features/workstream-board/lib/workstreamAgentStatuses.test.mjs
@@ -0,0 +1,55 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { classifyWorkstreamAgents } from "./workstreamAgentStatuses.ts";
+
+const A = "a".repeat(64);
+const B = "b".repeat(64);
+const C = "c".repeat(64);
+const assignees = [
+ { pubkey: A, name: "Active" },
+ { pubkey: B, name: "Blocked" },
+ { pubkey: C, name: "Unknown" },
+];
+
+test("classifies only evidence-backed working and waiting states", () => {
+ assert.deepEqual(
+ classifyWorkstreamAgents(
+ assignees,
+ { channelId: "channel", agentCount: 1, agentPubkeys: [A], anchorAt: 1 },
+ [
+ {
+ key: "blocked",
+ actor: { kind: "agent", pubkey: B.toUpperCase(), name: "Blocked" },
+ reason: "Needs a decision",
+ source: "manual",
+ },
+ ],
+ ),
+ { working: [assignees[0]], waiting: [assignees[1]] },
+ );
+});
+
+test("working wins when the same assigned agent also has a blocker", () => {
+ const result = classifyWorkstreamAgents(
+ assignees,
+ { channelId: "channel", agentCount: 1, agentPubkeys: [B], anchorAt: 1 },
+ [
+ {
+ key: "blocked",
+ actor: { kind: "agent", pubkey: B, name: "Blocked" },
+ reason: "Needs a decision",
+ source: "manual",
+ },
+ ],
+ );
+ assert.deepEqual(result.working, [assignees[1]]);
+ assert.deepEqual(result.waiting, []);
+});
+
+test("does not assert idle or waiting for non-active assignees without named blockers", () => {
+ assert.deepEqual(classifyWorkstreamAgents(assignees, undefined, []), {
+ working: [],
+ waiting: [],
+ });
+});
diff --git a/desktop/src/features/workstream-board/lib/workstreamAgentStatuses.ts b/desktop/src/features/workstream-board/lib/workstreamAgentStatuses.ts
new file mode 100644
index 0000000000..0814839590
--- /dev/null
+++ b/desktop/src/features/workstream-board/lib/workstreamAgentStatuses.ts
@@ -0,0 +1,45 @@
+import type { ActiveChannelTurnSummary } from "@/features/agents/activeAgentTurnsStore";
+import type { WorkstreamIdentity } from "@/features/workstream-board/lib/workstreamCardParser";
+import type { WorkstreamWait } from "@/features/workstream-board/lib/workstreamWaits";
+import { normalizePubkey } from "@/shared/lib/pubkey";
+
+export type WorkstreamAgentStatuses = {
+ working: WorkstreamIdentity[];
+ waiting: WorkstreamIdentity[];
+};
+
+/** Only projects states backed by current evidence. Non-active assignees remain unclassified. */
+export function classifyWorkstreamAgents(
+ assignees: readonly WorkstreamIdentity[],
+ activeWorking: ActiveChannelTurnSummary | undefined,
+ waits: readonly WorkstreamWait[],
+): WorkstreamAgentStatuses {
+ const active = new Set(
+ activeWorking?.agentPubkeys.map((pubkey) => normalizePubkey(pubkey)) ?? [],
+ );
+ const waiting = new Set(
+ waits.flatMap((wait) =>
+ wait.actor.pubkey ? [normalizePubkey(wait.actor.pubkey)] : [],
+ ),
+ );
+ const seen = new Set
();
+ const unique = assignees.filter((assignee) => {
+ const pubkey = normalizePubkey(assignee.pubkey);
+ if (seen.has(pubkey)) return false;
+ seen.add(pubkey);
+ return true;
+ });
+ const working = unique.filter((assignee) =>
+ active.has(normalizePubkey(assignee.pubkey)),
+ );
+ const workingKeys = new Set(
+ working.map((assignee) => normalizePubkey(assignee.pubkey)),
+ );
+ return {
+ working,
+ waiting: unique.filter((assignee) => {
+ const pubkey = normalizePubkey(assignee.pubkey);
+ return !workingKeys.has(pubkey) && waiting.has(pubkey);
+ }),
+ };
+}
diff --git a/desktop/src/features/workstream-board/lib/workstreamWaits.test.mjs b/desktop/src/features/workstream-board/lib/workstreamWaits.test.mjs
index 196c24712e..cbbe25a826 100644
--- a/desktop/src/features/workstream-board/lib/workstreamWaits.test.mjs
+++ b/desktop/src/features/workstream-board/lib/workstreamWaits.test.mjs
@@ -29,6 +29,12 @@ test("manual waits retain simultaneous unique entries and ignore malformed or du
reason: "Ignored",
},
{ key: "bad", actor: { kind: "person", name: "" }, reason: "Invalid" },
+ {
+ key: "bad-since",
+ actor: { kind: "person", name: "Logan" },
+ reason: "Invalid timestamp",
+ since: "not-a-date",
+ },
]).map((wait) => wait.key),
["log", "review"],
);
diff --git a/desktop/src/features/workstream-board/lib/workstreamWaits.ts b/desktop/src/features/workstream-board/lib/workstreamWaits.ts
index 9ad48684c3..041195518f 100644
--- a/desktop/src/features/workstream-board/lib/workstreamWaits.ts
+++ b/desktop/src/features/workstream-board/lib/workstreamWaits.ts
@@ -55,6 +55,11 @@ export function isWorkstreamWait(
)
return false;
if (typeof actor.name !== "string" || !actor.name.trim()) return false;
+ if (
+ wait.since !== undefined &&
+ (typeof wait.since !== "string" || !Number.isFinite(Date.parse(wait.since)))
+ )
+ return false;
return (
actor.pubkey === undefined ||
(typeof actor.pubkey === "string" && !!actor.pubkey.trim())
diff --git a/desktop/src/features/workstream-board/ui/WorkstreamAgentStatusPills.tsx b/desktop/src/features/workstream-board/ui/WorkstreamAgentStatusPills.tsx
new file mode 100644
index 0000000000..83bbb4d698
--- /dev/null
+++ b/desktop/src/features/workstream-board/ui/WorkstreamAgentStatusPills.tsx
@@ -0,0 +1,75 @@
+import type { UserProfileLookup } from "@/features/profile/lib/identity";
+import type { WorkstreamIdentity } from "@/features/workstream-board/lib/workstreamCardParser";
+import type { WorkstreamAgentStatuses } from "@/features/workstream-board/lib/workstreamAgentStatuses";
+import { normalizePubkey } from "@/shared/lib/pubkey";
+import { AvatarStack } from "@/shared/ui/AvatarStack";
+import { cn } from "@/shared/lib/cn";
+
+function StatusPill({
+ agents,
+ label,
+ profiles,
+ status,
+}: {
+ agents: readonly WorkstreamIdentity[];
+ label: string;
+ profiles?: UserProfileLookup;
+ status: "working" | "waiting";
+}) {
+ if (agents.length === 0) return null;
+ const items = agents.map((agent) => {
+ const profile = profiles?.[normalizePubkey(agent.pubkey)];
+ return {
+ id: normalizePubkey(agent.pubkey),
+ avatarUrl: profile?.avatarUrl ?? null,
+ displayName: profile?.displayName?.trim() || agent.name,
+ };
+ });
+ return (
+ item.displayName).join(", ")}
+ >
+
+
+ {agents.length} {label}
+
+
+ );
+}
+
+export function WorkstreamAgentStatusPills({
+ profiles,
+ statuses,
+}: {
+ profiles?: UserProfileLookup;
+ statuses: WorkstreamAgentStatuses;
+}) {
+ if (statuses.working.length === 0 && statuses.waiting.length === 0)
+ return null;
+ return (
+
+
+
+
+ );
+}
diff --git a/desktop/src/features/workstream-board/ui/WorkstreamBoardScreen.tsx b/desktop/src/features/workstream-board/ui/WorkstreamBoardScreen.tsx
index 372b752a7f..a470819a7e 100644
--- a/desktop/src/features/workstream-board/ui/WorkstreamBoardScreen.tsx
+++ b/desktop/src/features/workstream-board/ui/WorkstreamBoardScreen.tsx
@@ -131,6 +131,7 @@ export function WorkstreamBoardScreen() {
void goChannel(channelId)}
onWaitsChange={(waits, createdAt) =>
diff --git a/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx b/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx
index 9ce58af462..51e1a7ac97 100644
--- a/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx
+++ b/desktop/src/features/workstream-board/ui/WorkstreamCard.tsx
@@ -18,7 +18,9 @@ import {
import { buildWorkstreamCardViewModel } from "@/features/workstream-board/lib/workstreamCardViewModel";
import { WorkstreamPullRequests } from "@/features/workstream-board/ui/WorkstreamPullRequests";
import { WorkstreamWaits } from "@/features/workstream-board/ui/WorkstreamWaits";
-import { WorkstreamWorkingIndicator } from "@/features/workstream-board/ui/WorkstreamWorkingIndicator";
+import { WorkstreamAgentStatusPills } from "@/features/workstream-board/ui/WorkstreamAgentStatusPills";
+import { classifyWorkstreamAgents } from "@/features/workstream-board/lib/workstreamAgentStatuses";
+import { normalizePubkey } from "@/shared/lib/pubkey";
import type { Channel } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
@@ -26,6 +28,7 @@ type WorkstreamCardProps = {
activeWorking?: ActiveChannelTurnSummary;
channel: Channel;
profiles?: UserProfileLookup;
+ currentOwnerPubkey?: string;
onWaitsChange?: (
waits: readonly WorkstreamWait[],
createdAt: string | null | undefined,
@@ -36,6 +39,7 @@ type WorkstreamCardProps = {
export function WorkstreamCard({
activeWorking,
channel,
+ currentOwnerPubkey,
onSelect,
onWaitsChange,
profiles,
@@ -65,11 +69,30 @@ export function WorkstreamCard({
React.useEffect(() => {
onWaitsChange?.(waits, detailsQuery.data?.createdAt);
}, [detailsQuery.data?.createdAt, onWaitsChange, waits]);
+ const agentStatuses = React.useMemo(
+ () =>
+ viewModel.status === "ready"
+ ? classifyWorkstreamAgents(
+ viewModel.card.assignees,
+ activeWorking,
+ waits,
+ )
+ : { working: [], waiting: [] },
+ [activeWorking, viewModel, waits],
+ );
+ const waitingOnPrincipal = waits.some(
+ (wait) =>
+ wait.actor.pubkey &&
+ currentOwnerPubkey &&
+ normalizePubkey(wait.actor.pubkey) ===
+ normalizePubkey(currentOwnerPubkey),
+ );
return (
@@ -80,11 +103,20 @@ export function WorkstreamCard({
>
Open #{channel.name}
+ {waitingOnPrincipal ? (
+
+ Priority
+
+ ) : null}
-
+
+
{viewModel.status === "ready" ? (
<>
@@ -97,25 +129,21 @@ export function WorkstreamCard({
{viewModel.card.orchestrator.name}
- {viewModel.card.assignees.length > 0 ? (
-
- {viewModel.card.assignees.map((assignee) => (
-
- {assignee.name}
-
- ))}
-
- ) : null}
+
{references.length > 0 ? (
) : null}
-
+
>
) : viewModel.status === "loading" ? (
@@ -135,12 +163,6 @@ export function WorkstreamCard({
) : null}
)}
- {activeWorking ? (
-
- ) : null}