@tonyclaw/agent-inspector 3.0.7 → 3.0.9

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 (73) hide show
  1. package/.output/cli.js +245 -171
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-C0Kbsm-Q.js → CompareDrawer-CJKL4LMm.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DBUy29O-.js +106 -0
  5. package/.output/public/assets/ReplayDialog-BYanl-a_.js +1 -0
  6. package/.output/public/assets/{RequestAnatomy-fa0JXy5p.js → RequestAnatomy-DvPz1jqE.js} +1 -1
  7. package/.output/public/assets/ResponseView-sZnxp9GN.js +3 -0
  8. package/.output/public/assets/StreamingChunkSequence-G-tiYiBd.js +1 -0
  9. package/.output/public/assets/{_sessionId-D5fYH2C0.js → _sessionId-_hDcsmXq.js} +1 -1
  10. package/.output/public/assets/agent-inspector-58K1_MsC.ico +0 -0
  11. package/.output/public/assets/index-DAPAWsAd.js +1 -0
  12. package/.output/public/assets/index-DspnLqUW.css +1 -0
  13. package/.output/public/assets/{index-BEsqvywM.js → index-SuOSQb2_.js} +1 -1
  14. package/.output/public/assets/{json-viewer-B0QRbMSf.js → json-viewer-yQdhgjaw.js} +1 -1
  15. package/.output/public/assets/{main-CuKZD7n0.js → main-CgjcDxjR.js} +2 -2
  16. package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -2
  17. package/.output/server/{_sessionId-DdBTK3qm.mjs → _sessionId-BGISf2En.mjs} +3 -3
  18. package/.output/server/_ssr/{CompareDrawer-m82cVZ73.mjs → CompareDrawer-s-UF9gW3.mjs} +2 -2
  19. package/.output/server/_ssr/{ProxyViewerContainer-7geuf78f.mjs → ProxyViewerContainer-C0Sbz9ED.mjs} +1033 -530
  20. package/.output/server/_ssr/{ReplayDialog-B8RlWLLH.mjs → ReplayDialog-DvS8gvJr.mjs} +58 -23
  21. package/.output/server/_ssr/{RequestAnatomy-BE7pmwpv.mjs → RequestAnatomy-BwhWglJL.mjs} +2 -2
  22. package/.output/server/_ssr/{ResponseView-D-sDsdQr.mjs → ResponseView-wBVreqQf.mjs} +2 -2
  23. package/.output/server/_ssr/{StreamingChunkSequence-DU_hThJr.mjs → StreamingChunkSequence-Dr6eg-MJ.mjs} +30 -20
  24. package/.output/server/_ssr/{index-D0sV8UbE.mjs → index-B98Wia1X.mjs} +2 -2
  25. package/.output/server/_ssr/index.mjs +2 -2
  26. package/.output/server/_ssr/{json-viewer-CaDh1zrw.mjs → json-viewer-D7L8y0FH.mjs} +2 -2
  27. package/.output/server/_ssr/{router-Zh2iZ_u0.mjs → router-g9mo0nWT.mjs} +459 -196
  28. package/.output/server/_tanstack-start-manifest_v-CbGc-o50.mjs +4 -0
  29. package/.output/server/index.mjs +76 -69
  30. package/package.json +1 -1
  31. package/src/assets/IDE/opencode.svg +1 -1
  32. package/src/assets/IDE/xiaomimimo.svg +1 -1
  33. package/src/cli/detect-tools.ts +2 -27
  34. package/src/cli/templates/codex-skill-onboard.ts +1 -1
  35. package/src/cli/templates/skill-onboard.ts +2 -3
  36. package/src/cli.ts +9 -86
  37. package/src/components/ProxyViewer.tsx +249 -37
  38. package/src/components/ProxyViewerContainer.tsx +167 -22
  39. package/src/components/clients/ClientLogo.tsx +38 -3
  40. package/src/components/ecosystem/AgentLabDialog.tsx +204 -3
  41. package/src/components/groups/GroupsDialog.tsx +28 -40
  42. package/src/components/providers/ImportWizardDialog.tsx +51 -101
  43. package/src/components/providers/ProvidersPanel.tsx +82 -68
  44. package/src/components/providers/SettingsDialog.tsx +11 -12
  45. package/src/components/proxy-viewer/AgentTraceSummary.tsx +15 -23
  46. package/src/components/proxy-viewer/ReplayDialog.tsx +65 -22
  47. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +33 -20
  48. package/src/components/proxy-viewer/TurnGroup.tsx +0 -16
  49. package/src/components/proxy-viewer/log-formats/anthropic.ts +5 -5
  50. package/src/components/proxy-viewer/log-formats/openai.ts +7 -10
  51. package/src/components/proxy-viewer/requestTools.ts +5 -2
  52. package/src/lib/apiClient.ts +43 -10
  53. package/src/lib/ecosystemContract.ts +32 -0
  54. package/src/lib/export-logs.ts +37 -3
  55. package/src/proxy/ecosystemRegistry.ts +244 -0
  56. package/src/proxy/ecosystemTasks.ts +153 -0
  57. package/src/proxy/platformCommands.ts +212 -0
  58. package/src/proxy/socketTracker.ts +84 -143
  59. package/src/routes/api/ecosystem.packages.$packageId.help.ts +16 -0
  60. package/src/routes/api/ecosystem.packages.$packageId.install.ts +16 -0
  61. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +19 -0
  62. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +16 -0
  63. package/src/routes/api/ecosystem.packages.ts +1 -248
  64. package/src/routes/api/ecosystem.tasks.$taskId.ts +16 -0
  65. package/src/routes/api/ecosystem.tasks.ts +12 -0
  66. package/src/routes/api/providers.$providerId.test.log.ts +28 -24
  67. package/.output/public/assets/ProxyViewerContainer-p-rpYyiF.js +0 -106
  68. package/.output/public/assets/ReplayDialog-CM3dhP8v.js +0 -1
  69. package/.output/public/assets/ResponseView-Bq6sC7Ay.js +0 -3
  70. package/.output/public/assets/StreamingChunkSequence-CGOun3Df.js +0 -1
  71. package/.output/public/assets/index-C29Olfzt.js +0 -1
  72. package/.output/public/assets/index-DgANE0r3.css +0 -1
  73. package/.output/server/_tanstack-start-manifest_v-LCE-2AaF.mjs +0 -4
