@polpo-ai/dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +44 -0
  3. package/app/(dashboard)/error.tsx +47 -0
  4. package/app/(dashboard)/layout.tsx +41 -0
  5. package/app/(dashboard)/loading.tsx +30 -0
  6. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
  7. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
  8. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
  9. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
  10. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
  11. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
  12. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
  13. package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
  14. package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
  15. package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
  16. package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
  17. package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
  18. package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
  19. package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
  20. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
  21. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
  22. package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
  23. package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
  24. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
  25. package/app/(dashboard)/projects/[id]/page.tsx +68 -0
  26. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
  27. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
  28. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
  29. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
  30. package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
  31. package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
  32. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
  33. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
  34. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
  35. package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
  36. package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
  37. package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
  38. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
  39. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
  40. package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
  41. package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
  42. package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
  43. package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
  44. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
  45. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
  46. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
  47. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
  48. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
  49. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
  50. package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
  51. package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
  52. package/app/(dashboard)/projects/[id]/view.tsx +421 -0
  53. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
  54. package/app/(dashboard)/projects/projects-list.tsx +126 -0
  55. package/app/(playground)/error.tsx +42 -0
  56. package/app/(playground)/layout.tsx +27 -0
  57. package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
  58. package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
  59. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
  60. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
  61. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
  62. package/app/favicon.ico +0 -0
  63. package/app/globals.css +385 -0
  64. package/app/icon.svg +6 -0
  65. package/app/layout.tsx +39 -0
  66. package/app/page.tsx +11 -0
  67. package/components/ai-elements/code-block.tsx +562 -0
  68. package/components/dashboard/agent-capabilities.tsx +326 -0
  69. package/components/dashboard/agent-identity-header.tsx +50 -0
  70. package/components/dashboard/agent-model-picker.tsx +195 -0
  71. package/components/dashboard/agent-model-selector.tsx +121 -0
  72. package/components/dashboard/agent-picker-card.tsx +46 -0
  73. package/components/dashboard/agent-studio.tsx +242 -0
  74. package/components/dashboard/agents-table.tsx +48 -0
  75. package/components/dashboard/breadcrumb.tsx +70 -0
  76. package/components/dashboard/builder-chat.tsx +418 -0
  77. package/components/dashboard/chat-shell.tsx +40 -0
  78. package/components/dashboard/client-picker.tsx +63 -0
  79. package/components/dashboard/command-snippet.tsx +77 -0
  80. package/components/dashboard/connect-dialog.tsx +680 -0
  81. package/components/dashboard/copy-button.tsx +45 -0
  82. package/components/dashboard/copy-card.tsx +48 -0
  83. package/components/dashboard/file-browser.tsx +161 -0
  84. package/components/dashboard/hint.tsx +22 -0
  85. package/components/dashboard/inference-mode.tsx +15 -0
  86. package/components/dashboard/manual-refresh-button.tsx +52 -0
  87. package/components/dashboard/markdown.tsx +31 -0
  88. package/components/dashboard/mcp-install-panel.tsx +196 -0
  89. package/components/dashboard/mission-graph.tsx +262 -0
  90. package/components/dashboard/nav-tabs.tsx +86 -0
  91. package/components/dashboard/polpo-chat.tsx +468 -0
  92. package/components/dashboard/project-copilot.tsx +253 -0
  93. package/components/dashboard/sdk-snippet-panel.tsx +194 -0
  94. package/components/dashboard/section-header.tsx +37 -0
  95. package/components/dashboard/settings-form.tsx +2477 -0
  96. package/components/dashboard/sidebar.tsx +385 -0
  97. package/components/dashboard/skeletons.tsx +1054 -0
  98. package/components/dashboard/skills-install-wizard.tsx +273 -0
  99. package/components/dashboard/swarm-runs.tsx +316 -0
  100. package/components/dashboard/tab-toggle.tsx +35 -0
  101. package/components/dashboard/task-studio.tsx +224 -0
  102. package/components/dashboard/top-header.tsx +203 -0
  103. package/components/dashboard/webhook-deliveries.tsx +234 -0
  104. package/components/icons/coding-agents.tsx +151 -0
  105. package/components/json-ld.tsx +8 -0
  106. package/components/providers.tsx +25 -0
  107. package/components/ui/badge.tsx +52 -0
  108. package/components/ui/button.tsx +60 -0
  109. package/components/ui/card.tsx +103 -0
  110. package/components/ui/chart.tsx +356 -0
  111. package/components/ui/command.tsx +196 -0
  112. package/components/ui/dialog.tsx +157 -0
  113. package/components/ui/input-group.tsx +158 -0
  114. package/components/ui/input.tsx +20 -0
  115. package/components/ui/multi-select.tsx +161 -0
  116. package/components/ui/popover.tsx +90 -0
  117. package/components/ui/select.tsx +201 -0
  118. package/components/ui/separator.tsx +25 -0
  119. package/components/ui/sheet.tsx +135 -0
  120. package/components/ui/skeleton.tsx +13 -0
  121. package/components/ui/tabs.tsx +56 -0
  122. package/components/ui/textarea.tsx +18 -0
  123. package/components/ui/tooltip.tsx +66 -0
  124. package/hooks/use-desktop-sidebar.tsx +50 -0
  125. package/hooks/use-mobile-sidebar.tsx +37 -0
  126. package/index.ts +14 -0
  127. package/lib/api.ts +194 -0
  128. package/lib/builder-context.ts +60 -0
  129. package/lib/data-client.ts +68 -0
  130. package/lib/get-query-client.ts +25 -0
  131. package/lib/polpo-client.tsx +49 -0
  132. package/lib/tool-catalog.ts +234 -0
  133. package/lib/use-event-catalog.ts +28 -0
  134. package/lib/utils.ts +6 -0
  135. package/package.json +99 -0
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "#/components/ui/select";
4
+
5
+ /**
6
+ * Agent picker styled as a card: pill label + select dropdown.
7
+ * Shared across ConnectDialog SDK tab and OnboardingChecklist chat step.
8
+ */
9
+ export function AgentPickerCard({
10
+ agents,
11
+ agent,
12
+ onChange,
13
+ show,
14
+ }: {
15
+ agents: string[] | null;
16
+ agent: string;
17
+ onChange: (v: string) => void;
18
+ show: boolean;
19
+ }) {
20
+ if (!show) return null;
21
+ return (
22
+ <div className="flex flex-col gap-2 border border-border bg-background p-3">
23
+ <span className="inline-flex h-5 w-fit items-center rounded bg-secondary px-2 text-[11px] font-medium text-muted-foreground">
24
+ agent
25
+ </span>
26
+ {agents && agents.length > 1 ? (
27
+ <Select value={agent} onValueChange={(v) => { if (v) onChange(v); }}>
28
+ <SelectTrigger className="w-full font-mono text-sm">
29
+ <SelectValue />
30
+ </SelectTrigger>
31
+ <SelectContent>
32
+ {agents.map((name) => (
33
+ <SelectItem key={name} value={name}>
34
+ {name}
35
+ </SelectItem>
36
+ ))}
37
+ </SelectContent>
38
+ </Select>
39
+ ) : (
40
+ <p className="font-mono text-sm leading-6 text-foreground">
41
+ {agents === null ? "Loading…" : agent}
42
+ </p>
43
+ )}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,242 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Agent Studio — the agent detail surface, rendered as a chat canvas:
5
+ * left = the full agent editor (Models / Prompt / Tools / Skills /
6
+ * Memory / Vault, client-switched tabs), right = the Meta Agent builder
7
+ * chat that opens/closes (collapses by width → stays mounted, no reset).
8
+ *
9
+ * This IS the agent detail page (`/projects/[id]/agents/[name]`). Builder
10
+ * closed = plain detail; "Edit" reveals the chat alongside.
11
+ */
12
+
13
+ import { useEffect, useState } from "react";
14
+ import Link from "next/link";
15
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
16
+ import {
17
+ Bot,
18
+ Cpu,
19
+ BookMarked,
20
+ Wrench,
21
+ FileText,
22
+ Brain,
23
+ Lock,
24
+ Users,
25
+ MessageSquare,
26
+ MessagesSquare,
27
+ ExternalLink,
28
+ type LucideIcon,
29
+ } from "lucide-react";
30
+ import type { AgentConfig } from "@polpo-ai/core";
31
+ import { BuilderChat } from "#/components/dashboard/builder-chat";
32
+ import { AgentIdentityHeader } from "#/components/dashboard/agent-identity-header";
33
+ import { Breadcrumb } from "#/components/dashboard/breadcrumb";
34
+ import { useCopilot } from "#/components/dashboard/project-copilot";
35
+ import AgentModelsView from "#/app/(dashboard)/projects/[id]/agents/[name]/models-view";
36
+ import AgentSkillsView from "#/app/(dashboard)/projects/[id]/agents/[name]/skills/view";
37
+ import AgentToolsView from "#/app/(dashboard)/projects/[id]/agents/[name]/tools/view";
38
+ import AgentPromptView from "#/app/(dashboard)/projects/[id]/agents/[name]/prompt/view";
39
+ import AgentMemoryView from "#/app/(dashboard)/projects/[id]/agents/[name]/memory/view";
40
+ import AgentVaultView from "#/app/(dashboard)/projects/[id]/agents/[name]/vault/view";
41
+
42
+ export interface VaultEntry {
43
+ service: string;
44
+ type: string;
45
+ label: string | null;
46
+ }
47
+
48
+ type StudioTab = "models" | "prompt" | "tools" | "skills" | "memory" | "vault";
49
+
50
+ const TABS: { key: StudioTab; label: string; icon: LucideIcon }[] = [
51
+ { key: "models", label: "Models", icon: Cpu },
52
+ { key: "prompt", label: "Instructions", icon: FileText },
53
+ { key: "tools", label: "Tools", icon: Wrench },
54
+ { key: "skills", label: "Skills", icon: BookMarked },
55
+ { key: "memory", label: "Memory", icon: Brain },
56
+ { key: "vault", label: "Vault", icon: Lock },
57
+ ];
58
+
59
+ export function AgentStudio({
60
+ projectId,
61
+ apiUrl,
62
+ agent,
63
+ memoryContent,
64
+ vaultEntries,
65
+ }: {
66
+ projectId: string;
67
+ apiUrl: string;
68
+ agent: AgentConfig | null;
69
+ memoryContent: string;
70
+ vaultEntries: VaultEntry[];
71
+ }) {
72
+ const router = useRouter();
73
+ const copilot = useCopilot();
74
+ // Soft-disabled: the in-Studio docked builder stays in the tree but never
75
+ // opens — "Edit" now opens the global lateral Copilot (Ask Polpo) instead.
76
+ const chatOpen = false;
77
+
78
+ if (!agent) {
79
+ return (
80
+ <div className="border border-border p-10 text-center">
81
+ <Bot className="mx-auto h-5 w-5 text-muted-foreground/40" strokeWidth={1.5} />
82
+ <p className="mt-2 text-sm text-muted-foreground">Agent not found.</p>
83
+ <Link
84
+ href={`/projects/${projectId}/agents`}
85
+ className="mt-3 inline-block text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
86
+ >
87
+ Back to agents
88
+ </Link>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ // Fills the viewport (no page scroll); only inner panes scroll.
95
+ <div className="flex h-[calc(100vh-7rem)] min-h-[520px] flex-col overflow-hidden">
96
+ {/* Top bar — breadcrumb + actions. Fixed (does not scroll). */}
97
+ <div className="flex shrink-0 items-center gap-2 pb-3">
98
+ <Breadcrumb
99
+ items={[
100
+ { label: "Agents", href: `/projects/${projectId}/agents`, icon: Users },
101
+ { label: agent.name },
102
+ ]}
103
+ />
104
+
105
+ <div className="ml-auto flex items-center gap-2">
106
+ {/* Test = chat with the live agent in the playground (new tab). */}
107
+ <Link
108
+ href={`/projects/${projectId}/playground?agent=${encodeURIComponent(agent.name)}`}
109
+ target="_blank"
110
+ rel="noopener noreferrer"
111
+ className="inline-flex items-center gap-1.5 border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
112
+ >
113
+ <MessageSquare className="h-3.5 w-3.5" />
114
+ Test
115
+ <ExternalLink className="h-3 w-3 text-muted-foreground/60" />
116
+ </Link>
117
+ {/* Edit = open the global lateral Copilot (Polpo AI), scoped to
118
+ this agent via the route context. Hidden once it's open. */}
119
+ {!copilot?.open && (
120
+ <button
121
+ type="button"
122
+ onClick={() => copilot?.setOpen(true)}
123
+ className="inline-flex items-center gap-1.5 border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
124
+ >
125
+ <MessagesSquare className="h-3.5 w-3.5" strokeWidth={1.5} />
126
+ Edit
127
+ </button>
128
+ )}
129
+ </div>
130
+ </div>
131
+
132
+ {/* Split — content LEFT, builder chat RIGHT. The chat collapses by
133
+ WIDTH (stays mounted → no reset). A full-height vertical
134
+ separator divides the panes when the builder is open. */}
135
+ <div className="flex min-h-0 flex-1 overflow-hidden">
136
+ <div className="min-w-0 flex-1 overflow-hidden">
137
+ <FullAgentEditor
138
+ agent={agent}
139
+ projectId={projectId}
140
+ memoryContent={memoryContent}
141
+ vaultEntries={vaultEntries}
142
+ />
143
+ </div>
144
+ <div
145
+ className={`shrink-0 overflow-hidden transition-[width] duration-200 ease-out ${
146
+ chatOpen ? "w-[36%] border-l border-border" : "w-0"
147
+ }`}
148
+ >
149
+ <div className="flex h-full w-full min-w-[320px] flex-col overflow-hidden">
150
+ <BuilderChat
151
+ projectId={projectId}
152
+ apiUrl={apiUrl}
153
+ agentName={agent.name}
154
+ onMutation={() => router.refresh()}
155
+ />
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function FullAgentEditor({
164
+ agent,
165
+ projectId,
166
+ memoryContent,
167
+ vaultEntries,
168
+ }: {
169
+ agent: AgentConfig;
170
+ projectId: string;
171
+ memoryContent: string;
172
+ vaultEntries: VaultEntry[];
173
+ }) {
174
+ // Tab is URL-driven (?tab=) so the Meta Agent's `navigate` tool can point
175
+ // straight at a component, and the tab is deep-linkable.
176
+ const router = useRouter();
177
+ const pathname = usePathname();
178
+ const searchParams = useSearchParams();
179
+ const urlTab = searchParams.get("tab") as StudioTab | null;
180
+ const validUrlTab = urlTab && TABS.some((t) => t.key === urlTab) ? urlTab : null;
181
+ const [tab, setTab] = useState<StudioTab>(validUrlTab ?? "models");
182
+
183
+ // React to external URL changes (e.g. the copilot navigating here).
184
+ useEffect(() => {
185
+ if (validUrlTab && validUrlTab !== tab) setTab(validUrlTab);
186
+ // eslint-disable-next-line react-hooks/exhaustive-deps
187
+ }, [validUrlTab]);
188
+
189
+ function selectTab(key: StudioTab) {
190
+ setTab(key);
191
+ const params = new URLSearchParams(searchParams.toString());
192
+ params.set("tab", key);
193
+ router.replace(`${pathname}?${params.toString()}`, { scroll: false });
194
+ }
195
+
196
+ return (
197
+ <div className="flex h-full flex-col">
198
+ {/* Sticky: identity + tab nav (don't scroll) */}
199
+ <div className="shrink-0 pt-1 pr-4">
200
+ <AgentIdentityHeader name={agent.name} role={agent.role} />
201
+ <div className="mt-4 flex flex-wrap gap-1 border-b border-border">
202
+ {TABS.map((t) => {
203
+ const Icon = t.icon;
204
+ const active = t.key === tab;
205
+ return (
206
+ <button
207
+ key={t.key}
208
+ type="button"
209
+ onClick={() => selectTab(t.key)}
210
+ className={`relative flex items-center gap-1.5 px-3 py-2 text-sm transition-colors ${
211
+ active
212
+ ? "font-medium text-foreground"
213
+ : "text-muted-foreground hover:text-foreground"
214
+ }`}
215
+ >
216
+ <Icon className="h-3.5 w-3.5" strokeWidth={1.5} />
217
+ {t.label}
218
+ {active && (
219
+ <span className="absolute bottom-0 left-0 right-0 h-[2px] bg-foreground" />
220
+ )}
221
+ </button>
222
+ );
223
+ })}
224
+ </div>
225
+ </div>
226
+
227
+ {/* Only the tab content scrolls */}
228
+ <div className="min-h-0 flex-1 overflow-auto py-4 pr-4">
229
+ {tab === "models" && (
230
+ <AgentModelsView agent={agent} projectId={projectId} agentName={agent.name} />
231
+ )}
232
+ {tab === "skills" && <AgentSkillsView agent={agent} projectId={projectId} />}
233
+ {tab === "tools" && <AgentToolsView agent={agent} projectId={projectId} />}
234
+ {tab === "prompt" && (
235
+ <AgentPromptView agent={agent} projectId={projectId} agentName={agent.name} />
236
+ )}
237
+ {tab === "memory" && <AgentMemoryView id={projectId} content={memoryContent} />}
238
+ {tab === "vault" && <AgentVaultView entries={vaultEntries} />}
239
+ </div>
240
+ </div>
241
+ );
242
+ }
@@ -0,0 +1,48 @@
1
+ import Link from "next/link";
2
+ import { ChevronRight } from "lucide-react";
3
+ import type { AgentConfig } from "@polpo-ai/core";
4
+
5
+ const headerClass = "py-2.5 text-left text-xs font-medium text-muted-foreground";
6
+
7
+ export function AgentsTable({ agents, projectId }: { agents: AgentConfig[]; projectId: string }) {
8
+ return (
9
+ <div data-testid="agents-table" className="mt-4 rounded-lg border border-border overflow-hidden overflow-x-auto min-w-[600px]">
10
+ {/* Header — same flex widths as rows */}
11
+ <div className="flex items-center border-b border-border bg-secondary/50">
12
+ <span className={`px-4 flex-1 min-w-[120px] ${headerClass}`}>Agent</span>
13
+ <span className={`px-4 flex-1 ${headerClass}`}>Role</span>
14
+ <span className={`px-4 flex-1 ${headerClass}`}>Model</span>
15
+ <span className={`px-4 w-20 ${headerClass}`}>Tools</span>
16
+ <span className={`px-4 w-24 ${headerClass}`}>Reasoning</span>
17
+ <span className="w-8" />
18
+ </div>
19
+ {/* Rows */}
20
+ {agents.map((agent) => (
21
+ <Link
22
+ key={agent.name}
23
+ href={`/projects/${projectId}/agents/${agent.name}`}
24
+ data-testid={`agent-row-${agent.name}`}
25
+ className="flex items-center border-b border-border last:border-0 hover:bg-secondary/30 transition-colors group"
26
+ >
27
+ <span className="px-4 py-3 flex-1 min-w-[120px]">
28
+ <span className="font-mono text-xs font-medium">{agent.name}</span>
29
+ {agent.identity?.title && (
30
+ <span className="block mt-0.5 text-[11px] text-muted-foreground/50">{agent.identity.title}</span>
31
+ )}
32
+ </span>
33
+ <span className="px-4 py-3 flex-1 text-sm text-muted-foreground">{agent.role ?? "—"}</span>
34
+ <span className="px-4 py-3 flex-1 font-mono text-xs text-muted-foreground">{agent.model ?? "—"}</span>
35
+ <span className="px-4 py-3 w-20 text-xs text-muted-foreground">
36
+ {agent.allowedTools ? agent.allowedTools.length : 0} tools
37
+ </span>
38
+ <span className="px-4 py-3 w-24 text-xs text-muted-foreground">
39
+ {agent.reasoning ?? "off"}
40
+ </span>
41
+ <span className="px-2 py-3 w-8">
42
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground/30 group-hover:text-muted-foreground transition-colors" />
43
+ </span>
44
+ </Link>
45
+ ))}
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,70 @@
1
+ import { Fragment } from "react";
2
+ import Link from "next/link";
3
+ import { ChevronRight, type LucideIcon } from "lucide-react";
4
+
5
+ export interface Crumb {
6
+ label: string;
7
+ href?: string;
8
+ icon?: LucideIcon;
9
+ }
10
+
11
+ /**
12
+ * Shared breadcrumb — chevron-separated trail that reads unmistakably as a
13
+ * breadcrumb. Intermediate crumbs are muted links with a hover chip; the
14
+ * last crumb is the current location (foreground, mono), still linkable if
15
+ * it has an href. One component, used across the agent surfaces (DRY).
16
+ */
17
+ export function Breadcrumb({ items }: { items: Crumb[] }) {
18
+ return (
19
+ <nav
20
+ aria-label="Breadcrumb"
21
+ className="flex items-center gap-0.5 text-xs"
22
+ >
23
+ {items.map((item, i) => {
24
+ const isLast = i === items.length - 1;
25
+ const Icon = item.icon;
26
+ const inner = (
27
+ <span className="inline-flex items-center gap-1.5">
28
+ {Icon && <Icon className="h-3.5 w-3.5 shrink-0" strokeWidth={1.5} />}
29
+ <span className={isLast ? "font-mono font-medium" : ""}>
30
+ {item.label}
31
+ </span>
32
+ </span>
33
+ );
34
+ return (
35
+ <Fragment key={`${item.label}-${i}`}>
36
+ {i > 0 && (
37
+ <ChevronRight
38
+ className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40"
39
+ strokeWidth={2}
40
+ aria-hidden
41
+ />
42
+ )}
43
+ {item.href ? (
44
+ <Link
45
+ href={item.href}
46
+ aria-current={isLast ? "page" : undefined}
47
+ className={`inline-flex items-center rounded-sm px-1.5 py-1 transition-colors hover:bg-secondary ${
48
+ isLast
49
+ ? "text-foreground hover:text-foreground"
50
+ : "text-muted-foreground hover:text-foreground"
51
+ }`}
52
+ >
53
+ {inner}
54
+ </Link>
55
+ ) : (
56
+ <span
57
+ aria-current={isLast ? "page" : undefined}
58
+ className={`inline-flex items-center px-1.5 py-1 ${
59
+ isLast ? "text-foreground" : "text-muted-foreground"
60
+ }`}
61
+ >
62
+ {inner}
63
+ </span>
64
+ )}
65
+ </Fragment>
66
+ );
67
+ })}
68
+ </nav>
69
+ );
70
+ }