+
{visibleRuns.map((run) => (
visibleIssueIds[index] === run.issueId && query.isError)}
className={cardClassName}
/>
))}
)}
- {showMoreLink && hiddenRunCount > 0 && (
+ {showMoreLink && runs.length > 0 && (
- {hiddenRunCount} more active/recent run{hiddenRunCount === 1 ? "" : "s"}
+ {hiddenRunCount > 0
+ ? `${hiddenRunCount} more active/recent run${hiddenRunCount === 1 ? "" : "s"}`
+ : "View all runs"}
)}
@@ -163,88 +148,94 @@ export function ActiveAgentsPanel({
);
}
-const AgentRunCard = memo(function AgentRunCard({
+export const AgentRunCard = memo(function AgentRunCard({
companyId,
run,
issue,
- transcript,
- hasOutput,
- isActive,
+ transcript = EMPTY_TRANSCRIPT,
+ hasOutput = false,
+ showTranscript = false,
+ issueLoadFailed = false,
className,
}: {
companyId: string;
run: LiveRunForIssue;
- issue?: Issue;
- transcript: TranscriptEntry[];
- hasOutput: boolean;
- isActive: boolean;
+ issue?: Pick
;
+ transcript?: TranscriptEntry[];
+ hasOutput?: boolean;
+ showTranscript?: boolean;
+ issueLoadFailed?: boolean;
className?: string;
}) {
+ const statusLabel = runStatusLabels[run.status] ?? run.status.replace(/[_-]/g, " ");
+ const runUrl = `/agents/${run.agentId}/runs/${run.id}`;
+ const timestamp = run.finishedAt
+ ? `Finished ${relativeTime(run.finishedAt)}`
+ : run.startedAt ? `Started ${relativeTime(run.startedAt)}` : `Queued ${relativeTime(run.createdAt)}`;
+ const taskTitle = issue?.title ?? (issueLoadFailed ? "Task unavailable" : "Loading task…");
+
return (
-
-
-
-
- {isActive && (!run.execution || run.execution.phase === "working") ? (
-
-
-
-
- ) : (
-
- )}
-
-
-
- {isActive ? "Working" : run.finishedAt ? `Finished ${relativeTime(run.finishedAt)}` : `Started ${relativeTime(run.createdAt)}`}
-
-
+ )} data-run-status={run.status}>
+
+
+
+
+ {run.issueId ? (
-
+
+
+
+ {taskTitle}
+
+ {issue?.identifier ?? run.issueId.slice(0, 8)}
+
+
+ ) : (
+
+
+ {run.invocationSource === "timer" ? "Scheduled heartbeat" : "No linked task"}
-
-
- {run.issueId && (
-
-
- {issue?.identifier ?? run.issueId.slice(0, 8)}
- {issue?.title ? ` - ${issue.title}` : ""}
-
- {issue?.activeRecoveryAction ? (
-
-
-
- ) : null}
-
)}
+
-
-
-
+ {showTranscript && (
+
+
+
+ )}
);
});
diff --git a/ui/src/components/ActivityCharts.tsx b/ui/src/components/ActivityCharts.tsx
index cc72236270..104e94cbea 100644
--- a/ui/src/components/ActivityCharts.tsx
+++ b/ui/src/components/ActivityCharts.tsx
@@ -20,9 +20,9 @@ function emptyRunDay(date: string): DashboardRunActivityDay {
}
const runSegmentColors = {
- succeeded: "var(--hex-10b981)",
+ succeeded: "var(--status-task-icon-done)",
recovered: "var(--status-task-todo)",
- failed: "var(--hex-ef4444)",
+ failed: "var(--status-task-icon-blocked)",
other: "var(--hex-737373)",
} as const;
@@ -166,7 +166,7 @@ export function RunActivityChart(props: RunChartProps) {
}
const priorityColors: Record
= {
- critical: "var(--hex-ef4444)",
+ critical: "var(--status-task-icon-blocked)",
high: "var(--hex-f97316)",
medium: "var(--hex-eab308)",
low: "var(--hex-6b7280)",
@@ -223,14 +223,15 @@ export function PriorityChart({ issues }: { issues: { priority: string; createdA
// status vocabulary; badge, row, chart, and log agree). Previously an
// independent palette (todo blue, in_progress violet, etc.). `backlog`
// deliberately keeps --project-none (pre-B5, per user ruling); the
-// priority series and success-rate tints below are not status hues and
-// are left alone.
+// non-red priority series and warning success-rate tints retain their own hues.
+// Progress, done, and blocked use the icon hues so bars and legends match
+// the task icons in each theme.
const statusColors: Record = {
todo: "var(--status-task-todo)",
- in_progress: "var(--status-task-in_progress)",
+ in_progress: "var(--status-task-icon-in_progress)",
in_review: "var(--status-task-in_review)",
- done: "var(--status-task-done)",
- blocked: "var(--status-task-blocked)",
+ done: "var(--status-task-icon-done)",
+ blocked: "var(--status-task-icon-blocked)",
cancelled: "var(--status-task-cancelled)",
backlog: "var(--project-none)",
};
@@ -309,7 +310,7 @@ export function SuccessRateChart(props: RunChartProps) {
// rather than dragging it down as failures.
const effectiveSucceeded = entry.succeeded + entry.recovered;
const rate = entry.total > 0 ? effectiveSucceeded / entry.total : 0;
- const color = entry.total === 0 ? undefined : rate >= 0.8 ? "var(--hex-10b981)" : rate >= 0.5 ? "var(--hex-eab308)" : "var(--hex-ef4444)";
+ const color = entry.total === 0 ? undefined : rate >= 0.8 ? "var(--status-task-icon-done)" : rate >= 0.5 ? "var(--hex-eab308)" : "var(--status-task-icon-blocked)";
return (
0 ? Math.round(rate * 100) : 0}% (${effectiveSucceeded}/${entry.total})`}>
{entry.total > 0 ? (
diff --git a/ui/src/components/ActivityRow.tsx b/ui/src/components/ActivityRow.tsx
index 13d335e854..f2ca58a21c 100644
--- a/ui/src/components/ActivityRow.tsx
+++ b/ui/src/components/ActivityRow.tsx
@@ -53,27 +53,44 @@ export function ActivityRow({ event, agentMap, userProfileMap, entityNameMap, en
const inner = (
-
-
-
- {actorAvatarUrl && }
- {deriveInitials(actorName)}
-
-
- {actorName}
- {verb}
- {name && {name}}
- {entityTitle && — {entityTitle}}
-
+
+
+ {actorAvatarUrl && }
+ {deriveInitials(actorName)}
+
+
+
+
+
+ {actorName}{" "}
+ {verb}
+
+ {event.entityType === "issue" ? (
+ {entityTitle}
+ ) : (
+
+ {name && {name}}
+ {entityTitle && — {entityTitle}}
+
+ )}
+
+
+ {event.entityType === "issue" ? name : null}
+
+
+
+
+ {timeAgo(event.createdAt)}
+
+
-
{timeAgo(event.createdAt)}
);
const classes = cn(
- "px-4 py-2 text-sm",
+ "dashboard-list-row text-sm",
link && "cursor-pointer hover:bg-accent/50 transition-colors",
className,
);
diff --git a/ui/src/components/BreadcrumbBar.tsx b/ui/src/components/BreadcrumbBar.tsx
index e51f942b65..b123da3ea7 100644
--- a/ui/src/components/BreadcrumbBar.tsx
+++ b/ui/src/components/BreadcrumbBar.tsx
@@ -24,7 +24,7 @@ type GlobalToolbarContext = { companyId: string | null; companyPrefix: string |
function CrumbIdentifier({ identifier }: { identifier?: string }) {
if (!identifier) return null;
return (
-
+
{identifier}
);
@@ -113,9 +113,9 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?:
return (
{menuButton}
-
+
{currentCrumb.leading ? (
- {currentCrumb.leading}
+ {currentCrumb.leading}
) : null}
{currentCrumb.label}
@@ -137,9 +137,9 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?:
{isLast || !crumb.href ? (
crumb.leading || crumb.identifier ? (
-
+
{crumb.leading && (
- {crumb.leading}
+ {crumb.leading}
)}
{!taskDetailLayout ? : null}
{crumb.label}
@@ -154,12 +154,12 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?:
{crumb.leading && (
- {crumb.leading}
+ {crumb.leading}
)}
{!taskDetailLayout ? : null}
{crumb.label}
@@ -194,9 +194,9 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?:
{menuButton}
{breadcrumbs[0].leading || breadcrumbs[0].identifier ? (
-
+
{breadcrumbs[0].leading && (
- {breadcrumbs[0].leading}
+ {breadcrumbs[0].leading}
)}
{breadcrumbs[0].label}
diff --git a/ui/src/components/CompanySettingsSidebar.test.tsx b/ui/src/components/CompanySettingsSidebar.test.tsx
index 111f60690d..7a3418dcc8 100644
--- a/ui/src/components/CompanySettingsSidebar.test.tsx
+++ b/ui/src/components/CompanySettingsSidebar.test.tsx
@@ -144,8 +144,7 @@ describe("CompanySettingsSidebar", () => {
expect(container.textContent).not.toContain("Settings");
expect(container.querySelector('[aria-label="Back from Settings"]')).toBeNull();
const settingsSurface = container.querySelector('[data-contextual-sidebar="settings"]');
- expect(settingsSurface?.classList).toContain("bg-border/50");
- expect(settingsSurface?.classList).toContain("dark:bg-muted");
+ expect(settingsSurface?.classList).toContain("primary-sidebar-surface");
expect(container.querySelector('[data-slot="contextual-sidebar-nav"]')?.className).toBe(
primarySidebarStyles.nav,
);
diff --git a/ui/src/components/Sidebar.test.tsx b/ui/src/components/Sidebar.test.tsx
index 970709436f..bd71c36ff9 100644
--- a/ui/src/components/Sidebar.test.tsx
+++ b/ui/src/components/Sidebar.test.tsx
@@ -173,8 +173,7 @@ describe("Sidebar", () => {
const sidebar = container.querySelector("aside");
expect(sidebar?.classList).not.toContain("border-r");
expect(sidebar?.classList).not.toContain("border-border");
- expect(sidebar?.classList).toContain("bg-border/50");
- expect(sidebar?.classList).toContain("dark:bg-muted");
+ expect(sidebar?.classList).toContain("primary-sidebar-surface");
flushSync(() => {
root.unmount();
diff --git a/ui/src/components/SidebarNavItem.tsx b/ui/src/components/SidebarNavItem.tsx
index e918ed3540..efbcfa4e56 100644
--- a/ui/src/components/SidebarNavItem.tsx
+++ b/ui/src/components/SidebarNavItem.tsx
@@ -182,7 +182,7 @@ export function SidebarNavItem({
)}
{!rail && (hasLive || liveAccessory) && (
-
+
{liveAccessory}
{hasLive && (
<>
diff --git a/ui/src/components/SidebarRecentTasks.tsx b/ui/src/components/SidebarRecentTasks.tsx
index be9797bbf4..3bcaf90380 100644
--- a/ui/src/components/SidebarRecentTasks.tsx
+++ b/ui/src/components/SidebarRecentTasks.tsx
@@ -250,11 +250,11 @@ function RecentTasksList({
<>
{entries.map((entry) => (
-
+
{!rail ? (
@@ -265,7 +265,7 @@ function RecentTasksList({
variant="ghost"
size="icon-xs"
aria-label={`More actions for ${entry.title}`}
- className="absolute right-2 top-(--pct-50) z-10 -translate-y-(--pct-50) text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 pointer-coarse:opacity-100 group-hover/recent-task:opacity-100 group-focus-within/recent-task:opacity-100 data-[state=open]:bg-accent data-[state=open]:text-foreground data-[state=open]:opacity-100"
+ className="sidebar-action-menu absolute right-2 top-(--pct-50) z-10 -translate-y-(--pct-50) text-muted-foreground pointer-events-none opacity-0 transition-opacity hover:bg-sidebar-accent dark:hover:bg-sidebar-accent hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100 pointer-coarse:before:hidden group-hover/recent-task:pointer-events-auto group-hover/recent-task:opacity-100 group-focus-within/recent-task:pointer-events-auto group-focus-within/recent-task:opacity-100 data-[state=open]:pointer-events-auto data-[state=open]:bg-sidebar-accent data-[state=open]:text-foreground data-[state=open]:opacity-100"
>
diff --git a/ui/src/components/StatusGlyph.test.tsx b/ui/src/components/StatusGlyph.test.tsx
index a7ff3fbbcd..f5a92e4957 100644
--- a/ui/src/components/StatusGlyph.test.tsx
+++ b/ui/src/components/StatusGlyph.test.tsx
@@ -16,7 +16,7 @@ import { taskStatusIconVar } from "../lib/status-colors";
const STATUS_ICON_CLASS: Record
= {
backlog: "lucide-circle-dashed",
todo: "lucide-circle",
- in_progress: "lucide-rotate-cw",
+ in_progress: "lucide-task-progress-spinner",
in_review: "lucide-circle-dot",
done: "lucide-circle-check",
blocked: "lucide-circle-minus",
@@ -29,14 +29,17 @@ describe("StatusGlyph", () => {
for (const status of Object.keys(taskStatusIconVar)) {
const html = renderToStaticMarkup();
expect(html).toContain('viewBox="0 0 24 24"');
+ expect(html).toContain('stroke-width="2"');
expect(html).toContain("