Skip to content

Commit 1935a5b

Browse files
committed
style: add activity status indicator to specialist cards on Agents page
1 parent 3a94c9e commit 1935a5b

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

apps/desktop/src/features/agents/components/SpecialistCard.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,26 @@ interface SpecialistCardProps {
2222
onOutputNavigate?: ((artifact: ArtifactRecord) => void) | undefined;
2323
}
2424

25+
/** Check if any output or bundle was created within the last 24 hours. */
26+
function hasRecentActivity(outputs: ArtifactRecord[], bundles: SpecialistBundleGroup[]): boolean {
27+
const threshold = Date.now() - 24 * 60 * 60 * 1000;
28+
for (const a of outputs) {
29+
if (new Date(a.createdAt).getTime() >= threshold) return true;
30+
}
31+
for (const b of bundles) {
32+
if (new Date(b.createdAt).getTime() >= threshold) return true;
33+
}
34+
return false;
35+
}
36+
2537
export function SpecialistCard({ specialist, onChat, recentOutputs, recentBundles, onOutputNavigate }: SpecialistCardProps) {
2638
const Icon = resolveSpecialistIcon(specialist.icon);
2739
const isManager = specialist.category === "manager";
2840
const colors = getSpecialistColors(specialist.id);
2941
const outputs = recentOutputs?.length ? recentOutputs : [];
3042
const bundles = recentBundles?.length ? recentBundles : [];
3143
const hasOutputs = outputs.length > 0 || bundles.length > 0;
44+
const isActive = hasOutputs && hasRecentActivity(outputs, bundles);
3245

3346
return (
3447
<article
@@ -73,6 +86,11 @@ export function SpecialistCard({ specialist, onChat, recentOutputs, recentBundle
7386
Lead
7487
</span>
7588
) : null}
89+
{!isManager && isActive ? (
90+
<span className="rounded-md bg-emerald-500/10 px-1.5 py-0.5 font-mono text-[10px] font-semibold uppercase tracking-wide text-emerald-600 dark:bg-emerald-400/10 dark:text-emerald-400">
91+
ACTIVE
92+
</span>
93+
) : null}
7694
</div>
7795
<p className={cn(
7896
"mt-0.5 leading-relaxed text-muted-foreground",
@@ -108,6 +126,13 @@ export function SpecialistCard({ specialist, onChat, recentOutputs, recentBundle
108126
))}
109127
</div>
110128

129+
{/* Unused specialist prompt */}
130+
{!hasOutputs && !isManager ? (
131+
<p className="mt-4 font-mono text-[11px] text-muted-foreground/40">
132+
Start your first conversation
133+
</p>
134+
) : null}
135+
111136
{/* Recent outputs — bundles and standalone artifacts */}
112137
{hasOutputs ? (
113138
<div className="mt-4 rounded-lg border border-border/15 bg-muted/20 px-3 py-2.5 dark:border-white/[0.03] dark:bg-white/[0.02]">
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readFileSync } from "node:fs";
3+
import { resolve } from "node:path";
4+
5+
const agentsDir = resolve(
6+
__dirname,
7+
"../../../apps/desktop/src/features/agents",
8+
);
9+
const readSrc = (path: string) =>
10+
readFileSync(resolve(agentsDir, path), "utf-8");
11+
12+
const specialistCardSrc = readSrc("components/SpecialistCard.tsx");
13+
14+
// ---------------------------------------------------------------------------
15+
// Activity status — "ACTIVE" badge for specialists with recent (24h) outputs
16+
// ---------------------------------------------------------------------------
17+
describe("SpecialistCard activity status badge", () => {
18+
it("checks whether outputs are recent (within 24 hours)", () => {
19+
// Should have logic comparing output timestamps to determine recency
20+
expect(specialistCardSrc).toMatch(/24\s*\*\s*60\s*\*\s*60\s*\*\s*1000|86[_,]?400[_,]?000/);
21+
});
22+
23+
it("renders an ACTIVE badge for specialists with recent activity", () => {
24+
expect(specialistCardSrc).toContain("ACTIVE");
25+
});
26+
27+
it("uses emerald accent for the active badge", () => {
28+
// The active badge should use primary/emerald styling
29+
expect(specialistCardSrc).toMatch(/bg-primary|bg-emerald/);
30+
});
31+
32+
it("uses monospace font for the active badge (consistent with LEAD badge)", () => {
33+
// Active badge should use same styling pattern as the LEAD badge
34+
expect(specialistCardSrc).toMatch(/font-mono.*ACTIVE|ACTIVE.*font-mono/s);
35+
});
36+
37+
it("only shows ACTIVE badge on non-manager specialists", () => {
38+
// Should check isManager before rendering the ACTIVE badge
39+
// The ACTIVE badge should be in a conditional that excludes managers
40+
expect(specialistCardSrc).toMatch(/!isManager.*ACTIVE/s);
41+
});
42+
});
43+
44+
// ---------------------------------------------------------------------------
45+
// Activity status — "Start your first conversation" for unused specialists
46+
// ---------------------------------------------------------------------------
47+
describe("SpecialistCard unused specialist prompt", () => {
48+
it("renders a prompt for unused specialists", () => {
49+
expect(specialistCardSrc).toMatch(/Start your first conversation/i);
50+
});
51+
52+
it("uses muted styling for the unused prompt", () => {
53+
// Should use muted-foreground or similar subdued styling
54+
expect(specialistCardSrc).toMatch(/text-muted-foreground/);
55+
});
56+
57+
it("uses monospace font for the unused prompt", () => {
58+
expect(specialistCardSrc).toMatch(/font-mono.*Start your first conversation|Start your first conversation.*font-mono/si);
59+
});
60+
61+
it("only shows unused prompt when there are no outputs (not on managers)", () => {
62+
// Should conditionally render based on hasOutputs being false
63+
expect(specialistCardSrc).toMatch(/!hasOutputs|hasOutputs.*false/s);
64+
});
65+
});

0 commit comments

Comments
 (0)