@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.
- package/.output/cli.js +245 -171
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-C0Kbsm-Q.js → CompareDrawer-CJKL4LMm.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DBUy29O-.js +106 -0
- package/.output/public/assets/ReplayDialog-BYanl-a_.js +1 -0
- package/.output/public/assets/{RequestAnatomy-fa0JXy5p.js → RequestAnatomy-DvPz1jqE.js} +1 -1
- package/.output/public/assets/ResponseView-sZnxp9GN.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-G-tiYiBd.js +1 -0
- package/.output/public/assets/{_sessionId-D5fYH2C0.js → _sessionId-_hDcsmXq.js} +1 -1
- package/.output/public/assets/agent-inspector-58K1_MsC.ico +0 -0
- package/.output/public/assets/index-DAPAWsAd.js +1 -0
- package/.output/public/assets/index-DspnLqUW.css +1 -0
- package/.output/public/assets/{index-BEsqvywM.js → index-SuOSQb2_.js} +1 -1
- package/.output/public/assets/{json-viewer-B0QRbMSf.js → json-viewer-yQdhgjaw.js} +1 -1
- package/.output/public/assets/{main-CuKZD7n0.js → main-CgjcDxjR.js} +2 -2
- package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -2
- package/.output/server/{_sessionId-DdBTK3qm.mjs → _sessionId-BGISf2En.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-m82cVZ73.mjs → CompareDrawer-s-UF9gW3.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-7geuf78f.mjs → ProxyViewerContainer-C0Sbz9ED.mjs} +1033 -530
- package/.output/server/_ssr/{ReplayDialog-B8RlWLLH.mjs → ReplayDialog-DvS8gvJr.mjs} +58 -23
- package/.output/server/_ssr/{RequestAnatomy-BE7pmwpv.mjs → RequestAnatomy-BwhWglJL.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-D-sDsdQr.mjs → ResponseView-wBVreqQf.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DU_hThJr.mjs → StreamingChunkSequence-Dr6eg-MJ.mjs} +30 -20
- package/.output/server/_ssr/{index-D0sV8UbE.mjs → index-B98Wia1X.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-CaDh1zrw.mjs → json-viewer-D7L8y0FH.mjs} +2 -2
- package/.output/server/_ssr/{router-Zh2iZ_u0.mjs → router-g9mo0nWT.mjs} +459 -196
- package/.output/server/_tanstack-start-manifest_v-CbGc-o50.mjs +4 -0
- package/.output/server/index.mjs +76 -69
- package/package.json +1 -1
- package/src/assets/IDE/opencode.svg +1 -1
- package/src/assets/IDE/xiaomimimo.svg +1 -1
- package/src/cli/detect-tools.ts +2 -27
- package/src/cli/templates/codex-skill-onboard.ts +1 -1
- package/src/cli/templates/skill-onboard.ts +2 -3
- package/src/cli.ts +9 -86
- package/src/components/ProxyViewer.tsx +249 -37
- package/src/components/ProxyViewerContainer.tsx +167 -22
- package/src/components/clients/ClientLogo.tsx +38 -3
- package/src/components/ecosystem/AgentLabDialog.tsx +204 -3
- package/src/components/groups/GroupsDialog.tsx +28 -40
- package/src/components/providers/ImportWizardDialog.tsx +51 -101
- package/src/components/providers/ProvidersPanel.tsx +82 -68
- package/src/components/providers/SettingsDialog.tsx +11 -12
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +15 -23
- package/src/components/proxy-viewer/ReplayDialog.tsx +65 -22
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +33 -20
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -16
- package/src/components/proxy-viewer/log-formats/anthropic.ts +5 -5
- package/src/components/proxy-viewer/log-formats/openai.ts +7 -10
- package/src/components/proxy-viewer/requestTools.ts +5 -2
- package/src/lib/apiClient.ts +43 -10
- package/src/lib/ecosystemContract.ts +32 -0
- package/src/lib/export-logs.ts +37 -3
- package/src/proxy/ecosystemRegistry.ts +244 -0
- package/src/proxy/ecosystemTasks.ts +153 -0
- package/src/proxy/platformCommands.ts +212 -0
- package/src/proxy/socketTracker.ts +84 -143
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +19 -0
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +16 -0
- package/src/routes/api/ecosystem.packages.ts +1 -248
- package/src/routes/api/ecosystem.tasks.$taskId.ts +16 -0
- package/src/routes/api/ecosystem.tasks.ts +12 -0
- package/src/routes/api/providers.$providerId.test.log.ts +28 -24
- package/.output/public/assets/ProxyViewerContainer-p-rpYyiF.js +0 -106
- package/.output/public/assets/ReplayDialog-CM3dhP8v.js +0 -1
- package/.output/public/assets/ResponseView-Bq6sC7Ay.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-CGOun3Df.js +0 -1
- package/.output/public/assets/index-C29Olfzt.js +0 -1
- package/.output/public/assets/index-DgANE0r3.css +0 -1
- 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
|
|
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
|
|
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 {
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)
|
|
383
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
)}
|