feat(workstate): panneau Work state encore plus compact — intention tronquée + badge indicateur, ligne summary repliée supprimée — #235 (QA verte)

This commit is contained in:
2026-08-23 15:38:41 +02:00
parent a02f3a567d
commit a3b1c845ed
2 changed files with 92 additions and 21 deletions

View File

@ -159,6 +159,28 @@ function backgroundTaskSummary(tasks: BackgroundCompletion[]): string | null {
return `${tasks.length} background task${tasks.length > 1 ? "s" : ""}`; return `${tasks.length} background task${tasks.length > 1 ? "s" : ""}`;
} }
function compactIntent(text: string | null): string | null {
const normalized = text?.replace(/\s+/g, " ").trim();
if (!normalized) return null;
return normalized.length <= 44 ? normalized : `${normalized.slice(0, 41)}...`;
}
function compactIndicator({
tickets,
inboxDepth,
backgroundTasks,
}: {
tickets: AgentTicketState[];
inboxDepth: number;
backgroundTasks: BackgroundCompletion[];
}): string | null {
const queuedTickets = tickets.filter((ticket) => ticket.status === "queued").length;
if (queuedTickets > 0) return `+${queuedTickets}`;
if (inboxDepth > 0) return `${inboxDepth} inbox`;
if (backgroundTasks.length > 0) return `${backgroundTasks.length} bg`;
return null;
}
function requesterLabel(ticket: AgentTicketState): string { function requesterLabel(ticket: AgentTicketState): string {
if (ticket.source.kind === "human") return "Human"; if (ticket.source.kind === "human") return "Human";
const id = shortTicket(ticket.source.agentId); const id = shortTicket(ticket.source.agentId);
@ -592,16 +614,13 @@ function AgentRow({
const currentTurn = const currentTurn =
busy || tickets.some((ticket) => ticket.status === "inProgress"); busy || tickets.some((ticket) => ticket.status === "inProgress");
const activeTicket = tickets.find((ticket) => ticket.status === "inProgress"); const activeTicket = tickets.find((ticket) => ticket.status === "inProgress");
const activeSummary = activeTicket
? conversations.get(activeTicket.conversationId)
: undefined;
const statusLabel = agentStatusLabel(live, busy); const statusLabel = agentStatusLabel(live, busy);
const taskSummary = backgroundTaskSummary(backgroundTasks); const taskSummary = backgroundTaskSummary(backgroundTasks);
const activityLine = const activityLine = compactIntent(
activeTicket?.taskPreview ?? activeTicket?.taskPreview ??
(busyState.state === "busy" ? `Ticket ${shortTicket(busyState.ticket)}` : null) ?? (busyState.state === "busy" ? `Ticket ${shortTicket(busyState.ticket)}` : null),
(inboxDepth > 0 ? `${inboxDepth} queued` : null) ?? );
taskSummary; const indicator = compactIndicator({ tickets, inboxDepth, backgroundTasks });
const hasActions = Boolean(agent.live); const hasActions = Boolean(agent.live);
const hasDetails = const hasDetails =
hasActions || hasActions ||
@ -665,18 +684,26 @@ function AgentRow({
return ( return (
<li className="py-2 first:pt-0 last:pb-0"> <li className="py-2 first:pt-0 last:pb-0">
<div className="flex min-w-0 items-start justify-between gap-3"> <div className="flex min-w-0 items-center justify-between gap-2">
<span className="min-w-0 flex-1"> <span className="flex min-w-0 flex-1 items-baseline gap-2">
<span className="block truncate text-sm font-semibold text-content"> <span className="min-w-0 truncate text-sm font-semibold text-content">
{agent.name} {agent.name}
</span> </span>
{activityLine && ( {activityLine && (
<span className="mt-0.5 block truncate text-xs text-muted"> <span
className="min-w-0 truncate text-xs text-muted"
title={activeTicket?.taskPreview ?? activityLine}
>
{activityLine} {activityLine}
</span> </span>
)} )}
</span> </span>
<span className="flex shrink-0 items-center gap-2"> <span className="flex shrink-0 items-center gap-1.5">
{indicator && (
<span className="rounded-full bg-raised px-1.5 py-0.5 text-[11px] font-medium text-muted">
{indicator}
</span>
)}
<span <span
className={cn( className={cn(
"rounded-full px-2 py-0.5 text-xs font-medium", "rounded-full px-2 py-0.5 text-xs font-medium",
@ -699,13 +726,6 @@ function AgentRow({
</span> </span>
</div> </div>
{!detailsOpen && activeSummary && (
<ConversationSummaryLine
summary={activeSummary}
onOpenConversation={onOpenConversation}
/>
)}
{detailsOpen && ( {detailsOpen && (
<div <div
id={`work-agent-details-${agent.agentId}`} id={`work-agent-details-${agent.agentId}`}

View File

@ -149,6 +149,47 @@ describe("ProjectWorkStatePanel", () => {
expect(within(list).getByLabelText("9 more characters")).toBeTruthy(); expect(within(list).getByLabelText("9 more characters")).toBeTruthy();
}); });
it("keeps long ticket intent out of the default agent row", async () => {
const longPreview =
"Goal: inspect the entire repository and produce a verbose implementation summary";
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-long",
name: "Git",
profileId: "codex",
busy: {
state: "busy",
ticket: "ticket-long-abcdef",
sinceMs: 789,
},
tickets: [
{
ticketId: "ticket-long-abcdef",
conversationId: "conversation-long",
position: 0,
status: "inProgress",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: longPreview,
taskLen: longPreview.length,
},
],
},
],
});
renderPanel(workState);
expect(await screen.findByText("Git")).toBeTruthy();
expect(screen.queryByText(longPreview)).toBeNull();
expect(screen.getByText("Goal: inspect the entire repository and p...")).toBeTruthy();
await openAgentDetails("Git");
const list = await screen.findByLabelText("Git tickets");
expect(within(list).getByText(longPreview)).toBeTruthy();
});
it("renders two tickets in FIFO order", async () => { it("renders two tickets in FIFO order", async () => {
const workState = new MockWorkStateGateway(); const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, { workState._setProjectWorkState(PROJECT_ID, {
@ -291,7 +332,9 @@ describe("ProjectWorkStatePanel", () => {
renderPanel(workState); renderPanel(workState);
expect(await screen.findByText("6 background tasks (3 active, 3 failed)")).toBeTruthy(); expect(await screen.findByText("Runner")).toBeTruthy();
expect(screen.getByText("6 bg")).toBeTruthy();
expect(screen.queryByText("6 background tasks (3 active, 3 failed)")).toBeNull();
expect(screen.queryByLabelText("Runner background tasks")).toBeNull(); expect(screen.queryByLabelText("Runner background tasks")).toBeNull();
const list = await openBackgroundTasks("Runner"); const list = await openBackgroundTasks("Runner");
const cancelButtons = within(list).getAllByRole("button", { name: "Cancel" }); const cancelButtons = within(list).getAllByRole("button", { name: "Cancel" });
@ -378,7 +421,9 @@ describe("ProjectWorkStatePanel", () => {
renderPanel(workState); renderPanel(workState);
expect(await screen.findByText("4 background tasks (1 active, 1 failed)")).toBeTruthy(); expect(await screen.findByText("Reporter")).toBeTruthy();
expect(screen.getByText("4 bg")).toBeTruthy();
expect(screen.queryByText("4 background tasks (1 active, 1 failed)")).toBeNull();
expect(screen.queryByLabelText("Reporter background tasks")).toBeNull(); expect(screen.queryByLabelText("Reporter background tasks")).toBeNull();
const list = await openBackgroundTasks("Reporter"); const list = await openBackgroundTasks("Reporter");
// Terminal tasks with a summary render it discreetly. // Terminal tasks with a summary render it discreetly.
@ -604,6 +649,12 @@ describe("ProjectWorkStatePanel", () => {
expect(await screen.findByText("Continue implementation")).toBeTruthy(); expect(await screen.findByText("Continue implementation")).toBeTruthy();
expect(screen.queryByLabelText("Summarizer tickets")).toBeNull(); expect(screen.queryByLabelText("Summarizer tickets")).toBeNull();
expect(screen.queryByText("Goal: Ship the Work panel summaries")).toBeNull();
expect(
screen.queryByText("Previous turn established the adapter boundary."),
).toBeNull();
expect(screen.queryByRole("button", { name: "View" })).toBeNull();
expect(screen.queryByRole("button", { name: "Copy" })).toBeNull();
await openAgentDetails("Summarizer"); await openAgentDetails("Summarizer");
const list = await screen.findByLabelText("Summarizer tickets"); const list = await screen.findByLabelText("Summarizer tickets");
expect(within(list).getAllByText("Summary")).toHaveLength(2); expect(within(list).getAllByText("Summary")).toHaveLength(2);