@polpo-ai/dashboard 0.1.4 → 0.15.8

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 (184) hide show
  1. package/LICENSE +3 -1
  2. package/README.md +13 -40
  3. package/dist/components.d.ts +53 -0
  4. package/dist/components.d.ts.map +1 -0
  5. package/dist/components.js +81 -0
  6. package/dist/components.js.map +1 -0
  7. package/dist/host.d.ts +24 -0
  8. package/dist/host.d.ts.map +1 -0
  9. package/dist/host.js +18 -0
  10. package/dist/host.js.map +1 -0
  11. package/dist/index.d.ts +13 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +11 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/test/setup.d.ts +2 -0
  16. package/dist/test/setup.d.ts.map +1 -0
  17. package/dist/test/setup.js +2 -0
  18. package/dist/test/setup.js.map +1 -0
  19. package/dist/views/agent-detail.d.ts +4 -0
  20. package/dist/views/agent-detail.d.ts.map +1 -0
  21. package/dist/views/agent-detail.js +31 -0
  22. package/dist/views/agent-detail.js.map +1 -0
  23. package/dist/views/agents.d.ts +2 -0
  24. package/dist/views/agents.d.ts.map +1 -0
  25. package/dist/views/agents.js +81 -0
  26. package/dist/views/agents.js.map +1 -0
  27. package/dist/views/files.d.ts +2 -0
  28. package/dist/views/files.d.ts.map +1 -0
  29. package/dist/views/files.js +30 -0
  30. package/dist/views/files.js.map +1 -0
  31. package/dist/views/memory.d.ts +2 -0
  32. package/dist/views/memory.d.ts.map +1 -0
  33. package/dist/views/memory.js +19 -0
  34. package/dist/views/memory.js.map +1 -0
  35. package/dist/views/playground.d.ts +4 -0
  36. package/dist/views/playground.d.ts.map +1 -0
  37. package/dist/views/playground.js +43 -0
  38. package/dist/views/playground.js.map +1 -0
  39. package/dist/views/session-detail.d.ts +4 -0
  40. package/dist/views/session-detail.d.ts.map +1 -0
  41. package/dist/views/session-detail.js +23 -0
  42. package/dist/views/session-detail.js.map +1 -0
  43. package/dist/views/sessions.d.ts +2 -0
  44. package/dist/views/sessions.d.ts.map +1 -0
  45. package/dist/views/sessions.js +17 -0
  46. package/dist/views/sessions.js.map +1 -0
  47. package/dist/views/skills.d.ts +2 -0
  48. package/dist/views/skills.d.ts.map +1 -0
  49. package/dist/views/skills.js +16 -0
  50. package/dist/views/skills.js.map +1 -0
  51. package/package.json +47 -83
  52. package/src/styles.css +169 -0
  53. package/app/(dashboard)/error.tsx +0 -47
  54. package/app/(dashboard)/layout.tsx +0 -41
  55. package/app/(dashboard)/loading.tsx +0 -30
  56. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +0 -36
  57. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +0 -30
  58. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +0 -51
  59. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +0 -211
  60. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +0 -84
  61. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +0 -525
  62. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +0 -61
  63. package/app/(dashboard)/projects/[id]/agents/page.tsx +0 -39
  64. package/app/(dashboard)/projects/[id]/agents/view.tsx +0 -283
  65. package/app/(dashboard)/projects/[id]/layout.tsx +0 -11
  66. package/app/(dashboard)/projects/[id]/logs/page.tsx +0 -27
  67. package/app/(dashboard)/projects/[id]/logs/view.tsx +0 -184
  68. package/app/(dashboard)/projects/[id]/memory/page.tsx +0 -40
  69. package/app/(dashboard)/projects/[id]/memory/view.tsx +0 -17
  70. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +0 -88
  71. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +0 -569
  72. package/app/(dashboard)/projects/[id]/missions/page.tsx +0 -32
  73. package/app/(dashboard)/projects/[id]/missions/view.tsx +0 -282
  74. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +0 -359
  75. package/app/(dashboard)/projects/[id]/page.tsx +0 -68
  76. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +0 -5
  77. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +0 -433
  78. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +0 -5
  79. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +0 -222
  80. package/app/(dashboard)/projects/[id]/schedules/page.tsx +0 -44
  81. package/app/(dashboard)/projects/[id]/schedules/view.tsx +0 -385
  82. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +0 -55
  83. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +0 -373
  84. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +0 -124
  85. package/app/(dashboard)/projects/[id]/sessions/page.tsx +0 -31
  86. package/app/(dashboard)/projects/[id]/sessions/view.tsx +0 -242
  87. package/app/(dashboard)/projects/[id]/settings/page.tsx +0 -52
  88. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +0 -65
  89. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +0 -227
  90. package/app/(dashboard)/projects/[id]/skills/page.tsx +0 -31
  91. package/app/(dashboard)/projects/[id]/skills/view.tsx +0 -243
  92. package/app/(dashboard)/projects/[id]/storage/page.tsx +0 -5
  93. package/app/(dashboard)/projects/[id]/storage/view.tsx +0 -385
  94. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +0 -336
  95. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +0 -113
  96. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +0 -72
  97. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +0 -90
  98. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +0 -63
  99. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +0 -137
  100. package/app/(dashboard)/projects/[id]/tasks/page.tsx +0 -40
  101. package/app/(dashboard)/projects/[id]/tasks/view.tsx +0 -421
  102. package/app/(dashboard)/projects/[id]/view.tsx +0 -421
  103. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +0 -156
  104. package/app/(dashboard)/projects/projects-list.tsx +0 -126
  105. package/app/(playground)/error.tsx +0 -42
  106. package/app/(playground)/layout.tsx +0 -27
  107. package/app/(playground)/projects/[id]/playground/page.tsx +0 -57
  108. package/app/(playground)/projects/[id]/playground/view.tsx +0 -167
  109. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +0 -47
  110. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +0 -63
  111. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +0 -237
  112. package/app/favicon.ico +0 -0
  113. package/app/globals.css +0 -385
  114. package/app/icon.svg +0 -6
  115. package/app/layout.tsx +0 -39
  116. package/app/page.tsx +0 -11
  117. package/components/ai-elements/code-block.tsx +0 -562
  118. package/components/dashboard/agent-capabilities.tsx +0 -326
  119. package/components/dashboard/agent-identity-header.tsx +0 -50
  120. package/components/dashboard/agent-model-picker.tsx +0 -195
  121. package/components/dashboard/agent-model-selector.tsx +0 -121
  122. package/components/dashboard/agent-picker-card.tsx +0 -46
  123. package/components/dashboard/agent-studio.tsx +0 -242
  124. package/components/dashboard/agents-table.tsx +0 -48
  125. package/components/dashboard/breadcrumb.tsx +0 -70
  126. package/components/dashboard/builder-chat.tsx +0 -418
  127. package/components/dashboard/chat-shell.tsx +0 -40
  128. package/components/dashboard/client-picker.tsx +0 -63
  129. package/components/dashboard/command-snippet.tsx +0 -77
  130. package/components/dashboard/connect-dialog.tsx +0 -680
  131. package/components/dashboard/copy-button.tsx +0 -45
  132. package/components/dashboard/copy-card.tsx +0 -48
  133. package/components/dashboard/file-browser.tsx +0 -161
  134. package/components/dashboard/hint.tsx +0 -22
  135. package/components/dashboard/inference-mode.tsx +0 -15
  136. package/components/dashboard/manual-refresh-button.tsx +0 -52
  137. package/components/dashboard/markdown.tsx +0 -31
  138. package/components/dashboard/mcp-install-panel.tsx +0 -196
  139. package/components/dashboard/mission-graph.tsx +0 -262
  140. package/components/dashboard/nav-tabs.tsx +0 -86
  141. package/components/dashboard/polpo-chat.tsx +0 -468
  142. package/components/dashboard/project-copilot.tsx +0 -253
  143. package/components/dashboard/sdk-snippet-panel.tsx +0 -194
  144. package/components/dashboard/section-header.tsx +0 -37
  145. package/components/dashboard/settings-form.tsx +0 -2477
  146. package/components/dashboard/sidebar.tsx +0 -385
  147. package/components/dashboard/skeletons.tsx +0 -1054
  148. package/components/dashboard/skills-install-wizard.tsx +0 -192
  149. package/components/dashboard/swarm-runs.tsx +0 -316
  150. package/components/dashboard/tab-toggle.tsx +0 -35
  151. package/components/dashboard/task-studio.tsx +0 -224
  152. package/components/dashboard/top-header.tsx +0 -203
  153. package/components/dashboard/webhook-deliveries.tsx +0 -234
  154. package/components/icons/coding-agents.tsx +0 -151
  155. package/components/json-ld.tsx +0 -8
  156. package/components/providers.tsx +0 -25
  157. package/components/ui/badge.tsx +0 -52
  158. package/components/ui/button.tsx +0 -60
  159. package/components/ui/card.tsx +0 -103
  160. package/components/ui/chart.tsx +0 -356
  161. package/components/ui/command.tsx +0 -196
  162. package/components/ui/dialog.tsx +0 -157
  163. package/components/ui/input-group.tsx +0 -158
  164. package/components/ui/input.tsx +0 -20
  165. package/components/ui/multi-select.tsx +0 -161
  166. package/components/ui/popover.tsx +0 -90
  167. package/components/ui/select.tsx +0 -201
  168. package/components/ui/separator.tsx +0 -25
  169. package/components/ui/sheet.tsx +0 -135
  170. package/components/ui/skeleton.tsx +0 -13
  171. package/components/ui/tabs.tsx +0 -56
  172. package/components/ui/textarea.tsx +0 -18
  173. package/components/ui/tooltip.tsx +0 -66
  174. package/hooks/use-desktop-sidebar.tsx +0 -50
  175. package/hooks/use-mobile-sidebar.tsx +0 -37
  176. package/index.ts +0 -17
  177. package/lib/api.ts +0 -194
  178. package/lib/builder-context.ts +0 -60
  179. package/lib/data-client.ts +0 -68
  180. package/lib/get-query-client.ts +0 -25
  181. package/lib/polpo-client.tsx +0 -49
  182. package/lib/tool-catalog.ts +0 -234
  183. package/lib/use-event-catalog.ts +0 -28
  184. package/lib/utils.ts +0 -6
