Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/ui/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,26 @@
font-variant-numeric: tabular-nums;
}

.agent-halt-dot {
width: 6px;
height: 6px;
flex: none;
background: var(--cx-st-error);
}
.agent-halt-dot[data-halt="done"] {
background: var(--cx-st-done);
}
.agent-halt-dot[data-halt="awaiting_user"] {
background: var(--cx-st-waiting);
}

.agent-waiting-count {
flex: none;
color: var(--cx-st-waiting);
font-size: 10px;
font-variant-numeric: tabular-nums;
}

/* Presence activity string on an agent leaf (T8): a small dim run after the
* badge, truncated so a long status never widens the tree row. */
.agent-activity {
Expand Down Expand Up @@ -1908,6 +1928,14 @@
overflow: hidden;
text-overflow: ellipsis;
}

.fleet-waiting-count {
align-self: center;
margin-left: 10px;
color: var(--cx-st-waiting);
font-size: 11px;
white-space: nowrap;
}
/* A fleet tab's pane: a compact open-workspace header over the inline home-DM
conversation (design: architecture-lineage). */
.fleet-pane {
Expand Down
53 changes: 53 additions & 0 deletions apps/ui/src/components/LeftSidebar.live.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test";
import {
AccountSchema,
AgentAccountSchema,
AgentBoardSchema,
AgentBoardSummarySchema,
AgentPresence,
BoardHaltKind,
BoardHaltSchema,
BoardTaskSchema,
create,
RosterEntrySchema,
UserAccountSchema,
Expand Down Expand Up @@ -168,4 +173,52 @@ describe("LeftSidebar — live roster (T4/T5)", () => {
expect(container.querySelector(".tree-empty")).toBeNull();
expect(container.querySelectorAll(".tree-agent").length).toBeGreaterThan(0);
});

test("An agentBoardChanged event updates the sidebar badge", async () => {
const entry = (waitingOnOperator: number, halt?: BoardHaltKind) =>
create(RosterEntrySchema, {
agentAccountId: "acc-cook",
presence: AgentPresence.WORKING,
board: create(AgentBoardSummarySchema, {
taskCount: 1,
waitingOnOperator,
halt: halt ? create(BoardHaltSchema, { kind: halt }) : undefined,
}),
});
const fake = createFakeComms({
accounts: [wireUserAccount(CALLER), wireAgentAccount("acc-cook")],
roster: [entry(2)],
});
const { container } = await mountLive(fake);
const count = () =>
container
.querySelector(".tree-agent .agent-waiting-count")
?.getAttribute("aria-label");
const leaf = () => container.querySelector(".tree-agent");
expect(count()).toBe("2 waiting on you");
expect(leaf()?.querySelector(".agent-halt-dot")).toBeNull();
fake.setRoster([entry(1, BoardHaltKind.BLOCKED)]);
await fake.emit(
{
case: "agentBoardChanged",
value: {
agentAccountId: "acc-cook",
board: create(AgentBoardSchema, {
agentAccountId: "acc-cook",
halt: create(BoardHaltSchema, {
kind: BoardHaltKind.BLOCKED,
ref: "RIG-9",
}),
tasks: [create(BoardTaskSchema, { key: "a", title: "A" })],
writtenAtUnixMs: 1n,
}),
},
},
1n,
);
expect(
leaf()?.querySelector(".agent-halt-dot")?.getAttribute("data-halt"),
).toBe("blocked");
expect(count()).toBe("1 waiting on you");
});
});
23 changes: 23 additions & 0 deletions apps/ui/src/components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@ const AgentLeaf: Component<{ agent: Agent; badge?: number }> = (props) => {
<Glyph name="role" />
</span>
</Show>
<Show when={a().board?.halt}>
{(halt) => (
<span
class="agent-halt-dot"
data-halt={halt().kind}
role="img"
title={`Halted: ${halt().kind}${halt().ref ? ` ${halt().ref}` : ""}${halt().reason ? ` — ${halt().reason}` : ""}`}
aria-label={`Halted: ${halt().kind}`}
/>
)}
</Show>
<Show when={a().board?.waitingOnOperator}>
{(count) => (
<span
class="agent-waiting-count"
role="img"
title={`${count()} waiting on you`}
aria-label={`${count()} waiting on you`}
>
{count()}
</span>
)}
</Show>
<Show when={props.badge !== undefined}>
<span class="tree-badge">{props.badge}</span>
</Show>
Expand Down
5 changes: 5 additions & 0 deletions apps/ui/src/components/RightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,11 @@ const FleetPane: Component<{ item: AvatarTabItem }> = (props) => {
>
Open {a().account.handle}'s workspace
</button>
<Show when={(a().board?.waitingOnOperator ?? 0) > 0}>
<span class="fleet-waiting-count">
{a().board?.waitingOnOperator} waiting on you
</span>
</Show>
</div>
<ChannelView channel={homeDm()} />
</div>
Expand Down
178 changes: 178 additions & 0 deletions apps/ui/src/live/adapt.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { describe, expect, test } from "bun:test";
import { fromBinary } from "@bufbuild/protobuf";
import type { Ask as WireAsk } from "@compass/client";
import {
AccountSchema,
AgentAccountSchema,
AgentBoardSchema,
AgentBoardSummarySchema,
AgentPlanEntryStatus,
AgentPresence,
AgentSessionStatusSchema,
AgentToolCallStatus,
AskOptionSchema,
AskQuestionSchema,
AskSchema,
BoardAskSchema,
BoardHaltKind,
BoardHaltSchema,
BoardPrGate,
BoardRefKind,
BoardRefSchema,
BoardSubagentSchema,
BoardTaskSchema,
BoardTaskState,
BoardWaitingOn,
BoardWaitingSchema,
ChannelGroupSchema,
ChannelGroupVisibility,
ChannelKind,
Expand Down Expand Up @@ -37,6 +51,8 @@ import type { IssueState as DomainIssueState } from "../stub-data";
import { type Account, type Agent, agentTree } from "../stub-data";
import {
adaptAccount,
adaptAgentBoard,
adaptAgentBoardSummary,
adaptAsk,
adaptChannel,
adaptChannelGroup,
Expand Down Expand Up @@ -1043,6 +1059,168 @@ describe("adaptIssue", () => {
});
});

describe("adaptAgentBoard", () => {
test("maps board enums, bigint timestamps, and empty optional strings", () => {
const wire = create(AgentBoardSchema, {
agentAccountId: "acc-cook",
handle: "cook",
focusTaskKey: "task-a",
halt: create(BoardHaltSchema, {
kind: BoardHaltKind.UNSPECIFIED,
ref: "",
reason: "",
}),
tasks: [
create(BoardTaskSchema, {
key: "task-a",
title: "Ship the board",
primaryRef: "",
state: BoardTaskState.ACTIVE,
note: "",
waiting: [
create(BoardWaitingSchema, {
on: BoardWaitingOn.OPERATOR,
agentHandle: "",
what: "",
ref: "",
}),
create(BoardWaitingSchema, { on: BoardWaitingOn.UNSPECIFIED }),
],
subagents: [
create(BoardSubagentSchema, {
id: "sub-1",
agent: "builder",
description: "",
status: "working",
startedAtUnixMs: 10n,
endedAtUnixMs: 0n,
}),
],
}),
],
refs: [
create(BoardRefSchema, {
ref: "RIG-12",
kind: BoardRefKind.UNSPECIFIED,
title: "",
url: "",
gate: BoardPrGate.UNSPECIFIED,
stackTipRef: "",
owned: true,
staleApproval: true,
mergeQueueState: "",
trackerStatus: "",
}),
],
asks: [
create(BoardAskSchema, {
askId: "ask-1",
messageId: "msg-1",
channelId: "ch-1",
ref: "",
header: "Pick a fix",
postedAtUnixMs: 77n,
}),
],
writtenAtUnixMs: 1234n,
stale: true,
});
const board = adaptAgentBoard(wire);
expect(board.agentId).toBe("acc-cook");
expect(board.focusTaskKey).toBe("task-a");
expect(board.halt).toBeUndefined();
expect(board.writtenAtUnixMs).toBe(1234);
expect(board.tasks[0]).toMatchObject({
state: "active",
primaryRef: undefined,
note: undefined,
waiting: [
{
on: "operator",
agentHandle: undefined,
what: undefined,
ref: undefined,
},
{ on: undefined },
],
subagents: [{ startedAtUnixMs: 10, endedAtUnixMs: undefined }],
});
expect(board.refs[0]).toMatchObject({
kind: "unknown",
title: undefined,
url: undefined,
gate: undefined,
stackTipRef: undefined,
mergeQueueState: undefined,
trackerStatus: undefined,
});
expect(board.asks).toEqual([
{
askId: "ask-1",
messageId: "msg-1",
channelId: "ch-1",
ref: undefined,
header: "Pick a fix",
answered: false,
postedAtUnixMs: 77,
},
]);
});

test("maps a ref kind from a newer server to unknown", () => {
// Field 2 (kind) = 99: a value this client's enum does not define.
const ref = fromBinary(BoardRefSchema, new Uint8Array([0x10, 0x63]));
const board = adaptAgentBoard(create(AgentBoardSchema, { refs: [ref] }));
expect(board.refs[0]?.kind).toBe("unknown");
});

test("maps wire board summaries", () => {
expect(
adaptAgentBoardSummary(
create(AgentBoardSummarySchema, {
focus: "",
taskCount: 3,
waitingOnOperator: 2,
halt: create(BoardHaltSchema, {
kind: BoardHaltKind.AWAITING_USER,
ref: "RIG-9",
reason: "Need a decision",
}),
writtenAtUnixMs: 100n,
}),
),
).toEqual({
focus: undefined,
taskCount: 3,
waitingOnOperator: 2,
halt: { kind: "awaiting_user", ref: "RIG-9", reason: "Need a decision" },
writtenAtUnixMs: 100,
stale: false,
});
});
});

test("adaptRosterEntry carries the board summary", () => {
const [, info] = adaptRosterEntry(
create(RosterEntrySchema, {
agentAccountId: "acc-cook",
board: create(AgentBoardSummarySchema, {
focus: "Review pull request",
taskCount: 2,
waitingOnOperator: 1,
writtenAtUnixMs: 100n,
}),
}),
);
expect(info.board).toEqual({
focus: "Review pull request",
taskCount: 2,
waitingOnOperator: 1,
halt: undefined,
writtenAtUnixMs: 100,
stale: false,
});
});
describe("presenceLifecycle", () => {
test("maps each modeled AgentPresence to its AgentState dot", () => {
expect(presenceLifecycle(AgentPresence.WORKING)).toBe("working");
Expand Down
Loading
Loading