@tonyclaw/agent-inspector 3.0.5 → 3.0.7

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 (42) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Co-35_Xw.js → CompareDrawer-C0Kbsm-Q.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-p-rpYyiF.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-DghbvKdy.js → ReplayDialog-CM3dhP8v.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-CRXE5v8X.js → RequestAnatomy-fa0JXy5p.js} +1 -1
  6. package/.output/public/assets/{ResponseView-CzxoQRjp.js → ResponseView-Bq6sC7Ay.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-DnFyF_Hr.js → StreamingChunkSequence-CGOun3Df.js} +1 -1
  8. package/.output/public/assets/{_sessionId-DHRhWzKe.js → _sessionId-D5fYH2C0.js} +1 -1
  9. package/.output/public/assets/{index-Dl1oki9E.js → index-BEsqvywM.js} +1 -1
  10. package/.output/public/assets/index-C29Olfzt.js +1 -0
  11. package/.output/public/assets/index-DgANE0r3.css +1 -0
  12. package/.output/public/assets/{json-viewer-B84f7oiG.js → json-viewer-B0QRbMSf.js} +1 -1
  13. package/.output/public/assets/{main-DViDnJ9X.js → main-CuKZD7n0.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +292 -203
  15. package/.output/server/{_sessionId-C43vEAWe.mjs → _sessionId-DdBTK3qm.mjs} +3 -3
  16. package/.output/server/_ssr/{CompareDrawer-DOL6q0jD.mjs → CompareDrawer-m82cVZ73.mjs} +4 -4
  17. package/.output/server/_ssr/{ProxyViewerContainer-S9izw3ND.mjs → ProxyViewerContainer-7geuf78f.mjs} +366 -37
  18. package/.output/server/_ssr/{ReplayDialog-tqthwJh0.mjs → ReplayDialog-B8RlWLLH.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-CgWDjFPR.mjs → RequestAnatomy-BE7pmwpv.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-Dk8Op622.mjs → ResponseView-D-sDsdQr.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-AlS7PK8f.mjs → StreamingChunkSequence-DU_hThJr.mjs} +2 -2
  22. package/.output/server/_ssr/{index-BGwvMN2M.mjs → index-D0sV8UbE.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-CKHkh5U3.mjs → json-viewer-CaDh1zrw.mjs} +3 -3
  25. package/.output/server/_ssr/{router-BJMaHkr1.mjs → router-Zh2iZ_u0.mjs} +357 -146
  26. package/.output/server/_tanstack-start-manifest_v-LCE-2AaF.mjs +4 -0
  27. package/.output/server/index.mjs +71 -71
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +32 -14
  30. package/src/components/alerts/AlertsDialog.tsx +3 -3
  31. package/src/components/ecosystem/AgentLabDialog.tsx +381 -0
  32. package/src/components/groups/GroupsDialog.tsx +3 -3
  33. package/src/components/providers/ImportWizardDialog.tsx +1 -1
  34. package/src/components/providers/ProviderCard.tsx +4 -4
  35. package/src/components/proxy-viewer/CompareDrawer.tsx +1 -1
  36. package/src/components/proxy-viewer/ConversationHeader.tsx +2 -2
  37. package/src/lib/ecosystemContract.ts +47 -0
  38. package/src/routes/api/ecosystem.packages.ts +263 -0
  39. package/.output/public/assets/ProxyViewerContainer-DPv1rBo7.js +0 -106
  40. package/.output/public/assets/index-D8cruW0P.css +0 -1
  41. package/.output/public/assets/index-DZyTpd2w.js +0 -1
  42. package/.output/server/_tanstack-start-manifest_v-C_HvRzgP.mjs +0 -4
@@ -0,0 +1,381 @@
1
+ import { type JSX, useMemo, useState } from "react";
2
+ import {
3
+ Activity,
4
+ Beaker,
5
+ Check,
6
+ Copy,
7
+ FlaskConical,
8
+ Network,
9
+ PackageCheck,
10
+ RefreshCw,
11
+ Rocket,
12
+ Sparkles,
13
+ Terminal,
14
+ } from "lucide-react";
15
+ import useSWR from "swr";
16
+ import {
17
+ EcosystemPackagesResponseSchema,
18
+ type EcosystemPackage,
19
+ type EcosystemPackageState,
20
+ type EcosystemRunnerPreset,
21
+ } from "../../lib/ecosystemContract";
22
+ import { fetchJson } from "../../lib/apiClient";
23
+ import { copyTextToClipboard } from "../../lib/clipboard";
24
+ import { cn } from "../../lib/utils";
25
+ import { Badge } from "../ui/badge";
26
+ import { Button } from "../ui/button";
27
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
28
+ import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
29
+
30
+ type AgentLabDialogProps = {
31
+ currentSessionId: string | null;
32
+ logCount: number;
33
+ };
34
+
35
+ type WorkflowCard = {
36
+ id: string;
37
+ title: string;
38
+ label: string;
39
+ description: string;
40
+ metric: string;
41
+ icon: typeof Activity;
42
+ };
43
+
44
+ const WORKFLOWS: WorkflowCard[] = [
45
+ {
46
+ id: "observe",
47
+ title: "Observe",
48
+ label: "Live",
49
+ description: "Capture IDE, process, session, turn, request, response, tools, and providers.",
50
+ metric: "Inspector core",
51
+ icon: Activity,
52
+ },
53
+ {
54
+ id: "replay",
55
+ title: "Replay",
56
+ label: "Ready",
57
+ description:
58
+ "Promote real logs into repeatable replay evidence without losing the raw context.",
59
+ metric: "Session asset",
60
+ icon: RefreshCw,
61
+ },
62
+ {
63
+ id: "evaluate",
64
+ title: "Evaluate",
65
+ label: "Next",
66
+ description: "Use eval-harness to turn sessions into regression cases and provider bakeoffs.",
67
+ metric: "Eval loop",
68
+ icon: Beaker,
69
+ },
70
+ {
71
+ id: "connect",
72
+ title: "Connect",
73
+ label: "MCP",
74
+ description:
75
+ "Expose Inspector state to coding agents as queryable context and durable evidence.",
76
+ metric: "/api/mcp",
77
+ icon: Network,
78
+ },
79
+ ];
80
+
81
+ async function fetchEcosystemPackages(url: string) {
82
+ return fetchJson(
83
+ url,
84
+ EcosystemPackagesResponseSchema,
85
+ undefined,
86
+ (response) => `Failed to load ecosystem packages: ${String(response.status)}`,
87
+ );
88
+ }
89
+
90
+ function stateLabel(state: EcosystemPackageState): string {
91
+ switch (state) {
92
+ case "installed":
93
+ return "Installed";
94
+ case "update-available":
95
+ return "Update";
96
+ case "available":
97
+ return "Available";
98
+ case "planned":
99
+ return "Planned";
100
+ case "unknown":
101
+ return "Unknown";
102
+ }
103
+ }
104
+
105
+ function stateClassName(state: EcosystemPackageState): string {
106
+ switch (state) {
107
+ case "installed":
108
+ return "border-emerald-400/20 bg-emerald-400/8 text-emerald-200";
109
+ case "update-available":
110
+ return "border-amber-400/20 bg-amber-400/8 text-amber-200";
111
+ case "available":
112
+ return "border-sky-400/20 bg-sky-400/8 text-sky-200";
113
+ case "planned":
114
+ return "border-white/10 bg-white/[0.04] text-white/55";
115
+ case "unknown":
116
+ return "border-white/10 bg-white/[0.04] text-white/45";
117
+ }
118
+ }
119
+
120
+ function versionLabel(pkg: EcosystemPackage): string {
121
+ if (pkg.installedVersion !== null && pkg.latestVersion !== null) {
122
+ if (pkg.installedVersion === pkg.latestVersion) return `v${pkg.installedVersion}`;
123
+ return `v${pkg.installedVersion} -> v${pkg.latestVersion}`;
124
+ }
125
+ if (pkg.installedVersion !== null) return `v${pkg.installedVersion}`;
126
+ if (pkg.latestVersion !== null) return `latest v${pkg.latestVersion}`;
127
+ return "not published";
128
+ }
129
+
130
+ function PackageStatusCard({ pkg }: { pkg: EcosystemPackage }): JSX.Element {
131
+ return (
132
+ <div className="rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
133
+ <div className="flex flex-wrap items-start justify-between gap-3">
134
+ <div className="min-w-0">
135
+ <div className="flex min-w-0 items-start gap-2">
136
+ <PackageCheck className="size-4 shrink-0 text-white/55" />
137
+ <div className="text-sm font-semibold leading-snug">{pkg.name}</div>
138
+ </div>
139
+ <div className="mt-1 break-all font-mono text-[11px] leading-snug text-white/35">
140
+ {pkg.npmName}
141
+ </div>
142
+ </div>
143
+ <Badge className={cn("shrink-0", stateClassName(pkg.state))} variant="outline">
144
+ {stateLabel(pkg.state)}
145
+ </Badge>
146
+ </div>
147
+ <div className="mt-3 text-xs leading-5 text-muted-foreground">{pkg.description}</div>
148
+ <div className="mt-3 flex flex-wrap items-center gap-2 text-[11px]">
149
+ <span className="rounded bg-white/[0.035] px-2 py-1 font-mono text-white/50">
150
+ {versionLabel(pkg)}
151
+ </span>
152
+ <span className="rounded bg-white/[0.035] px-2 py-1 text-white/45">{pkg.capability}</span>
153
+ <span className="rounded bg-white/[0.035] px-2 py-1 font-mono text-white/35">
154
+ {pkg.installCommand}
155
+ </span>
156
+ </div>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ function WorkflowTile({ workflow }: { workflow: WorkflowCard }): JSX.Element {
162
+ const Icon = workflow.icon;
163
+ return (
164
+ <div className="rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
165
+ <div className="flex items-start justify-between gap-3">
166
+ <div className="flex min-w-0 items-center gap-2">
167
+ <Icon className="size-4 shrink-0 text-cyan-200/75" />
168
+ <div className="text-sm font-semibold">{workflow.title}</div>
169
+ </div>
170
+ <span className="rounded bg-white/[0.04] px-2 py-0.5 font-mono text-[10px] text-white/45">
171
+ {workflow.label}
172
+ </span>
173
+ </div>
174
+ <div className="mt-2 min-h-10 text-xs leading-5 text-muted-foreground">
175
+ {workflow.description}
176
+ </div>
177
+ <div className="mt-3 font-mono text-[11px] text-white/35">{workflow.metric}</div>
178
+ </div>
179
+ );
180
+ }
181
+
182
+ function buildSessionCommand(sessionId: string | null): string {
183
+ if (sessionId === null) {
184
+ return 'npx @tonyclaw/eval-harness inspector-smoke --title "TonyClaw Lab smoke" --latest-log-limit 5';
185
+ }
186
+ return `npx @tonyclaw/eval-harness inspector-smoke --title "TonyClaw Lab smoke ${sessionId}" --latest-log-limit 5`;
187
+ }
188
+
189
+ function RunnerPresetCard({ preset }: { preset: EcosystemRunnerPreset }): JSX.Element {
190
+ return (
191
+ <div className="rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
192
+ <div className="flex flex-wrap items-start justify-between gap-3">
193
+ <div className="min-w-0">
194
+ <div className="flex min-w-0 items-start gap-2">
195
+ <Rocket className="size-4 shrink-0 text-cyan-200/75" />
196
+ <div className="text-sm font-semibold leading-snug">{preset.name}</div>
197
+ </div>
198
+ <div className="mt-1 break-all font-mono text-[11px] leading-snug text-white/35">
199
+ {preset.binary} {preset.modelFlag}
200
+ {preset.variantFlag !== null ? ` ${preset.variantFlag}` : ""}
201
+ </div>
202
+ </div>
203
+ <Badge variant="outline" className="border-white/10 bg-white/[0.04] text-white/55">
204
+ v{preset.validatedVersion}
205
+ </Badge>
206
+ </div>
207
+ <div className="mt-3 grid grid-cols-2 gap-2 text-[11px]">
208
+ <div className="rounded bg-black/15 px-2 py-1 text-white/45">logs/{preset.logDir}</div>
209
+ <div className="rounded bg-black/15 px-2 py-1 text-white/45">{preset.tools.join(", ")}</div>
210
+ <div className="rounded bg-black/15 px-2 py-1 font-mono text-white/35">
211
+ {preset.skillsDir}
212
+ </div>
213
+ <div className="rounded bg-black/15 px-2 py-1 font-mono text-white/35">
214
+ {preset.agentsDir}
215
+ </div>
216
+ </div>
217
+ </div>
218
+ );
219
+ }
220
+
221
+ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogProps): JSX.Element {
222
+ const [open, setOpen] = useState(false);
223
+ const [copied, setCopied] = useState(false);
224
+ const [copiedPresets, setCopiedPresets] = useState(false);
225
+ const response = useSWR("/api/ecosystem/packages", fetchEcosystemPackages, {
226
+ revalidateOnFocus: false,
227
+ revalidateIfStale: false,
228
+ });
229
+ const packages = response.data?.packages ?? [];
230
+ const runnerPresets = response.data?.runnerPresets ?? [];
231
+ const installedCount = useMemo(
232
+ () =>
233
+ packages.filter((pkg) => pkg.state === "installed" || pkg.state === "update-available")
234
+ .length,
235
+ [packages],
236
+ );
237
+ const sessionCommand = buildSessionCommand(currentSessionId);
238
+ const presetsCommand = "npx @tonyclaw/eval-harness runner-presets";
239
+
240
+ const copySessionCommand = (): void => {
241
+ void copyTextToClipboard(sessionCommand).then(() => {
242
+ setCopied(true);
243
+ window.setTimeout(() => setCopied(false), 1200);
244
+ });
245
+ };
246
+ const copyPresetsCommand = (): void => {
247
+ void copyTextToClipboard(presetsCommand).then(() => {
248
+ setCopiedPresets(true);
249
+ window.setTimeout(() => setCopiedPresets(false), 1200);
250
+ });
251
+ };
252
+
253
+ return (
254
+ <Dialog open={open} onOpenChange={setOpen}>
255
+ <DialogTrigger asChild>
256
+ <Button
257
+ variant="ghost"
258
+ size="icon"
259
+ className={INSPECTOR_ICON_TRIGGER_CLASS}
260
+ aria-label="TonyClaw Lab"
261
+ title="TonyClaw Lab"
262
+ >
263
+ <FlaskConical className="size-4" />
264
+ <span className="sr-only">TonyClaw Lab</span>
265
+ </Button>
266
+ </DialogTrigger>
267
+ <DialogContent className="flex max-h-[84vh] max-w-5xl flex-col overflow-hidden">
268
+ <DialogHeader>
269
+ <DialogTitle className="flex items-center gap-2">
270
+ <Sparkles className="size-4 text-cyan-200/80" />
271
+ TonyClaw Lab
272
+ </DialogTitle>
273
+ </DialogHeader>
274
+
275
+ <div className="overflow-y-auto pr-2">
276
+ <div className="grid gap-3 lg:grid-cols-[1.1fr_0.9fr]">
277
+ <section className="rounded-lg bg-white/[0.025] p-4 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
278
+ <div className="flex flex-wrap items-start justify-between gap-3">
279
+ <div>
280
+ <div className="text-sm font-semibold">Session asset pipeline</div>
281
+ <div className="mt-1 max-w-2xl text-xs leading-5 text-muted-foreground">
282
+ Treat captured traffic as durable assets: observe it, replay it, evaluate it,
283
+ and expose it back to agents through MCP.
284
+ </div>
285
+ </div>
286
+ <Badge variant="outline" className="border-cyan-300/20 bg-cyan-300/8 text-cyan-100">
287
+ {String(installedCount)}/{String(packages.length)} packages
288
+ </Badge>
289
+ </div>
290
+
291
+ <div className="mt-4 grid gap-3 sm:grid-cols-2">
292
+ {WORKFLOWS.map((workflow) => (
293
+ <WorkflowTile key={workflow.id} workflow={workflow} />
294
+ ))}
295
+ </div>
296
+ </section>
297
+
298
+ <section className="rounded-lg bg-white/[0.025] p-4 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
299
+ <div className="flex items-start justify-between gap-3">
300
+ <div>
301
+ <div className="text-sm font-semibold">Current session</div>
302
+ <div className="mt-1 text-xs text-muted-foreground">
303
+ {currentSessionId === null ? "MCP smoke command" : currentSessionId}
304
+ </div>
305
+ </div>
306
+ <Badge variant="outline" className="border-white/10 bg-white/[0.04] text-white/55">
307
+ {String(logCount)} logs
308
+ </Badge>
309
+ </div>
310
+
311
+ <div className="mt-4 rounded-md bg-black/20 p-3 font-mono text-[11px] leading-5 text-white/55 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
312
+ {sessionCommand}
313
+ </div>
314
+
315
+ <div className="mt-3 flex flex-wrap items-center gap-2">
316
+ <Button size="sm" variant="secondary" onClick={copySessionCommand}>
317
+ {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
318
+ {copied ? "Copied" : "Copy"}
319
+ </Button>
320
+ <Button size="sm" variant="outline" onClick={copyPresetsCommand}>
321
+ <Beaker className="size-3.5" />
322
+ {copiedPresets ? "Presets copied" : "Runner Presets"}
323
+ </Button>
324
+ </div>
325
+ </section>
326
+ </div>
327
+
328
+ <section className="mt-3">
329
+ <div className="mb-2 flex items-center justify-between gap-3">
330
+ <div className="text-sm font-semibold">Runner presets</div>
331
+ <div className="hidden items-center gap-1 font-mono text-[11px] text-muted-foreground sm:flex">
332
+ <Terminal className="size-3.5" />
333
+ {presetsCommand}
334
+ </div>
335
+ </div>
336
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
337
+ {runnerPresets.map((preset) => (
338
+ <RunnerPresetCard key={preset.id} preset={preset} />
339
+ ))}
340
+ {response.isLoading &&
341
+ runnerPresets.length === 0 &&
342
+ [0, 1, 2, 3].map((index) => (
343
+ <div
344
+ key={index}
345
+ className="h-32 animate-pulse rounded-lg bg-white/[0.025] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]"
346
+ />
347
+ ))}
348
+ </div>
349
+ </section>
350
+
351
+ <section className="mt-3">
352
+ <div className="mb-2 flex items-center justify-between gap-3">
353
+ <div className="text-sm font-semibold">TonyClaw packages</div>
354
+ {response.isLoading && (
355
+ <div className="font-mono text-[11px] text-muted-foreground">checking npm...</div>
356
+ )}
357
+ </div>
358
+ {response.error !== undefined && (
359
+ <div className="rounded-lg bg-red-500/8 p-3 text-xs text-red-100 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.16)]">
360
+ Package status is temporarily unavailable.
361
+ </div>
362
+ )}
363
+ <div className="grid gap-3 md:grid-cols-3">
364
+ {packages.map((pkg) => (
365
+ <PackageStatusCard key={pkg.id} pkg={pkg} />
366
+ ))}
367
+ {response.isLoading &&
368
+ packages.length === 0 &&
369
+ [0, 1, 2].map((index) => (
370
+ <div
371
+ key={index}
372
+ className="h-36 animate-pulse rounded-lg bg-white/[0.025] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]"
373
+ />
374
+ ))}
375
+ </div>
376
+ </section>
377
+ </div>
378
+ </DialogContent>
379
+ </Dialog>
380
+ );
381
+ }
@@ -598,7 +598,7 @@ function GroupList({
598
598
  )}
599
599
  >
600
600
  <div className="flex min-w-0 items-center justify-between gap-2">
601
- <span className="min-w-0 truncate text-sm font-medium">{group.title}</span>
601
+ <span className="min-w-0 text-sm font-medium leading-snug">{group.title}</span>
602
602
  <Badge variant="outline" className={statusClassName(group.status)}>
603
603
  {statusLabel(group.status)}
604
604
  </Badge>
@@ -673,7 +673,7 @@ function GroupDetails({
673
673
  <div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
674
674
  <div className="min-w-0 space-y-1">
675
675
  <div className="flex min-w-0 flex-wrap items-center gap-2">
676
- <h3 className="min-w-0 truncate text-base font-semibold">{group.title}</h3>
676
+ <h3 className="min-w-0 text-base font-semibold leading-snug">{group.title}</h3>
677
677
  <Badge variant="outline" className={statusClassName(group.status)}>
678
678
  {statusLabel(group.status)}
679
679
  </Badge>
@@ -1119,7 +1119,7 @@ function MemberRow({
1119
1119
  </div>
1120
1120
  </td>
1121
1121
  <td className="max-w-52 px-3 py-2">
1122
- <span className="block truncate">{providerModel(member)}</span>
1122
+ <span className="block whitespace-normal leading-snug">{providerModel(member)}</span>
1123
1123
  </td>
1124
1124
  <td className="px-3 py-2">
1125
1125
  <Badge variant="outline" className={statusClassName(member.status)}>
@@ -295,7 +295,7 @@ export function ImportWizardDialog({
295
295
  <span className="text-[10px] text-muted-foreground">Already added</span>
296
296
  )}
297
297
  </div>
298
- <div className="text-xs text-muted-foreground mt-1 truncate">
298
+ <div className="mt-1 text-xs leading-snug text-muted-foreground">
299
299
  {p.models.slice(0, 4).join(", ")}
300
300
  {p.models.length > 4 ? ` +${p.models.length - 4} more` : ""}
301
301
  </div>
@@ -730,13 +730,13 @@ export function ProviderCard({
730
730
  <div
731
731
  className={`flex flex-col gap-3 rounded-[8px] border border-border bg-card p-4 shadow-sm transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`}
732
732
  >
733
- <div className="flex items-start justify-between gap-2">
733
+ <div className="flex flex-wrap items-start justify-between gap-2">
734
734
  <div className="min-w-0 space-y-1">
735
- <div className="flex min-w-0 items-center gap-2">
736
- <span className="truncate font-medium">{provider.name}</span>
735
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
736
+ <span className="font-medium leading-snug">{provider.name}</span>
737
737
  {providerSourceBadge}
738
738
  </div>
739
- <div className="truncate text-xs text-muted-foreground">
739
+ <div className="text-xs leading-snug text-muted-foreground">
740
740
  {compactModelLabel(provider)}
741
741
  </div>
742
742
  </div>
@@ -573,7 +573,7 @@ function SideSummary({
573
573
  #{displayNumber}
574
574
  </span>
575
575
  {log.model !== null && (
576
- <span className="font-mono text-muted-foreground truncate">{log.model}</span>
576
+ <span className="font-mono leading-snug text-muted-foreground">{log.model}</span>
577
577
  )}
578
578
  </div>
579
579
  <div className="flex items-center gap-3 text-muted-foreground font-mono">
@@ -237,11 +237,11 @@ export function ConversationHeader({
237
237
 
238
238
  {showClientIdentity && clientDisplayLabel !== null && clientDisplayLabel !== "" && (
239
239
  <span
240
- className="bg-white/[0.045] inline-flex h-6 max-w-[180px] shrink-0 items-center gap-1 rounded-md px-1.5 text-xs text-muted-foreground"
240
+ className="bg-white/[0.045] inline-flex min-h-6 shrink-0 flex-wrap items-center gap-1 rounded-md px-1.5 py-0.5 text-xs text-muted-foreground"
241
241
  title={clientTooltip ?? clientDisplayLabel}
242
242
  >
243
243
  <ClientLogo client={clientApp} className="size-4 shrink-0" />
244
- <span className="truncate font-mono tabular-nums">{clientDisplayLabel}</span>
244
+ <span className="font-mono leading-snug tabular-nums">{clientDisplayLabel}</span>
245
245
  </span>
246
246
  )}
247
247
 
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+
3
+ export const EcosystemPackageStateSchema = z.enum([
4
+ "installed",
5
+ "update-available",
6
+ "available",
7
+ "planned",
8
+ "unknown",
9
+ ]);
10
+
11
+ export const EcosystemPackageSchema = z.object({
12
+ id: z.string(),
13
+ name: z.string(),
14
+ npmName: z.string(),
15
+ capability: z.string(),
16
+ description: z.string(),
17
+ workflow: z.string(),
18
+ installCommand: z.string(),
19
+ state: EcosystemPackageStateSchema,
20
+ installedVersion: z.string().nullable(),
21
+ latestVersion: z.string().nullable(),
22
+ primary: z.boolean(),
23
+ });
24
+
25
+ export const EcosystemRunnerPresetSchema = z.object({
26
+ id: z.string(),
27
+ name: z.string(),
28
+ validatedVersion: z.string(),
29
+ binary: z.string(),
30
+ modelFlag: z.string(),
31
+ variantFlag: z.string().nullable(),
32
+ tools: z.array(z.string()),
33
+ logDir: z.string(),
34
+ skillsDir: z.string(),
35
+ agentsDir: z.string(),
36
+ });
37
+
38
+ export const EcosystemPackagesResponseSchema = z.object({
39
+ checkedAt: z.string(),
40
+ packages: z.array(EcosystemPackageSchema),
41
+ runnerPresets: z.array(EcosystemRunnerPresetSchema),
42
+ });
43
+
44
+ export type EcosystemPackageState = z.infer<typeof EcosystemPackageStateSchema>;
45
+ export type EcosystemPackage = z.infer<typeof EcosystemPackageSchema>;
46
+ export type EcosystemRunnerPreset = z.infer<typeof EcosystemRunnerPresetSchema>;
47
+ export type EcosystemPackagesResponse = z.infer<typeof EcosystemPackagesResponseSchema>;