Skip to content

Commit 05501ed

Browse files
committed
feat: reframe specialist roster from role-first to outcome-first display
Add produces field to SpecialistMeta with concrete output noun-phrases per specialist. Flip chip visual hierarchy so produces pills lead after the name, demoting role text to subtle secondary position. Update section heading from "Your AI Team" to "Your Specialists".
1 parent be91050 commit 05501ed

File tree

5 files changed

+116
-32
lines changed

5 files changed

+116
-32
lines changed

apps/desktop/src/features/agents/specialist-meta.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export interface SpecialistMeta {
44
role: string;
55
/** Outcome-focused "best for" statement shown on dashboard cards. */
66
bestAt: string;
7+
/** Concrete output noun-phrases this specialist helps produce. */
8+
produces: string[];
79
icon: string;
810
starterSuggestions: [string, string, string];
911
}
@@ -13,6 +15,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
1315
name: "CMO",
1416
role: "Top-level marketing lead who routes work, synthesizes across specialists, and helps when you're unsure where to start",
1517
bestAt: "Best for prioritizing your next move, coordinating specialists, and getting cross-channel strategy",
18+
produces: ["Marketing priorities", "Cross-channel strategy", "Specialist coordination"],
1619
icon: "brain",
1720
starterSuggestions: [
1821
"What's the highest-leverage marketing move for my company right now?",
@@ -24,6 +27,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
2427
name: "Market Intel",
2528
role: "Competitor, community, customer-voice, and market-research specialist",
2629
bestAt: "Best for competitor research, community mapping, and understanding customer language",
30+
produces: ["Competitor maps", "Community targets", "Customer language analysis"],
2731
icon: "search",
2832
starterSuggestions: [
2933
"Map my competitor landscape and find messaging gaps.",
@@ -35,6 +39,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
3539
name: "Positioning",
3640
role: "Sharpens how the company should be framed, differentiated, and messaged",
3741
bestAt: "Best for one-liners, differentiation angles, and ICP-specific messaging",
42+
produces: ["Value props", "One-liners", "Category framing"],
3843
icon: "target",
3944
starterSuggestions: [
4045
"Sharpen my one-liner to stand out from competitors.",
@@ -46,6 +51,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
4651
name: "Website Conversion",
4752
role: "Improves the website's ability to convert visitors into users or customers",
4853
bestAt: "Best for homepage hero rewrites, CTA optimization, and conversion audits",
54+
produces: ["Hero rewrites", "CTA rewrites", "Conversion audits"],
4955
icon: "layout",
5056
starterSuggestions: [
5157
"Rewrite my homepage hero with 3 variants.",
@@ -57,6 +63,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
5763
name: "SEO/AEO",
5864
role: "Owns search visibility and answer-engine visibility",
5965
bestAt: "Best for search wedges, comparison pages, and AI-answer opportunities",
66+
produces: ["Search wedges", "Comparison pages", "Answer-engine opportunities"],
6067
icon: "globe",
6168
starterSuggestions: [
6269
"Find my best SEO quick wins by effort vs. impact.",
@@ -68,6 +75,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
6875
name: "Distribution",
6976
role: "Owns launches, communities, directories, and founder-led distribution",
7077
bestAt: "Best for Product Hunt launches, community posts, and channel strategy",
78+
produces: ["Launch packs", "Community targets", "Launch sequencing"],
7179
icon: "megaphone",
7280
starterSuggestions: [
7381
"Create a Product Hunt launch pack.",
@@ -79,6 +87,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
7987
name: "Content",
8088
role: "Owns ongoing founder-led and product-led content production",
8189
bestAt: "Best for founder-led content ideas, editorial briefs, and repurposing plans",
90+
produces: ["Content ideas", "Editorial briefs", "Repurposing plans"],
8291
icon: "pen-tool",
8392
starterSuggestions: [
8493
"Generate 10 founder-led content ideas with hooks.",
@@ -90,6 +99,7 @@ export const SPECIALIST_META: Record<string, SpecialistMeta> = {
9099
name: "Outbound",
91100
role: "Owns direct outreach and messaging sequences",
92101
bestAt: "Best for cold email sequences, subject lines, and segment-specific outreach",
102+
produces: ["Cold outreach sequences", "Subject lines", "Outreach angles"],
93103
icon: "send",
94104
starterSuggestions: [
95105
"Draft a 3-email cold outreach sequence.",

apps/desktop/src/features/dashboard/components/DashboardAgentRoster.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function DashboardAgentRoster({ specialists, onChat }: DashboardAgentRost
1919
<div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-primary/8">
2020
<UsersIcon className="size-3 text-primary" />
2121
</div>
22-
<h2 className="section-label">Your AI Team</h2>
22+
<h2 className="section-label">Your Specialists</h2>
2323
<span className="rounded-full bg-muted/50 px-2 py-0.5 font-mono text-[10px] tabular-nums text-muted-foreground">
2424
{specialists.length}
2525
</span>

apps/desktop/src/features/dashboard/components/DashboardSpecialistChip.tsx

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export function DashboardSpecialistChip({ specialist, onChat }: DashboardSpecial
1414
const isManager = specialist.category === "manager";
1515
const colors = getSpecialistColors(specialist.id);
1616
const meta = getSpecialistMeta(specialist.id);
17-
const exampleJobs = specialist.outputTypes?.slice(0, 2) ?? [];
1817

1918
return (
2019
<button
@@ -64,42 +63,37 @@ export function DashboardSpecialistChip({ specialist, onChat }: DashboardSpecial
6463
</span>
6564
) : null}
6665
</div>
67-
<p className={cn(
68-
"mt-0.5 leading-snug text-muted-foreground/70 line-clamp-1",
69-
isManager ? "text-[12px]" : "text-[11px]",
70-
)}>
71-
{specialist.role}
72-
</p>
7366
</div>
7467
</div>
7568

76-
{/* Best at — outcome-focused description */}
77-
{meta?.bestAt ? (
78-
<p className={cn(
79-
"leading-relaxed text-muted-foreground",
80-
isManager ? "text-[13px]" : "text-[12px]",
81-
)}>
82-
{meta.bestAt}
83-
</p>
84-
) : null}
85-
86-
{/* Example jobs from outputTypes */}
87-
{exampleJobs.length > 0 ? (
88-
<div className={cn(
89-
"flex gap-x-4 gap-y-1.5 rounded-lg border border-border/15 bg-muted/15 px-3 py-2 dark:border-white/[0.03] dark:bg-white/[0.015]",
90-
isManager ? "flex-wrap" : "flex-col",
91-
)}>
92-
{exampleJobs.map((job) => (
93-
<div key={job} className="flex items-center gap-2">
94-
<span className={cn("size-1.5 shrink-0 rounded-full", colors.dotColor)} />
95-
<span className="text-[11px] font-medium leading-tight text-muted-foreground">
96-
{job}
97-
</span>
98-
</div>
69+
{/* Produces — concrete output pills (outcome-first) */}
70+
{meta?.produces && meta.produces.length > 0 ? (
71+
<div className="flex flex-wrap gap-1.5">
72+
{meta.produces.map((output) => (
73+
<span
74+
key={output}
75+
className={cn(
76+
"inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-[11px] font-medium leading-tight",
77+
isManager
78+
? "bg-primary/[0.06] text-primary/80"
79+
: cn("bg-muted/30 dark:bg-white/[0.04]", colors.iconText, "opacity-80"),
80+
)}
81+
>
82+
<span className={cn("size-1 shrink-0 rounded-full", colors.dotColor)} />
83+
{output}
84+
</span>
9985
))}
10086
</div>
10187
) : null}
10288

89+
{/* Role — demoted to subtle secondary text */}
90+
<p className={cn(
91+
"leading-snug text-muted-foreground/50 line-clamp-1",
92+
isManager ? "text-[11px]" : "text-[10px]",
93+
)}>
94+
{specialist.role}
95+
</p>
96+
10397
{/* Chat CTA — always visible, more prominent on hover */}
10498
<div className="mt-auto flex justify-end pt-1">
10599
<span className={cn(

apps/desktop/src/features/dashboard/components/dashboard-agent-roster.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ void test("DashboardAgentRoster: accepts specialists and onChat props", () => {
2222

2323
void test("DashboardAgentRoster: renders section header with team icon", () => {
2424
assert.ok(
25-
src.includes("Your AI Team") || src.includes("Agent Roster"),
25+
src.includes("Your Specialists") || src.includes("Your AI Team") || src.includes("Agent Roster"),
2626
"Expected section header text",
2727
);
2828
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
import { readFileSync } from "node:fs";
4+
import { resolve } from "node:path";
5+
6+
const chipSrc = readFileSync(
7+
resolve(import.meta.dirname, "DashboardSpecialistChip.tsx"),
8+
"utf-8",
9+
);
10+
11+
const metaSrc = readFileSync(
12+
resolve(import.meta.dirname, "../../../features/agents/specialist-meta.ts"),
13+
"utf-8",
14+
);
15+
16+
void test("specialist-meta: SpecialistMeta interface includes produces field", () => {
17+
assert.ok(
18+
metaSrc.includes("produces:") || metaSrc.includes("produces :"),
19+
"Expected 'produces' field in SpecialistMeta interface",
20+
);
21+
});
22+
23+
void test("specialist-meta: every specialist has a produces array", () => {
24+
const specialistIds = [
25+
"cmo",
26+
"market-intel",
27+
"positioning",
28+
"website-conversion",
29+
"seo-aeo",
30+
"distribution",
31+
"content",
32+
"outbound",
33+
];
34+
for (const id of specialistIds) {
35+
// Find the specialist block and check it contains produces
36+
// Keys may be quoted or unquoted depending on whether they contain hyphens
37+
const hasKey = metaSrc.includes(`"${id}":`) || metaSrc.includes(`${id}:`);
38+
assert.ok(
39+
hasKey,
40+
`Expected specialist '${id}' in SPECIALIST_META`,
41+
);
42+
}
43+
// Check that produces appears at least 8 times (once per specialist)
44+
const producesCount = (metaSrc.match(/produces:/g) || []).length;
45+
assert.ok(
46+
producesCount >= 8,
47+
`Expected at least 8 'produces:' entries, found ${producesCount}`,
48+
);
49+
});
50+
51+
void test("DashboardSpecialistChip: renders produces pills from meta", () => {
52+
assert.ok(
53+
chipSrc.includes("produces") || chipSrc.includes("meta?.produces"),
54+
"Expected DashboardSpecialistChip to reference 'produces' field from meta",
55+
);
56+
});
57+
58+
void test("DashboardSpecialistChip: produces pills appear before role text", () => {
59+
// The produces rendering should appear before the role text in the component
60+
const producesIndex = chipSrc.indexOf("produces");
61+
const roleIndex = chipSrc.indexOf("specialist.role");
62+
assert.ok(
63+
producesIndex > 0 && roleIndex > 0 && producesIndex < roleIndex,
64+
"Expected 'produces' rendering to appear before 'specialist.role' in the chip component",
65+
);
66+
});
67+
68+
void test("DashboardSpecialistChip: no longer shows bestAt as primary description", () => {
69+
// bestAt should either not appear or appear only after produces
70+
const bestAtIndex = chipSrc.indexOf("bestAt");
71+
const producesIndex = chipSrc.indexOf("produces");
72+
if (bestAtIndex >= 0) {
73+
assert.ok(
74+
producesIndex < bestAtIndex,
75+
"If bestAt is still rendered, produces should come first",
76+
);
77+
}
78+
// Either way, produces must be present
79+
assert.ok(producesIndex >= 0, "Expected 'produces' to be rendered in chip");
80+
});

0 commit comments

Comments
 (0)