@@ -1,10 +1,12 @@
1
- import { type JSX, useMemo, useState } from "react";
1
+ import { type JSX, useEffect, useMemo, useState } from "react";
2
2
  import {
3
3
  Activity,
4
4
  Beaker,
5
5
  Check,
6
6
  Copy,
7
+ Download,
7
8
  FlaskConical,
9
+ Loader2,
8
10
  Network,
9
11
  PackageCheck,
10
12
  RefreshCw,
@@ -15,9 +17,12 @@ import {
15
17
  import useSWR from "swr";
16
18
  import {
17
19
  EcosystemPackagesResponseSchema,
20
+ EcosystemTaskResponseSchema,
18
21
  type EcosystemPackage,
19
22
  type EcosystemPackageState,
20
23
  type EcosystemRunnerPreset,
24
+ type EcosystemTask,
25
+ type EcosystemTaskAction,
21
26
  } from "../../lib/ecosystemContract";
22
27
  import { fetchJson } from "../../lib/apiClient";
23
28
  import { copyTextToClipboard } from "../../lib/clipboard";
@@ -87,6 +92,28 @@ async function fetchEcosystemPackages(url: string) {
87
92
  );
88
93
  }
89
94
 
95
+ async function fetchEcosystemTask(url: string) {
96
+ return fetchJson(
97
+ url,
98
+ EcosystemTaskResponseSchema,
99
+ undefined,
100
+ (response) => `Failed to load ecosystem task: ${String(response.status)}`,
101
+ );
102
+ }
103
+
104
+ async function startEcosystemTask(
105
+ packageId: string,
106
+ action: EcosystemTaskAction,
107
+ ): Promise<EcosystemTask> {
108
+ const response = await fetchJson(
109
+ `/api/ecosystem/packages/${encodeURIComponent(packageId)}/${action}`,
110
+ EcosystemTaskResponseSchema,
111
+ { method: "POST" },
112
+ (result) => `Failed to start ecosystem task: ${String(result.status)}`,
113
+ );
114
+ return response.task;
115
+ }
116
+
90
117
  function stateLabel(state: EcosystemPackageState): string {
91
118
  switch (state) {
92
119
  case "installed":
@@ -127,7 +154,57 @@ function versionLabel(pkg: EcosystemPackage): string {
127
154
  return "not published";
128
155
  }
129
156
 
130
- function PackageStatusCard({ pkg }: { pkg: EcosystemPackage }): JSX.Element {
157
+ function taskActionLabel(action: EcosystemTaskAction): string {
158
+ switch (action) {
159
+ case "install":
160
+ return "Install";
161
+ case "upgrade":
162
+ return "Upgrade";
163
+ case "help":
164
+ return "Help";
165
+ case "runner-presets":
166
+ return "Presets";
167
+ }
168
+ }
169
+
170
+ function taskStatusLabel(task: EcosystemTask): string {
171
+ switch (task.status) {
172
+ case "queued":
173
+ return "Queued";
174
+ case "running":
175
+ return `${taskActionLabel(task.action)} running`;
176
+ case "succeeded":
177
+ return `${taskActionLabel(task.action)} complete`;
178
+ case "failed":
179
+ return `${taskActionLabel(task.action)} failed`;
180
+ }
181
+ }
182
+
183
+ function packagePrimaryAction(pkg: EcosystemPackage): EcosystemTaskAction | null {
184
+ switch (pkg.state) {
185
+ case "available":
186
+ return "install";
187
+ case "update-available":
188
+ return "upgrade";
189
+ case "installed":
190
+ case "planned":
191
+ case "unknown":
192
+ return null;
193
+ }
194
+ }
195
+
196
+ function PackageStatusCard({
197
+ pkg,
198
+ activeTask,
199
+ onStartTask,
200
+ }: {
201
+ pkg: EcosystemPackage;
202
+ activeTask: EcosystemTask | null;
203
+ onStartTask: (packageId: string, action: EcosystemTaskAction) => void;
204
+ }): JSX.Element {
205
+ const primaryAction = packagePrimaryAction(pkg);
206
+ const packageTaskRunning =
207
+ activeTask !== null && activeTask.packageId === pkg.id && activeTask.status === "running";
131
208
  return (
132
209
  <div className="rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
133
210
  <div className="flex flex-wrap items-start justify-between gap-3">
@@ -154,10 +231,87 @@ function PackageStatusCard({ pkg }: { pkg: EcosystemPackage }): JSX.Element {
154
231
  {pkg.installCommand}
155
232
  </span>
156
233
  </div>
234
+ <div className="mt-3 flex flex-wrap items-center gap-2">
235
+ {primaryAction !== null && (
236
+ <Button
237
+ type="button"
238
+ size="sm"
239
+ variant="secondary"
240
+ disabled={packageTaskRunning}
241
+ onClick={() => onStartTask(pkg.id, primaryAction)}
242
+ >
243
+ {packageTaskRunning ? (
244
+ <Loader2 className="size-3.5 animate-spin" />
245
+ ) : (
246
+ <Download className="size-3.5" />
247
+ )}
248
+ {taskActionLabel(primaryAction)}
249
+ </Button>
250
+ )}
251
+ <Button
252
+ type="button"
253
+ size="sm"
254
+ variant="outline"
255
+ disabled={packageTaskRunning || pkg.state === "planned"}
256
+ onClick={() => onStartTask(pkg.id, "help")}
257
+ >
258
+ <Terminal className="size-3.5" />
259
+ Help
260
+ </Button>
261
+ {pkg.id === "eval-harness" && (
262
+ <Button
263
+ type="button"
264
+ size="sm"
265
+ variant="ghost"
266
+ disabled={packageTaskRunning || pkg.state === "planned"}
267
+ onClick={() => onStartTask(pkg.id, "runner-presets")}
268
+ >
269
+ <Beaker className="size-3.5" />
270
+ Presets
271
+ </Button>
272
+ )}
273
+ </div>
157
274
  </div>
158
275
  );
159
276
  }
160
277
 
278
+ function TaskConsole({ task }: { task: EcosystemTask | null }): JSX.Element | null {
279
+ if (task === null) return null;
280
+ const isRunning = task.status === "queued" || task.status === "running";
281
+ return (
282
+ <section className="mt-3 rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
283
+ <div className="flex flex-wrap items-center justify-between gap-3">
284
+ <div className="flex min-w-0 items-center gap-2">
285
+ {isRunning ? (
286
+ <Loader2 className="size-4 animate-spin text-cyan-200/75" />
287
+ ) : (
288
+ <Terminal className="size-4 text-white/55" />
289
+ )}
290
+ <div className="min-w-0">
291
+ <div className="text-sm font-semibold">{taskStatusLabel(task)}</div>
292
+ <div className="break-all font-mono text-[11px] text-white/35">{task.command}</div>
293
+ </div>
294
+ </div>
295
+ <Badge
296
+ variant="outline"
297
+ className={cn(
298
+ "shrink-0",
299
+ task.status === "failed"
300
+ ? "border-red-400/20 bg-red-400/8 text-red-100"
301
+ : "border-cyan-300/20 bg-cyan-300/8 text-cyan-100",
302
+ )}
303
+ >
304
+ {task.status}
305
+ </Badge>
306
+ </div>
307
+ <pre className="mt-3 max-h-56 overflow-auto whitespace-pre-wrap rounded-md bg-black/30 p-3 font-mono text-[11px] leading-5 text-white/60 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
308
+ {task.output.trim().length > 0 ? task.output : "Waiting for npm output..."}
309
+ </pre>
310
+ {task.error !== null && <div className="mt-2 text-xs text-red-100">{task.error}</div>}
311
+ </section>
312
+ );
313
+ }
314
+
161
315
  function WorkflowTile({ workflow }: { workflow: WorkflowCard }): JSX.Element {
162
316
  const Icon = workflow.icon;
163
317
  return (
@@ -222,10 +376,25 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
222
376
  const [open, setOpen] = useState(false);
223
377
  const [copied, setCopied] = useState(false);
224
378
  const [copiedPresets, setCopiedPresets] = useState(false);
379
+ const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
380
+ const [taskError, setTaskError] = useState<string | null>(null);
225
381
  const response = useSWR("/api/ecosystem/packages", fetchEcosystemPackages, {
226
382
  revalidateOnFocus: false,
227
383
  revalidateIfStale: false,
228
384
  });
385
+ const activeTaskResponse = useSWR(
386
+ activeTaskId === null ? null : `/api/ecosystem/tasks/${activeTaskId}`,
387
+ fetchEcosystemTask,
388
+ {
389
+ refreshInterval: (taskResponse) => {
390
+ const task = taskResponse?.task;
391
+ return task !== undefined && (task.status === "queued" || task.status === "running")
392
+ ? 900
393
+ : 0;
394
+ },
395
+ revalidateOnFocus: false,
396
+ },
397
+ );
229
398
  const packages = response.data?.packages ?? [];
230
399
  const runnerPresets = response.data?.runnerPresets ?? [];
231
400
  const installedCount = useMemo(
@@ -250,6 +419,26 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
250
419
  });
251
420
  };
252
421
 
422
+ const activeTask = activeTaskResponse.data?.task ?? null;
423
+
424
+ useEffect(() => {
425
+ if (activeTask === null) return;
426
+ if (activeTask.status !== "succeeded") return;
427
+ void response.mutate();
428
+ }, [activeTask, response]);
429
+
430
+ const handleStartTask = (packageId: string, action: EcosystemTaskAction): void => {
431
+ setTaskError(null);
432
+ void startEcosystemTask(packageId, action)
433
+ .then((task) => {
434
+ setActiveTaskId(task.id);
435
+ void activeTaskResponse.mutate({ task }, { revalidate: true });
436
+ })
437
+ .catch((error: unknown) => {
438
+ setTaskError(error instanceof Error ? error.message : "Failed to start ecosystem task");
439
+ });
440
+ };
441
+
253
442
  return (
254
443
  <Dialog open={open} onOpenChange={setOpen}>
255
444
  <DialogTrigger asChild>
@@ -362,7 +551,12 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
362
551
  )}
363
552
  <div className="grid gap-3 md:grid-cols-3">
364
553
  {packages.map((pkg) => (
365
- <PackageStatusCard key={pkg.id} pkg={pkg} />
554
+ <PackageStatusCard
555
+ key={pkg.id}
556
+ pkg={pkg}
557
+ activeTask={activeTask}
558
+ onStartTask={handleStartTask}
559
+ />
366
560
  ))}
367
561
  {response.isLoading &&
368
562
  packages.length === 0 &&
@@ -374,6 +568,13 @@ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogPro
374
568
  ))}
375
569
  </div>
376
570
  </section>
571
+
572
+ {taskError !== null && (
573
+ <div className="mt-3 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)]">
574
+ {taskError}
575
+ </div>
576
+ )}
577
+ <TaskConsole task={activeTask} />
377
578
  </div>
378
579
  </DialogContent>
379
580
  </Dialog>
@@ -16,7 +16,7 @@ import { Badge } from "../ui/badge";
16
16
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
17
17
  import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
18
18
  import { cn } from "../../lib/utils";
19
- import { parseJsonResponse, readApiError } from "../../lib/apiClient";
19
+ import { fetchJsonWithTimeout } from "../../lib/apiClient";
20
20
  import {
21
21
  DeleteInspectorGroupResponseSchema,
22
22
  DeleteInspectorGroupsResponseSchema,
@@ -53,6 +53,8 @@ type GroupMemberRow = {
53
53
  source: "group" | "export" | "evidence";
54
54
  };
55
55
 
56
+ const GROUP_OPERATION_TIMEOUT_MS = 20_000;
57
+
56
58
  export function groupDeletePath(groupId: string, deleteEvidence: boolean): string {
57
59
  const path = `/api/groups/${encodeURIComponent(groupId)}`;
58
60
  return deleteEvidence ? `${path}?deleteEvidence=1` : path;
@@ -333,19 +335,13 @@ export function GroupsDialog(): JSX.Element {
333
335
  setExportError(null);
334
336
 
335
337
  try {
336
- const response = await fetch(groupsClearPath(false), { method: "DELETE" });
337
- if (!response.ok) {
338
- showToast({
339
- message: await readApiError(
340
- response,
341
- `Clear failed with status ${String(response.status)}`,
342
- ),
343
- tone: "error",
344
- });
345
- return;
346
- }
347
-
348
- const parsed = await parseJsonResponse(response, DeleteInspectorGroupsResponseSchema);
338
+ const parsed = await fetchJsonWithTimeout(
339
+ groupsClearPath(false),
340
+ DeleteInspectorGroupsResponseSchema,
341
+ GROUP_OPERATION_TIMEOUT_MS,
342
+ { method: "DELETE" },
343
+ (response) => `Clear failed with status ${String(response.status)}`,
344
+ );
349
345
  setExportResult(null);
350
346
  setSelectedGroupId(null);
351
347
  await mutate();
@@ -370,20 +366,17 @@ export function GroupsDialog(): JSX.Element {
370
366
  setExportError(null);
371
367
 
372
368
  try {
373
- const response = await fetch(`/api/groups/${encodeURIComponent(group.id)}/evidence`, {
374
- method: "POST",
375
- headers: { "content-type": "application/json" },
376
- body: JSON.stringify({ latestLogLimit: 10 }),
377
- });
378
-
379
- if (!response.ok) {
380
- setExportError(
381
- await readApiError(response, `Export failed with status ${String(response.status)}`),
382
- );
383
- return;
384
- }
385
-
386
- const parsed = await parseJsonResponse(response, GroupEvidenceExportResultSchema);
369
+ const parsed = await fetchJsonWithTimeout(
370
+ `/api/groups/${encodeURIComponent(group.id)}/evidence`,
371
+ GroupEvidenceExportResultSchema,
372
+ GROUP_OPERATION_TIMEOUT_MS,
373
+ {
374
+ method: "POST",
375
+ headers: { "content-type": "application/json" },
376
+ body: JSON.stringify({ latestLogLimit: 10 }),
377
+ },
378
+ (response) => `Export failed with status ${String(response.status)}`,
379
+ );
387
380
  setExportResult(parsed);
388
381
  await mutate();
389
382
  await mutateEvidence();
@@ -424,18 +417,13 @@ export function GroupsDialog(): JSX.Element {
424
417
  setDeleteError(null);
425
418
 
426
419
  try {
427
- const response = await fetch(groupDeletePath(group.id, deleteEvidence), {
428
- method: "DELETE",
429
- });
430
-
431
- if (!response.ok) {
432
- setDeleteError(
433
- await readApiError(response, `Delete failed with status ${String(response.status)}`),
434
- );
435
- return;
436
- }
437
-
438
- await parseJsonResponse(response, DeleteInspectorGroupResponseSchema);
420
+ await fetchJsonWithTimeout(
421
+ groupDeletePath(group.id, deleteEvidence),
422
+ DeleteInspectorGroupResponseSchema,
423
+ GROUP_OPERATION_TIMEOUT_MS,
424
+ { method: "DELETE" },
425
+ (response) => `Delete failed with status ${String(response.status)}`,
426
+ );
439
427
  setExportResult((current) =>
440
428
  current !== null && current.group.id === group.id ? null : current,
441
429
  );
@@ -1,66 +1,12 @@
1
- import { type JSX, useState, useCallback, useEffect } from "react";
1
+ import { type JSX, useState, useCallback, useEffect, useRef } from "react";
2
2
  import { z } from "zod";
3
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
4
4
  import { Button } from "../ui/button";
5
+ import { fetchJsonWithTimeout } from "../../lib/apiClient";
6
+ import { ClientLogo } from "../clients/ClientLogo";
5
7
 
6
8
  import { Loader2, Download, AlertCircle } from "lucide-react";
7
9
 
8
- function ClaudeCodeIcon(): JSX.Element {
9
- return (
10
- <svg
11
- fill="currentColor"
12
- fillRule="evenodd"
13
- viewBox="0 0 24 24"
14
- xmlns="http://www.w3.org/2000/svg"
15
- className="size-6 shrink-0"
16
- >
17
- <title>Claude Code</title>
18
- <path
19
- clipRule="evenodd"
20
- d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"
21
- />
22
- </svg>
23
- );
24
- }
25
-
26
- function OpenCodeIcon(): JSX.Element {
27
- return (
28
- <svg
29
- fill="currentColor"
30
- fillRule="evenodd"
31
- viewBox="0 0 24 24"
32
- xmlns="http://www.w3.org/2000/svg"
33
- className="size-6 shrink-0"
34
- >
35
- <title>OpenCode</title>
36
- <path d="M16 6H8v12h8V6zm4 16H4V2h16v20z" />
37
- </svg>
38
- );
39
- }
40
-
41
- function MiMoCodeIcon(): JSX.Element {
42
- return (
43
- <svg
44
- fill="currentColor"
45
- viewBox="0 0 24 24"
46
- xmlns="http://www.w3.org/2000/svg"
47
- className="size-6 shrink-0"
48
- >
49
- <title>MiMo Code</title>
50
- <text
51
- x="12"
52
- y="18"
53
- textAnchor="middle"
54
- fontSize="16"
55
- fontWeight="800"
56
- fontFamily="system-ui, sans-serif"
57
- >
58
- M
59
- </text>
60
- </svg>
61
- );
62
- }
63
-
64
10
  const ExternalProviderSchema = z.object({
65
11
  name: z.string(),
66
12
  apiKey: z.string(),
@@ -85,11 +31,7 @@ const ImportResponseSchema = z.object({
85
31
  errors: z.array(z.string()).optional(),
86
32
  });
87
33
 
88
- const sourceLogoMap: Record<string, () => JSX.Element> = {
89
- "claude-code": ClaudeCodeIcon,
90
- opencode: OpenCodeIcon,
91
- "mimo-code": MiMoCodeIcon,
92
- };
34
+ const PROVIDER_IMPORT_TIMEOUT_MS = 20_000;
93
35
 
94
36
  type ImportWizardDialogProps = {
95
37
  open: boolean;
@@ -110,8 +52,12 @@ export function ImportWizardDialog({
110
52
  const [importing, setImporting] = useState(false);
111
53
  const [importResult, setImportResult] = useState<string | null>(null);
112
54
  const [importError, setImportError] = useState<string | null>(null);
55
+ const scanRequestIdRef = useRef(0);
56
+ const importRequestIdRef = useRef(0);
113
57
 
114
58
  const scan = useCallback(() => {
59
+ const requestId = scanRequestIdRef.current + 1;
60
+ scanRequestIdRef.current = requestId;
115
61
  setScanning(true);
116
62
  setScanError(null);
117
63
  setWarnings([]);
@@ -120,33 +66,29 @@ export function ImportWizardDialog({
120
66
  setImportResult(null);
121
67
  setImportError(null);
122
68
 
123
- fetch("/api/providers/scan")
124
- .then((res) => {
125
- if (!res.ok) {
126
- return res.text().then((text) => {
127
- setScanError(`Scan failed (${res.status}): ${text}`);
128
- });
129
- }
130
- return res.json().then((data: unknown) => {
131
- const parsed = ScanResponseSchema.safeParse(data);
132
- if (!parsed.success) {
133
- setScanError(`Invalid response: ${parsed.error.message}`);
134
- return;
135
- }
136
- setProviders(parsed.data.providers);
137
- setWarnings(parsed.data.warnings ?? []);
138
- // Pre-select non-existing providers
139
- const indices = new Set<number>();
140
- parsed.data.providers.forEach((p, i) => {
141
- if (!p.alreadyExists) indices.add(i);
142
- });
143
- setSelected(indices);
69
+ void fetchJsonWithTimeout(
70
+ "/api/providers/scan",
71
+ ScanResponseSchema,
72
+ PROVIDER_IMPORT_TIMEOUT_MS,
73
+ undefined,
74
+ (response) => `Scan failed (${String(response.status)})`,
75
+ )
76
+ .then((data) => {
77
+ if (scanRequestIdRef.current !== requestId) return;
78
+ setProviders(data.providers);
79
+ setWarnings(data.warnings ?? []);
80
+ const indices = new Set<number>();
81
+ data.providers.forEach((provider, index) => {
82
+ if (!provider.alreadyExists) indices.add(index);
144
83
  });
84
+ setSelected(indices);
145
85
  })
146
86
  .catch((err: unknown) => {
87
+ if (scanRequestIdRef.current !== requestId) return;
147
88
  setScanError(err instanceof Error ? err.message : String(err));
148
89
  })
149
90
  .finally(() => {
91
+ if (scanRequestIdRef.current !== requestId) return;
150
92
  setScanning(false);
151
93
  });
152
94
  }, []);
@@ -158,6 +100,14 @@ export function ImportWizardDialog({
158
100
  }
159
101
  }, [open, scan]);
160
102
 
103
+ useEffect(() => {
104
+ if (open) return;
105
+ scanRequestIdRef.current += 1;
106
+ importRequestIdRef.current += 1;
107
+ setScanning(false);
108
+ setImporting(false);
109
+ }, [open]);
110
+
161
111
  const toggleProvider = useCallback((index: number) => {
162
112
  setSelected((prev) => {
163
113
  const next = new Set(prev);
@@ -174,6 +124,8 @@ export function ImportWizardDialog({
174
124
  const toImport = providers.filter((_, i) => selected.has(i));
175
125
  if (toImport.length === 0) return;
176
126
 
127
+ const requestId = importRequestIdRef.current + 1;
128
+ importRequestIdRef.current = requestId;
177
129
  setImporting(true);
178
130
  setImportError(null);
179
131
  setImportResult(null);
@@ -193,19 +145,19 @@ export function ImportWizardDialog({
193
145
  updatedAt: now,
194
146
  }));
195
147
 
196
- fetch("/api/providers/import", {
197
- method: "POST",
198
- headers: { "Content-Type": "application/json" },
199
- body: JSON.stringify({ providers: providersPayload }),
200
- })
201
- .then((res) => res.json())
202
- .then((data: unknown) => {
203
- const parsed = ImportResponseSchema.safeParse(data);
204
- if (!parsed.success) {
205
- setImportError(`Invalid response: ${parsed.error.message}`);
206
- return;
207
- }
208
- const result = parsed.data;
148
+ void fetchJsonWithTimeout(
149
+ "/api/providers/import",
150
+ ImportResponseSchema,
151
+ PROVIDER_IMPORT_TIMEOUT_MS,
152
+ {
153
+ method: "POST",
154
+ headers: { "Content-Type": "application/json" },
155
+ body: JSON.stringify({ providers: providersPayload }),
156
+ },
157
+ (response) => `Import failed (${String(response.status)})`,
158
+ )
159
+ .then((result) => {
160
+ if (importRequestIdRef.current !== requestId) return;
209
161
  // Mark imported providers as already existing so they can't be re-selected
210
162
  setProviders((prev) =>
211
163
  prev.map((p, i) => (selected.has(i) ? { ...p, alreadyExists: true } : p)),
@@ -224,9 +176,11 @@ export function ImportWizardDialog({
224
176
  onImportComplete();
225
177
  })
226
178
  .catch((err: unknown) => {
179
+ if (importRequestIdRef.current !== requestId) return;
227
180
  setImportError(err instanceof Error ? err.message : String(err));
228
181
  })
229
182
  .finally(() => {
183
+ if (importRequestIdRef.current !== requestId) return;
230
184
  setImporting(false);
231
185
  });
232
186
  }, [providers, selected, onImportComplete]);
@@ -286,11 +240,7 @@ export function ImportWizardDialog({
286
240
  )}
287
241
  <div className="flex-1 min-w-0">
288
242
  <div className="flex items-center justify-between">
289
- {(() => {
290
- const LogoComponent = sourceLogoMap[p.sourceTool];
291
- if (LogoComponent === undefined) return null;
292
- return <LogoComponent />;
293
- })()}
243
+ <ClientLogo client={p.sourceTool} className="size-7 rounded-[8px]" />
294
244
  {p.alreadyExists && (
295
245
  <span className="text-[10px] text-muted-foreground">Already added</span>
296
246
  )}