@@ -1,192 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo, useState } from "react";
4
- import { useParams } from "next/navigation";
5
- import { useMutation, useQueryClient } from "@tanstack/react-query";
6
- import { ArrowLeft, Check, Download, FilePlus2, Link2, Loader2 } from "lucide-react";
7
- import { usePolpoClient } from "../../lib/polpo-client";
8
- import {
9
- SkillsProvider,
10
- SkillCreateForm,
11
- type SkillsAdapter,
12
- type SkillCreateInput,
13
- type Skill,
14
- } from "@lumea-labs/skills";
15
- import {
16
- Dialog,
17
- DialogContent,
18
- DialogDescription,
19
- DialogHeader,
20
- DialogTitle,
21
- DialogTrigger,
22
- } from "../../components/ui/dialog";
23
-
24
- /**
25
- * SkillsInstallWizard — guided stepper that actually installs skills, dogfooding
26
- * the SDK end to end:
27
- * - From a URL → client.installSkills(githubUrl)
28
- * - Paste/create → @lumea-labs/skills <SkillCreateForm> over a minimal
29
- * SkillsAdapter built on the PolpoClient (adapter.createSkill).
30
- *
31
- * (Registry browse via Mastra's @mastra/skills-api is a future addition — it's a
32
- * Node lib with bundled data, so it needs a server action, not a browser fetch.)
33
- */
34
-
35
- type Source = "url" | "paste";
36
-
37
- export function SkillsInstallWizard() {
38
- return <WizardDialog />;
39
- }
40
-
41
- function WizardDialog() {
42
- const { id } = useParams<{ id: string }>();
43
- const client = usePolpoClient(id);
44
- const qc = useQueryClient();
45
-
46
- const [open, setOpen] = useState(false);
47
- const [source, setSource] = useState<Source | null>(null);
48
- const [url, setUrl] = useState("");
49
- const [done, setDone] = useState<string | null>(null);
50
-
51
- // Minimal SkillsAdapter on the PolpoClient (same de-dup seam usePolpoClient
52
- // resolves: OSS direct, cloud session-proxy). Only createSkill is needed.
53
- const adapter = useMemo<SkillsAdapter>(
54
- () => ({
55
- createSkill: async (input: SkillCreateInput): Promise<Skill> => {
56
- const res = await client.createSkill({
57
- name: input.name,
58
- description: input.description,
59
- content: input.body,
60
- allowedTools: (input.frontmatter as { allowedTools?: string[] })?.allowedTools,
61
- });
62
- return { id: res.name, name: res.name, description: input.description, installed: true } as Skill;
63
- },
64
- }),
65
- [client],
66
- );
67
-
68
- const install = useMutation({
69
- mutationFn: (src: string) => client.installSkills(src),
70
- onSuccess: (_res, src) => {
71
- qc.invalidateQueries({ queryKey: ["skills", id] });
72
- setDone(src);
73
- },
74
- });
75
-
76
- const reset = () => {
77
- setSource(null);
78
- setUrl("");
79
- setDone(null);
80
- install.reset();
81
- };
82
-
83
- const afterCreate = () => {
84
- qc.invalidateQueries({ queryKey: ["skills", id] });
85
- setDone("created");
86
- };
87
-
88
- return (
89
- <Dialog open={open} onOpenChange={(o) => { setOpen(o); if (!o) reset(); }}>
90
- <DialogTrigger className="inline-flex items-center gap-1.5 border border-foreground bg-foreground px-3 py-1.5 text-xs font-medium text-background transition-colors hover:bg-foreground/90">
91
- <Download className="h-3.5 w-3.5" strokeWidth={2} />
92
- Install skills
93
- </DialogTrigger>
94
- <DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-lg">
95
- <DialogHeader>
96
- <DialogTitle>Install skills</DialogTitle>
97
- <DialogDescription>
98
- Skills inject domain knowledge into your agents&apos; prompts — no code
99
- changes. Pick a source, install, then assign to agents.
100
- </DialogDescription>
101
- </DialogHeader>
102
-
103
- {done ? (
104
- <div className="flex flex-col items-center gap-3 py-8 text-center">
105
- <div className="grid h-10 w-10 place-items-center rounded-full bg-emerald-500/15 text-emerald-500">
106
- <Check className="h-5 w-5" />
107
- </div>
108
- <p className="text-sm font-medium">Skill installed</p>
109
- <p className="max-w-sm font-mono text-[11px] text-muted-foreground break-all">{done}</p>
110
- <div className="mt-2 flex gap-2">
111
- <button onClick={reset} className="border border-border px-3 py-1.5 text-xs hover:border-foreground/30">
112
- Install another
113
- </button>
114
- <button onClick={() => setOpen(false)} className="border border-foreground bg-foreground px-3 py-1.5 text-xs text-background hover:bg-foreground/90">
115
- Done
116
- </button>
117
- </div>
118
- </div>
119
- ) : !source ? (
120
- <div className="grid gap-2 pt-1">
121
- <SourceCard
122
- icon={<Link2 className="h-4 w-4" />}
123
- title="From a URL"
124
- desc="Any public GitHub repo with a skill pack."
125
- onClick={() => setSource("url")}
126
- />
127
- <SourceCard
128
- icon={<FilePlus2 className="h-4 w-4" />}
129
- title="Paste / create"
130
- desc="Author a skill inline from its contents."
131
- onClick={() => setSource("paste")}
132
- />
133
- </div>
134
- ) : (
135
- <div className="flex flex-col gap-3 pt-1">
136
- <button
137
- onClick={() => { setSource(null); install.reset(); }}
138
- className="inline-flex w-fit items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground"
139
- >
140
- <ArrowLeft className="h-3 w-3" /> Back
141
- </button>
142
-
143
- {source === "url" && (
144
- <form
145
- onSubmit={(e) => { e.preventDefault(); if (url.trim()) install.mutate(url.trim()); }}
146
- className="flex flex-col gap-2"
147
- >
148
- <input
149
- value={url}
150
- autoFocus
151
- onChange={(e) => setUrl(e.target.value)}
152
- placeholder="https://github.com/you/your-skills"
153
- className="border border-border px-3 py-2 text-sm outline-none focus:border-foreground/30"
154
- />
155
- <button
156
- type="submit"
157
- disabled={!url.trim() || install.isPending}
158
- className="inline-flex items-center justify-center gap-1.5 border border-foreground bg-foreground px-3 py-2 text-xs text-background hover:bg-foreground/90 disabled:opacity-50"
159
- >
160
- {install.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Download className="h-3.5 w-3.5" />}
161
- Install
162
- </button>
163
- {install.isError && <p className="text-xs text-destructive">{(install.error as Error).message}</p>}
164
- </form>
165
- )}
166
-
167
- {source === "paste" && (
168
- <SkillsProvider adapter={adapter}>
169
- <SkillCreateForm onCreated={afterCreate} onCancel={() => setSource(null)} />
170
- </SkillsProvider>
171
- )}
172
- </div>
173
- )}
174
- </DialogContent>
175
- </Dialog>
176
- );
177
- }
178
-
179
- function SourceCard({ icon, title, desc, onClick }: { icon: React.ReactNode; title: string; desc: string; onClick: () => void }) {
180
- return (
181
- <button
182
- onClick={onClick}
183
- className="flex items-start gap-3 border border-border px-4 py-3 text-left transition-colors hover:border-foreground/30 hover:bg-secondary/30"
184
- >
185
- <span className="mt-0.5 text-muted-foreground">{icon}</span>
186
- <span className="min-w-0">
187
- <span className="block text-sm font-medium">{title}</span>
188
- <span className="block text-[11px] text-muted-foreground">{desc}</span>
189
- </span>
190
- </button>
191
- );
192
- }
@@ -1,316 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Custom Swarm run row + list — replaces `@lumea-labs/orchestrator`'s
5
- * SwarmRunRow / SwarmRunList for the project dashboard so the design
6
- * matches Polpo's neo-brutalist palette (sharp edges, mono labels,
7
- * theme tokens, no rounded corners except the status pulse dot).
8
- *
9
- * Scope is intentionally narrow:
10
- * - Accepts a thin `RunRow` shape, not the full SwarmRun (no
11
- * toolCalls / artifacts / events here). The dashboard surface is
12
- * a scannable list, not a drill-down.
13
- * - No cancel / retry handlers yet — those land in this file when
14
- * we wire the SDK to abort + re-queue tasks.
15
- * - Skeleton variant lives next to the real component so layout
16
- * drift is impossible.
17
- *
18
- * Columns left → right:
19
- * 1. Status dot (pulses on running)
20
- * 2. Status label (mono uppercase, e.g. RUNNING)
21
- * 3. Agent monogram tile + name (mono uppercase)
22
- * 4. Task title (truncated)
23
- * 5. Last log (right-aligned, muted, hidden < md)
24
- * 6. Elapsed / relative time (right-aligned mono)
25
- */
26
-
27
- import { useEffect, useState } from "react";
28
- import Link from "next/link";
29
-
30
- export type RunStatus =
31
- | "scheduled"
32
- | "pending"
33
- | "running"
34
- | "review"
35
- | "done"
36
- | "failed"
37
- | "cancelled";
38
-
39
- export interface RunRow {
40
- id: string;
41
- status: RunStatus;
42
- agentName: string;
43
- agentColor?: string;
44
- title: string;
45
- lastLog?: string;
46
- /** ISO timestamp. Used to compute elapsed (running) or duration (done). */
47
- startedAt?: string;
48
- /** ISO timestamp. Set when status is `done`, `failed`, or `cancelled`. */
49
- finishedAt?: string;
50
- /** Optional href — when provided, the whole row becomes a Link. */
51
- href?: string;
52
- }
53
-
54
- /** Tailwind bg + fg color tokens per status. Mapped to our theme so
55
- * the palette flips correctly between light + dark mode. */
56
- const STATUS_STYLE: Record<RunStatus, { dot: string; text: string }> = {
57
- scheduled: { dot: "bg-violet-500", text: "text-violet-500" },
58
- pending: { dot: "bg-muted-foreground/60", text: "text-muted-foreground" },
59
- running: { dot: "bg-brand", text: "text-brand" },
60
- review: { dot: "bg-amber-500", text: "text-amber-500" },
61
- done: { dot: "bg-emerald-500", text: "text-emerald-500" },
62
- failed: { dot: "bg-destructive", text: "text-destructive" },
63
- cancelled: { dot: "bg-muted-foreground/30", text: "text-muted-foreground/60" },
64
- };
65
-
66
- const STATUS_LABEL: Record<RunStatus, string> = {
67
- scheduled: "scheduled",
68
- pending: "pending",
69
- running: "running",
70
- review: "review",
71
- done: "done",
72
- failed: "failed",
73
- cancelled: "cancelled",
74
- };
75
-
76
- /** Counts for every run status. Mirrors @lumea-labs/orchestrator's
77
- * SwarmCounts but is the source of truth for our own status bar so we
78
- * can render ALL states (the package bar only shows running/done/failed). */
79
- export interface RunCounts {
80
- scheduled: number;
81
- pending: number;
82
- running: number;
83
- review: number;
84
- done: number;
85
- failed: number;
86
- cancelled: number;
87
- total: number;
88
- }
89
-
90
- /** The order states appear in the bar, left → right (lifecycle order). */
91
- const BAR_ORDER: RunStatus[] = [
92
- "running",
93
- "pending",
94
- "scheduled",
95
- "review",
96
- "done",
97
- "failed",
98
- "cancelled",
99
- ];
100
-
101
- /**
102
- * Horizontal status bar — one stat per run status. Always rendered (shows
103
- * 0 across the board when a project has no tasks yet). Replaces
104
- * @lumea-labs/orchestrator's SwarmStatusBar, which is hardcoded to three
105
- * buckets. Running pulses when > 0; all states use the shared
106
- * STATUS_STYLE tokens so the dot colours match the run rows exactly.
107
- */
108
- export function SwarmStatusBar({ counts }: { counts: RunCounts }) {
109
- return (
110
- <div className="flex flex-wrap items-center gap-x-5 gap-y-2 border border-border bg-card px-4 py-2.5">
111
- {BAR_ORDER.map((status, i) => {
112
- const value = counts[status];
113
- const style = STATUS_STYLE[status];
114
- const dim = value === 0;
115
- return (
116
- <span key={status} className="flex items-center gap-3">
117
- {i > 0 && <span className="h-4 w-px bg-border" aria-hidden />}
118
- <span className="inline-flex items-center gap-1.5">
119
- <span className="relative inline-flex size-2 shrink-0">
120
- {status === "running" && value > 0 && (
121
- <span className={`absolute inset-0 rounded-full opacity-60 animate-ping ${style.dot}`} aria-hidden />
122
- )}
123
- <span
124
- className={`relative inline-block size-2 rounded-full ${style.dot} ${dim ? "opacity-30" : ""}`}
125
- aria-hidden
126
- />
127
- </span>
128
- <span className={`font-mono text-sm font-bold tabular-nums ${dim ? "text-muted-foreground/40" : style.text}`}>
129
- {value}
130
- </span>
131
- <span className={`font-mono text-[10px] font-medium uppercase tracking-[0.12em] ${dim ? "text-muted-foreground/40" : "text-muted-foreground"}`}>
132
- {STATUS_LABEL[status]}
133
- </span>
134
- </span>
135
- </span>
136
- );
137
- })}
138
- </div>
139
- );
140
- }
141
-
142
- function formatElapsed(seconds: number): string {
143
- if (seconds < 60) return `${Math.max(0, Math.round(seconds))}s`;
144
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
145
- if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
146
- return `${Math.floor(seconds / 86400)}d`;
147
- }
148
-
149
- /**
150
- * Returns the elapsed seconds between startedAt and finishedAt (or
151
- * `now` when still running). Recomputed every second via the parent
152
- * `now` prop so running rows tick live without forcing each row to
153
- * own a timer.
154
- */
155
- function elapsedSeconds(
156
- startedAt: string | undefined,
157
- finishedAt: string | undefined,
158
- now: number,
159
- ): number {
160
- if (!startedAt) return 0;
161
- const start = new Date(startedAt).getTime();
162
- const end = finishedAt ? new Date(finishedAt).getTime() : now;
163
- return Math.max(0, (end - start) / 1000);
164
- }
165
-
166
- function AgentBadge({
167
- name,
168
- color,
169
- }: {
170
- name: string;
171
- color?: string;
172
- }) {
173
- const initial = (name || "?").charAt(0).toUpperCase();
174
- return (
175
- <span
176
- aria-hidden
177
- className="grid size-4 shrink-0 place-items-center text-[9px] font-bold leading-none text-white"
178
- style={{ background: color ?? "hsl(220 8% 50%)" }}
179
- >
180
- {initial}
181
- </span>
182
- );
183
- }
184
-
185
- export function SwarmRunRowItem({
186
- run,
187
- now,
188
- }: {
189
- run: RunRow;
190
- now: number;
191
- }) {
192
- const isLive = run.status === "running" || run.status === "pending";
193
- const elapsed = elapsedSeconds(run.startedAt, run.finishedAt, now);
194
- const elapsedLabel = run.startedAt ? formatElapsed(elapsed) : "—";
195
- const style = STATUS_STYLE[run.status];
196
- const label = STATUS_LABEL[run.status];
197
-
198
- const inner = (
199
- <div className="group flex items-center gap-3 border-b border-border px-3 py-2 transition-colors last:border-0 hover:bg-foreground/5">
200
- {/* Status dot — pulses on running */}
201
- <span className="relative inline-flex size-2 shrink-0">
202
- {run.status === "running" && (
203
- <span
204
- aria-hidden
205
- className={`absolute inset-0 rounded-full opacity-60 animate-ping ${style.dot}`}
206
- />
207
- )}
208
- <span
209
- aria-hidden
210
- className={`relative inline-block size-2 rounded-full ${style.dot}`}
211
- />
212
- </span>
213
-
214
- {/* Status label — mono uppercase */}
215
- <span
216
- className={`w-[68px] shrink-0 truncate font-mono text-[10px] font-bold uppercase tracking-[0.14em] ${style.text}`}
217
- >
218
- {label}
219
- </span>
220
-
221
- {/* Agent — monogram tile + name */}
222
- <span className="flex shrink-0 items-center gap-1.5">
223
- <AgentBadge name={run.agentName} color={run.agentColor} />
224
- <span className="w-[88px] truncate font-mono text-[10.5px] font-medium uppercase tracking-[0.1em] text-muted-foreground">
225
- {run.agentName || "—"}
226
- </span>
227
- </span>
228
-
229
- {/* Title — flex-1, truncates */}
230
- <span className="min-w-0 flex-1 truncate text-[12.5px] leading-tight text-foreground">
231
- {run.title}
232
- </span>
233
-
234
- {/* Last log — hidden below md */}
235
- {run.lastLog ? (
236
- <span className="hidden min-w-0 max-w-[28%] truncate font-mono text-[10px] text-muted-foreground/70 md:inline">
237
- {run.lastLog}
238
- </span>
239
- ) : null}
240
-
241
- {/* Elapsed / duration */}
242
- <span className="w-12 shrink-0 text-right font-mono text-[10.5px] tabular-nums text-muted-foreground">
243
- {isLive && run.startedAt ? `${elapsedLabel} ago` : elapsedLabel}
244
- </span>
245
- </div>
246
- );
247
-
248
- if (run.href) {
249
- return (
250
- <Link
251
- href={run.href}
252
- className="block"
253
- data-testid={`swarm-run-row-${run.id}`}
254
- >
255
- {inner}
256
- </Link>
257
- );
258
- }
259
- return <div data-testid={`swarm-run-row-${run.id}`}>{inner}</div>;
260
- }
261
-
262
- export function SwarmRunList({
263
- runs,
264
- emptyLabel = "No runs yet.",
265
- }: {
266
- runs: RunRow[];
267
- emptyLabel?: string;
268
- }) {
269
- // Tick every second so running rows show live elapsed time without
270
- // each row owning its own setInterval. Pauses when nothing is live.
271
- const hasLive = runs.some(
272
- (r) => r.status === "running" || r.status === "pending",
273
- );
274
- const [now, setNow] = useState(() => Date.now());
275
- useEffect(() => {
276
- if (!hasLive) return;
277
- const id = setInterval(() => setNow(Date.now()), 1000);
278
- return () => clearInterval(id);
279
- }, [hasLive]);
280
-
281
- if (runs.length === 0) {
282
- return (
283
- <div className="px-3 py-8 text-center text-xs text-muted-foreground">
284
- {emptyLabel}
285
- </div>
286
- );
287
- }
288
-
289
- return (
290
- <div data-testid="swarm-run-list">
291
- {runs.map((run) => (
292
- <SwarmRunRowItem key={run.id} run={run} now={now} />
293
- ))}
294
- </div>
295
- );
296
- }
297
-
298
- /**
299
- * Skeleton row — matches the real row's column layout exactly so the
300
- * transition from skeleton → real content has zero layout shift.
301
- */
302
- export function SwarmRunRowSkeleton() {
303
- return (
304
- <div className="flex items-center gap-3 border-b border-border px-3 py-2 last:border-0">
305
- <span className="size-2 shrink-0 rounded-full bg-muted-foreground/20" />
306
- <span className="h-3 w-[68px] shrink-0 bg-muted-foreground/15" />
307
- <span className="flex shrink-0 items-center gap-1.5">
308
- <span className="size-4 shrink-0 bg-muted-foreground/15" />
309
- <span className="h-3 w-[88px] bg-muted-foreground/15" />
310
- </span>
311
- <span className="h-3 min-w-0 flex-1 bg-muted-foreground/10" />
312
- <span className="hidden h-3 w-[18%] bg-muted-foreground/10 md:inline-block" />
313
- <span className="h-3 w-12 shrink-0 bg-muted-foreground/15" />
314
- </div>
315
- );
316
- }
@@ -1,35 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Inline tab toggle — squared pill buttons in a shared border strip.
5
- * Used across SdkSnippetPanel, SkillStepDetail, and anywhere we need
6
- * a compact tab switcher inside a card or detail panel.
7
- */
8
- export function TabToggle<T extends string>({
9
- value,
10
- onChange,
11
- options,
12
- }: {
13
- value: T;
14
- onChange: (v: T) => void;
15
- options: Array<{ value: T; label: string }>;
16
- }) {
17
- return (
18
- <div className="inline-flex items-center gap-1 border border-border p-0.5 w-fit">
19
- {options.map((opt) => (
20
- <button
21
- key={opt.value}
22
- type="button"
23
- onClick={() => onChange(opt.value)}
24
- className={`px-2.5 py-1 text-xs font-medium transition-colors ${
25
- value === opt.value
26
- ? "bg-secondary text-foreground"
27
- : "text-muted-foreground hover:text-foreground"
28
- }`}
29
- >
30
- {opt.label}
31
- </button>
32
- ))}
33
- </div>
34
- );
35
- }