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:
@ -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}`}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user