@tonyclaw/agent-inspector 3.0.28 → 3.0.30

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 (156) hide show
  1. package/.output/cli.js +791 -191
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-B6cXJohL.js +1 -0
  4. package/.output/public/assets/InspectorPet-mAzeGmEE.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-CkFWv_Nm.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CZMApaF4.js → ReplayDialog-DodiTL6E.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-DNWccR7O.js → RequestAnatomy-B3IauToI.js} +1 -1
  8. package/.output/public/assets/ResponseView-Hn-5ordK.js +3 -0
  9. package/.output/public/assets/StreamingChunkSequence-vLDOVQM_.js +1 -0
  10. package/.output/public/assets/_sessionId-BFH4TOaI.js +1 -0
  11. package/.output/public/assets/{_sessionId-DRfvuE2k.js → _sessionId-Cj_HIFZx.js} +1 -1
  12. package/.output/public/assets/{index-D4HkS7Ct.js → index-Bmj2lcWE.js} +1 -1
  13. package/.output/public/assets/index-CgJj-HBC.css +1 -0
  14. package/.output/public/assets/index-Ch7uqnCT.js +1 -0
  15. package/.output/public/assets/index-DkDdKvLl.js +76 -0
  16. package/.output/public/assets/{index-B3aAuqWq.js → index-ZlhqCrSg.js} +6 -6
  17. package/.output/public/assets/{json-viewer-wuJSKaed.js → json-viewer-BdrnVQu-.js} +1 -1
  18. package/.output/public/assets/jszip.min-CS_nt_o1.js +2 -0
  19. package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
  20. package/.output/server/_libs/jszip.mjs +35 -9
  21. package/.output/server/_libs/lucide-react.mjs +86 -86
  22. package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
  23. package/.output/server/_libs/tanstack__react-router.mjs +1 -1
  24. package/.output/server/{_sessionId-BJTNiP_O.mjs → _sessionId-C5S3pGZd.mjs} +15 -15
  25. package/.output/server/_sessionId-g6D69lKq.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-BMRH3N97.mjs → CompareDrawer-CqUmGHal.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-DI0UJd01.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-AFwcHAIR.mjs → ProxyViewerContainer-CvsUmbJ3.mjs} +1216 -1659
  29. package/.output/server/_ssr/{ReplayDialog-CbWvRHiN.mjs → ReplayDialog-DS48dpFG.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-C4kcPyqN.mjs → RequestAnatomy-DxYVQU1C.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-D8VYt-gZ.mjs → ResponseView-BLXW73eq.mjs} +60 -60
  32. package/.output/server/_ssr/{StreamingChunkSequence-BUUo6vU0.mjs → StreamingChunkSequence-DD0iFuy0.mjs} +16 -16
  33. package/.output/server/_ssr/{index-DTwMEEXO.mjs → index-B10LIaK0.mjs} +15 -15
  34. package/.output/server/_ssr/index-RHVJnU2p.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BqF1pSQD.mjs → json-viewer-LDu2XBaK.mjs} +58 -58
  37. package/.output/server/_ssr/{router-C8BOns7y.mjs → router-GKvrwifJ.mjs} +9005 -6488
  38. package/.output/server/_tanstack-start-manifest_v-Df2n4mbT.mjs +4 -0
  39. package/.output/server/index.mjs +105 -77
  40. package/.output/workers/logFinalizer.worker.js +16908 -0
  41. package/.output/workers/sessionWorkerEntry.js +16904 -0
  42. package/README.md +21 -5
  43. package/package.json +6 -2
  44. package/src/assets/logos/qwen.webp +0 -0
  45. package/src/cli.ts +85 -10
  46. package/src/components/OnboardingBanner.tsx +41 -21
  47. package/src/components/ProxyViewer.tsx +198 -269
  48. package/src/components/ProxyViewerContainer.tsx +128 -17
  49. package/src/components/clients/ClientLogo.tsx +9 -6
  50. package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
  51. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  52. package/src/components/pi-agent/PiAgentPanel.tsx +4 -1
  53. package/src/components/providers/ImportWizardDialog.tsx +18 -42
  54. package/src/components/providers/ProviderLogo.tsx +1 -1
  55. package/src/components/providers/ProvidersPanel.tsx +8 -5
  56. package/src/components/providers/SettingsDialog.tsx +34 -11
  57. package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
  58. package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
  59. package/src/components/proxy-viewer/ConversationGroupList.tsx +49 -34
  60. package/src/components/proxy-viewer/ConversationHeader.tsx +22 -18
  61. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
  62. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  63. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  64. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  65. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  66. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  67. package/src/components/proxy-viewer/accessibility.ts +8 -0
  68. package/src/components/proxy-viewer/lazy.ts +4 -0
  69. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  70. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  71. package/src/components/proxy-viewer/viewerState.ts +2 -6
  72. package/src/components/ui/select.tsx +1 -1
  73. package/src/components/ui/tabs.tsx +3 -3
  74. package/src/components/ui/transient-toast.tsx +1 -1
  75. package/src/lib/apiClient.ts +17 -2
  76. package/src/lib/ecosystemContract.ts +34 -0
  77. package/src/lib/export-logs.ts +1 -1
  78. package/src/lib/providerContract.ts +70 -4
  79. package/src/lib/providerImportContract.ts +27 -0
  80. package/src/lib/providerModelMetadata.ts +16 -7
  81. package/src/lib/resourceLimits.ts +152 -0
  82. package/src/lib/safeDiagnostic.ts +38 -0
  83. package/src/lib/useProviders.ts +4 -4
  84. package/src/lib/utils.ts +5 -1
  85. package/src/mcp/server.ts +39 -6
  86. package/src/mcp/toolHandlers.ts +131 -4
  87. package/src/proxy/chunkStorage.ts +20 -6
  88. package/src/proxy/config.ts +20 -6
  89. package/src/proxy/dataDir.ts +3 -0
  90. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  91. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  92. package/src/proxy/ecosystemTasks.ts +48 -0
  93. package/src/proxy/evidenceExporter.ts +23 -9
  94. package/src/proxy/groupEvidenceExporter.ts +26 -5
  95. package/src/proxy/groupStore.ts +13 -3
  96. package/src/proxy/handler.ts +103 -20
  97. package/src/proxy/identityProxy.ts +333 -14
  98. package/src/proxy/logFinalizer.ts +73 -6
  99. package/src/proxy/logImportUpload.ts +128 -0
  100. package/src/proxy/logImporter.ts +321 -70
  101. package/src/proxy/logIndex.ts +16 -6
  102. package/src/proxy/logger.ts +294 -41
  103. package/src/proxy/privateDataPath.ts +183 -0
  104. package/src/proxy/providerScanStore.ts +87 -0
  105. package/src/proxy/providerSecretStore.ts +58 -19
  106. package/src/proxy/providers.ts +175 -58
  107. package/src/proxy/rawStreamCapture.ts +66 -5
  108. package/src/proxy/runStore.ts +13 -3
  109. package/src/proxy/runtimeAdmission.ts +52 -0
  110. package/src/proxy/runtimeHealth.ts +206 -0
  111. package/src/proxy/runtimeShutdown.ts +75 -0
  112. package/src/proxy/sessionArchive.ts +15 -2
  113. package/src/proxy/sessionProcess.ts +19 -0
  114. package/src/proxy/sessionRuntime.ts +7 -0
  115. package/src/proxy/shutdownCoordinator.ts +90 -0
  116. package/src/proxy/sqliteLogIndex.ts +18 -2
  117. package/src/proxy/store.ts +18 -2
  118. package/src/routes/__root.tsx +18 -1
  119. package/src/routes/api/alerts.summary.ts +12 -8
  120. package/src/routes/api/alerts.ts +27 -9
  121. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  122. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  123. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  124. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  125. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  126. package/src/routes/api/health.ts +9 -2
  127. package/src/routes/api/knowledge.project-context.ts +11 -0
  128. package/src/routes/api/knowledge.search.ts +33 -2
  129. package/src/routes/api/logs.$id.body.ts +16 -2
  130. package/src/routes/api/logs.import.ts +7 -18
  131. package/src/routes/api/logs.stream.ts +147 -69
  132. package/src/routes/api/logs.ts +55 -14
  133. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  134. package/src/routes/api/providers.$providerId.ts +37 -6
  135. package/src/routes/api/providers.export.ts +34 -16
  136. package/src/routes/api/providers.import.ts +42 -8
  137. package/src/routes/api/providers.scan.ts +13 -8
  138. package/src/routes/api/providers.ts +36 -5
  139. package/src/routes/api/runs.ts +12 -12
  140. package/src/routes/api/sessions.ts +15 -8
  141. package/src/routes/index.tsx +6 -0
  142. package/src/routes/livez.ts +13 -0
  143. package/src/routes/readyz.ts +18 -0
  144. package/src/routes/session/$sessionId.tsx +6 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-TghZr5t1.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-fShBAZjj.js +0 -4234
  148. package/.output/public/assets/ResponseView-d25MdFhY.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-DIFZEnWe.js +0 -1
  150. package/.output/public/assets/index-BsxAGqMA.css +0 -1
  151. package/.output/public/assets/index-DzARL2Fi.js +0 -76
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-BBf2nRhb.mjs +0 -4
  154. package/src/assets/logos/mcp.png +0 -0
  155. package/src/assets/logos/qwen.png +0 -0
  156. package/src/components/ui/mcp-logo.tsx +0 -20
@@ -0,0 +1,201 @@
1
+ import {
2
+ Component,
3
+ type ErrorInfo,
4
+ type JSX,
5
+ type ReactNode,
6
+ useCallback,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
10
+
11
+ import { copyTextToClipboard } from "../../lib/clipboard";
12
+ import { createSafeDiagnosticReference } from "../../lib/safeDiagnostic";
13
+ import { Button } from "../ui/button";
14
+ import { CrabLogo } from "../ui/crab-logo";
15
+
16
+ export type SafeErrorFallbackProps = {
17
+ reference: string;
18
+ title: string;
19
+ description: string;
20
+ variant: "page" | "feature";
21
+ onRetry?: () => void;
22
+ onReload?: () => void;
23
+ };
24
+
25
+ export function SafeErrorFallback({
26
+ reference,
27
+ title,
28
+ description,
29
+ variant,
30
+ onRetry,
31
+ onReload,
32
+ }: SafeErrorFallbackProps): JSX.Element {
33
+ const [copied, setCopied] = useState(false);
34
+ const handleCopy = useCallback(() => {
35
+ void copyTextToClipboard(reference).then((success) => {
36
+ if (!success) return;
37
+ setCopied(true);
38
+ });
39
+ }, [reference]);
40
+
41
+ const content = (
42
+ <div
43
+ role="alert"
44
+ className={
45
+ variant === "page"
46
+ ? "border-border/70 bg-card/80 rounded-lg border p-7 shadow-sm"
47
+ : "border-amber-400/20 bg-amber-500/[0.07] rounded-lg border p-4 shadow-sm"
48
+ }
49
+ >
50
+ <div className="flex items-center gap-3">
51
+ <CrabLogo className="size-9 shrink-0 text-amber-500" />
52
+ <div>
53
+ <div className="text-muted-foreground text-xs font-semibold uppercase tracking-wider">
54
+ Agent Inspector recovery
55
+ </div>
56
+ <h1 className={variant === "page" ? "mt-1 text-xl font-semibold" : "mt-1 font-semibold"}>
57
+ {title}
58
+ </h1>
59
+ </div>
60
+ </div>
61
+ <p className="text-muted-foreground mt-4 max-w-xl text-sm leading-6">{description}</p>
62
+ <div className="border-border/60 bg-background/70 mt-4 flex flex-wrap items-center gap-2 rounded-md border px-3 py-2">
63
+ <span className="text-muted-foreground text-xs">Diagnostic reference</span>
64
+ <code className="min-w-0 flex-1 break-all font-mono text-xs text-foreground">
65
+ {reference}
66
+ </code>
67
+ <Button type="button" variant="outline" size="sm" onClick={handleCopy}>
68
+ {copied ? "Copied" : "Copy reference"}
69
+ </Button>
70
+ </div>
71
+ <p className="text-muted-foreground mt-2 text-xs">
72
+ Error details are hidden to protect captured prompts, source content, headers, and secrets.
73
+ </p>
74
+ <div className="mt-5 flex flex-wrap gap-2">
75
+ {onRetry !== undefined && (
76
+ <Button type="button" size="sm" onClick={onRetry}>
77
+ Retry
78
+ </Button>
79
+ )}
80
+ {onReload !== undefined && (
81
+ <Button type="button" variant="outline" size="sm" onClick={onReload}>
82
+ Reload Inspector
83
+ </Button>
84
+ )}
85
+ </div>
86
+ </div>
87
+ );
88
+
89
+ if (variant === "feature") {
90
+ return content;
91
+ }
92
+
93
+ return (
94
+ <main className="min-h-screen bg-background text-foreground">
95
+ <div className="mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center px-6 py-16">
96
+ {content}
97
+ </div>
98
+ </main>
99
+ );
100
+ }
101
+
102
+ type SafeErrorBoundaryProps = {
103
+ children: ReactNode;
104
+ scope: string;
105
+ title: string;
106
+ description: string;
107
+ variant?: "page" | "feature";
108
+ onRetry?: () => void;
109
+ };
110
+
111
+ type SafeErrorBoundaryState = {
112
+ caughtAt: number;
113
+ error: unknown;
114
+ failed: boolean;
115
+ };
116
+
117
+ const INITIAL_ERROR_STATE: SafeErrorBoundaryState = {
118
+ caughtAt: 0,
119
+ error: null,
120
+ failed: false,
121
+ };
122
+
123
+ export class SafeErrorBoundary extends Component<SafeErrorBoundaryProps, SafeErrorBoundaryState> {
124
+ override state: SafeErrorBoundaryState = INITIAL_ERROR_STATE;
125
+
126
+ static getDerivedStateFromError(error: unknown): SafeErrorBoundaryState {
127
+ return { caughtAt: Date.now(), error, failed: true };
128
+ }
129
+
130
+ override componentDidCatch(_error: unknown, _errorInfo: ErrorInfo): void {
131
+ // The safe reference is intentionally the only error detail exposed by this boundary.
132
+ }
133
+
134
+ private readonly retry = (): void => {
135
+ this.setState(INITIAL_ERROR_STATE);
136
+ this.props.onRetry?.();
137
+ };
138
+
139
+ override render(): ReactNode {
140
+ if (!this.state.failed) {
141
+ return this.props.children;
142
+ }
143
+
144
+ const reference = createSafeDiagnosticReference(
145
+ this.props.scope,
146
+ this.state.error,
147
+ this.state.caughtAt,
148
+ );
149
+ return (
150
+ <SafeErrorFallback
151
+ reference={reference}
152
+ title={this.props.title}
153
+ description={this.props.description}
154
+ variant={this.props.variant ?? "feature"}
155
+ onRetry={this.retry}
156
+ onReload={() => window.location.reload()}
157
+ />
158
+ );
159
+ }
160
+ }
161
+
162
+ export function LazyFeatureBoundary({
163
+ feature,
164
+ children,
165
+ }: {
166
+ feature: string;
167
+ children: ReactNode;
168
+ }): JSX.Element {
169
+ return (
170
+ <SafeErrorBoundary
171
+ scope={`lazy-${feature}`}
172
+ title={`${feature} could not be displayed`}
173
+ description="The rest of the workspace is still available. Retry this feature or reload Inspector."
174
+ variant="feature"
175
+ >
176
+ {children}
177
+ </SafeErrorBoundary>
178
+ );
179
+ }
180
+
181
+ export function SafeRouteError({
182
+ error,
183
+ reset,
184
+ scope,
185
+ }: {
186
+ error: Error;
187
+ reset: () => void;
188
+ scope: string;
189
+ }): JSX.Element {
190
+ const reference = useMemo(() => createSafeDiagnosticReference(scope, error), [error, scope]);
191
+ return (
192
+ <SafeErrorFallback
193
+ reference={reference}
194
+ title="Inspector workspace could not be displayed"
195
+ description="Agent Inspector stopped this view safely. Retry the route or reload the application."
196
+ variant="page"
197
+ onRetry={reset}
198
+ onReload={() => window.location.reload()}
199
+ />
200
+ );
201
+ }
@@ -91,7 +91,10 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
91
91
 
92
92
  void fetch("/api/pi-agent", {
93
93
  method: "POST",
94
- headers: { "content-type": "application/json" },
94
+ headers: {
95
+ "content-type": "application/json",
96
+ "x-agent-inspector-csrf": "1",
97
+ },
95
98
  body: JSON.stringify(body),
96
99
  })
97
100
  .then(async (response) => {
@@ -3,27 +3,14 @@ import { z } from "zod";
3
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
4
4
  import { Button } from "../ui/button";
5
5
  import { fetchJsonWithTimeout } from "../../lib/apiClient";
6
+ import {
7
+ ProviderScanResponseSchema,
8
+ type ScannedProviderDto,
9
+ } from "../../lib/providerImportContract";
6
10
  import { ClientLogo } from "../clients/ClientLogo";
7
11
 
8
12
  import { Loader2, Download, AlertCircle } from "lucide-react";
9
13
 
10
- const ExternalProviderSchema = z.object({
11
- name: z.string(),
12
- apiKey: z.string(),
13
- format: z.enum(["anthropic", "openai"]),
14
- anthropicBaseUrl: z.string(),
15
- openaiBaseUrl: z.string(),
16
- openaiResponsesBaseUrl: z.string().optional().default(""),
17
- models: z.array(z.string()),
18
- sourceTool: z.enum(["claude-code", "opencode", "mimo-code"]),
19
- alreadyExists: z.boolean(),
20
- });
21
-
22
- const ScanResponseSchema = z.object({
23
- providers: z.array(ExternalProviderSchema),
24
- warnings: z.array(z.string()).optional(),
25
- });
26
-
27
14
  const ImportResponseSchema = z.object({
28
15
  success: z.boolean().optional(),
29
16
  imported: z.number().optional(),
@@ -46,7 +33,8 @@ export function ImportWizardDialog({
46
33
  }: ImportWizardDialogProps): JSX.Element {
47
34
  const [scanning, setScanning] = useState(false);
48
35
  const [scanError, setScanError] = useState<string | null>(null);
49
- const [providers, setProviders] = useState<z.infer<typeof ExternalProviderSchema>[]>([]);
36
+ const [scanId, setScanId] = useState<string | null>(null);
37
+ const [providers, setProviders] = useState<ScannedProviderDto[]>([]);
50
38
  const [warnings, setWarnings] = useState<string[]>([]);
51
39
  const [selected, setSelected] = useState<Set<number>>(new Set());
52
40
  const [importing, setImporting] = useState(false);
@@ -61,6 +49,7 @@ export function ImportWizardDialog({
61
49
  setScanning(true);
62
50
  setScanError(null);
63
51
  setWarnings([]);
52
+ setScanId(null);
64
53
  setProviders([]);
65
54
  setSelected(new Set());
66
55
  setImportResult(null);
@@ -68,13 +57,14 @@ export function ImportWizardDialog({
68
57
 
69
58
  void fetchJsonWithTimeout(
70
59
  "/api/providers/scan",
71
- ScanResponseSchema,
60
+ ProviderScanResponseSchema,
72
61
  PROVIDER_IMPORT_TIMEOUT_MS,
73
- undefined,
62
+ { method: "POST" },
74
63
  (response) => `Scan failed (${String(response.status)})`,
75
64
  )
76
65
  .then((data) => {
77
66
  if (scanRequestIdRef.current !== requestId) return;
67
+ setScanId(data.scanId);
78
68
  setProviders(data.providers);
79
69
  setWarnings(data.warnings ?? []);
80
70
  const indices = new Set<number>();
@@ -122,7 +112,7 @@ export function ImportWizardDialog({
122
112
 
123
113
  const importSelected = useCallback(() => {
124
114
  const toImport = providers.filter((_, i) => selected.has(i));
125
- if (toImport.length === 0) return;
115
+ if (toImport.length === 0 || scanId === null) return;
126
116
 
127
117
  const requestId = importRequestIdRef.current + 1;
128
118
  importRequestIdRef.current = requestId;
@@ -130,21 +120,6 @@ export function ImportWizardDialog({
130
120
  setImportError(null);
131
121
  setImportResult(null);
132
122
 
133
- // Convert ExternalProvider[] to ProviderConfig shape for import
134
- const now = new Date().toISOString();
135
- const providersPayload = toImport.map((p) => ({
136
- id: window.crypto.randomUUID(),
137
- name: p.name,
138
- apiKey: p.apiKey,
139
- format: p.format,
140
- anthropicBaseUrl: p.anthropicBaseUrl,
141
- openaiBaseUrl: p.openaiBaseUrl,
142
- openaiResponsesBaseUrl: p.openaiResponsesBaseUrl,
143
- models: p.models,
144
- createdAt: now,
145
- updatedAt: now,
146
- }));
147
-
148
123
  void fetchJsonWithTimeout(
149
124
  "/api/providers/import",
150
125
  ImportResponseSchema,
@@ -152,7 +127,10 @@ export function ImportWizardDialog({
152
127
  {
153
128
  method: "POST",
154
129
  headers: { "Content-Type": "application/json" },
155
- body: JSON.stringify({ providers: providersPayload }),
130
+ body: JSON.stringify({
131
+ scanId,
132
+ selectionIds: toImport.map((provider) => provider.selectionId),
133
+ }),
156
134
  },
157
135
  (response) => `Import failed (${String(response.status)})`,
158
136
  )
@@ -183,7 +161,7 @@ export function ImportWizardDialog({
183
161
  if (importRequestIdRef.current !== requestId) return;
184
162
  setImporting(false);
185
163
  });
186
- }, [providers, selected, onImportComplete]);
164
+ }, [providers, scanId, selected, onImportComplete]);
187
165
 
188
166
  const hasSelectable = providers.some((p) => !p.alreadyExists);
189
167
 
@@ -226,7 +204,7 @@ export function ImportWizardDialog({
226
204
  {!scanning &&
227
205
  providers.map((p, i) => (
228
206
  <label
229
- key={`${p.sourceTool}-${p.name}`}
207
+ key={p.selectionId}
230
208
  className={`flex items-start gap-3 p-3 border rounded-md ${p.alreadyExists ? "" : "cursor-pointer hover:bg-muted/50"}`}
231
209
  >
232
210
  {!p.alreadyExists && (
@@ -257,9 +235,7 @@ export function ImportWizardDialog({
257
235
  : p.openaiBaseUrl}
258
236
  </div>
259
237
  <div className="text-xs text-muted-foreground mt-0.5 font-mono">
260
- {p.apiKey.length > 8
261
- ? `${p.apiKey.slice(0, 4)}••••${p.apiKey.slice(-4)}`
262
- : "••••"}
238
+ {p.apiKeyHint}
263
239
  </div>
264
240
  </div>
265
241
  </label>
@@ -5,7 +5,7 @@ import OpenAILogoSvg from "../../assets/logos/openai.svg";
5
5
  import DeepSeekLogoSvg from "../../assets/logos/deepseek.svg";
6
6
  import MiniMaxLogoSvg from "../../assets/logos/minimax.jpeg";
7
7
  import AlibabaLogoSvg from "../../assets/logos/alibaba.svg";
8
- import QwenLogoSvg from "../../assets/logos/qwen.png";
8
+ import QwenLogoSvg from "../../assets/logos/qwen.webp";
9
9
  import ZhipuAILogoSvg from "../../assets/logos/zhipuai.svg";
10
10
 
11
11
  export type Provider =
@@ -15,7 +15,7 @@ import {
15
15
  } from "../../lib/apiClient";
16
16
  import { copyTextToClipboard } from "../../lib/clipboard";
17
17
  import {
18
- ProviderConfigSchema,
18
+ RedactedProviderDtoSchema,
19
19
  type ProviderConfig,
20
20
  type ProviderModelMetadata,
21
21
  } from "../../lib/providerContract";
@@ -41,7 +41,7 @@ const ImportResponseSchema = z.object({
41
41
  });
42
42
 
43
43
  const ModelMetadataRefreshResponseSchema = z.object({
44
- provider: ProviderConfigSchema,
44
+ provider: RedactedProviderDtoSchema,
45
45
  imported: z.number(),
46
46
  matchedModels: z.array(z.string()),
47
47
  missingModels: z.array(z.string()),
@@ -385,7 +385,7 @@ export function ProvidersPanel({
385
385
  try {
386
386
  const newProvider = await fetchJsonWithTimeout(
387
387
  "/api/providers",
388
- ProviderConfigSchema,
388
+ RedactedProviderDtoSchema,
389
389
  PROVIDER_WRITE_TIMEOUT_MS,
390
390
  {
391
391
  method: "POST",
@@ -413,7 +413,7 @@ export function ProvidersPanel({
413
413
  try {
414
414
  const updated = await fetchJsonWithTimeout(
415
415
  `/api/providers/${editingProvider.id}`,
416
- ProviderConfigSchema,
416
+ RedactedProviderDtoSchema,
417
417
  PROVIDER_WRITE_TIMEOUT_MS,
418
418
  {
419
419
  method: "PUT",
@@ -563,7 +563,10 @@ export function ProvidersPanel({
563
563
 
564
564
  return (
565
565
  <div className="space-y-4">
566
- <div className="sticky top-0 z-10 flex items-center justify-end gap-2 rounded-[8px] bg-background/95 px-1 py-1 backdrop-blur">
566
+ <div
567
+ data-provider-toolbar="true"
568
+ className="sticky top-0 z-10 flex min-w-0 flex-wrap items-center justify-start gap-2 rounded-[8px] bg-background/95 px-1 py-1 backdrop-blur sm:justify-end"
569
+ >
567
570
  <Button
568
571
  variant="outline"
569
572
  size="sm"
@@ -17,6 +17,29 @@ import {
17
17
  type TimeDisplayFormat,
18
18
  } from "../../lib/runtimeConfig";
19
19
 
20
+ export const SETTINGS_SECTIONS = [
21
+ { value: "providers", label: "Providers" },
22
+ { value: "proxy", label: "Proxy" },
23
+ { value: "storage", label: "Storage" },
24
+ { value: "mcp", label: "MCP" },
25
+ { value: "onboarding", label: "Onboarding" },
26
+ ];
27
+
28
+ export function ResponsiveSettingsTabs(): JSX.Element {
29
+ return (
30
+ <TabsList
31
+ aria-label="Settings sections"
32
+ className="grid w-full grid-cols-3 justify-stretch gap-1 bg-muted/35 p-1 group-data-[orientation=horizontal]/tabs:h-auto sm:grid-cols-5"
33
+ >
34
+ {SETTINGS_SECTIONS.map((section) => (
35
+ <TabsTrigger key={section.value} value={section.value} className="min-w-0 w-full">
36
+ {section.label}
37
+ </TabsTrigger>
38
+ ))}
39
+ </TabsList>
40
+ );
41
+ }
42
+
20
43
  export function SettingsDialog(): JSX.Element {
21
44
  const [open, setOpen] = useState(false);
22
45
  const [activeTab, setActiveTab] = useState("providers");
@@ -74,21 +97,19 @@ export function SettingsDialog(): JSX.Element {
74
97
  <span className="sr-only">Settings</span>
75
98
  </Button>
76
99
  </DialogTrigger>
77
- <DialogContent className="flex max-h-[80vh] max-w-2xl flex-col overflow-hidden">
100
+ <DialogContent className="flex max-h-[calc(100vh-1rem)] w-[calc(100%-1rem)] max-w-2xl flex-col overflow-hidden p-4 sm:max-h-[80vh] sm:p-6">
78
101
  <DialogHeader>
79
102
  <DialogTitle>Settings</DialogTitle>
80
103
  </DialogHeader>
81
104
 
82
- <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 overflow-hidden">
83
- <TabsList className="w-full justify-start bg-muted/35">
84
- <TabsTrigger value="providers">Providers</TabsTrigger>
85
- <TabsTrigger value="proxy">Proxy</TabsTrigger>
86
- <TabsTrigger value="storage">Storage</TabsTrigger>
87
- <TabsTrigger value="mcp">MCP</TabsTrigger>
88
- <TabsTrigger value="onboarding">Onboarding</TabsTrigger>
89
- </TabsList>
105
+ <Tabs
106
+ value={activeTab}
107
+ onValueChange={setActiveTab}
108
+ className="min-h-0 min-w-0 flex-1 overflow-hidden"
109
+ >
110
+ <ResponsiveSettingsTabs />
90
111
 
91
- <div className="mt-4 flex-1 overflow-x-hidden overflow-y-auto pr-2">
112
+ <div className="mt-4 min-w-0 flex-1 overflow-x-hidden overflow-y-auto pr-1 sm:pr-2">
92
113
  <TabsContent value="providers">
93
114
  <ProvidersPanel
94
115
  externalProviders={providers}
@@ -283,7 +304,9 @@ function CopyableSetupValue({
283
304
  <div className="min-w-0 rounded-md border border-border/45 bg-muted/15 px-3 py-2 transition-colors hover:bg-muted/25">
284
305
  <div className="mb-1 text-xs font-medium text-muted-foreground">{label}</div>
285
306
  <div className="flex min-w-0 items-center gap-2">
286
- <code className="min-w-0 flex-1 truncate font-mono text-xs text-foreground">{value}</code>
307
+ <code className="min-w-0 flex-1 truncate font-mono text-xs text-foreground" title={value}>
308
+ {value}
309
+ </code>
287
310
  <Button
288
311
  type="button"
289
312
  variant="ghost"
@@ -0,0 +1,63 @@
1
+ import { type JSX, type ReactNode } from "react";
2
+ import packageJson from "../../../package.json";
3
+ import { CrabLogo } from "../ui/crab-logo";
4
+ import { LiveConnectionBadge } from "./LiveConnectionStatus";
5
+ import type { LiveConnectionState } from "./liveConnectionState";
6
+
7
+ export function ApplicationBar({
8
+ liveConnection,
9
+ alertsAction,
10
+ settingsAction,
11
+ }: {
12
+ liveConnection: LiveConnectionState | undefined;
13
+ alertsAction: ReactNode;
14
+ settingsAction: ReactNode;
15
+ }): JSX.Element {
16
+ return (
17
+ <header
18
+ data-application-bar="true"
19
+ className="border-border/70 bg-background/95 sticky top-0 z-50 shrink-0 border-b px-3 py-2 shadow-[0_10px_30px_rgba(0,0,0,0.14)] backdrop-blur lg:px-4"
20
+ >
21
+ <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-2 sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
22
+ <a
23
+ href="/"
24
+ aria-label="Agent Inspector home"
25
+ className="flex min-w-0 items-center gap-2 rounded-md focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
26
+ >
27
+ <CrabLogo className="size-7 shrink-0 text-amber-500" />
28
+ <span className="min-w-0">
29
+ <span className="block truncate text-sm font-bold leading-4">Agent Inspector</span>
30
+ <span className="hidden truncate text-[10px] font-medium text-muted-foreground md:block">
31
+ Local First. Evidence First.
32
+ </span>
33
+ </span>
34
+ <span className="hidden shrink-0 font-mono text-[10px] text-muted-foreground lg:inline">
35
+ v{packageJson.version}
36
+ </span>
37
+ </a>
38
+
39
+ <div className="order-3 col-span-2 flex min-w-0 justify-start sm:order-2 sm:col-span-1 sm:justify-center">
40
+ {liveConnection === undefined ? (
41
+ <span
42
+ data-connection-state="stored"
43
+ className="border-border/70 bg-muted/50 inline-flex h-7 max-w-full items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium text-muted-foreground"
44
+ >
45
+ <span className="size-1.5 shrink-0 rounded-full bg-slate-400" />
46
+ Stored session
47
+ </span>
48
+ ) : (
49
+ <LiveConnectionBadge state={liveConnection} />
50
+ )}
51
+ </div>
52
+
53
+ <nav
54
+ aria-label="Application actions"
55
+ className="order-2 flex shrink-0 items-center justify-end gap-1 sm:order-3"
56
+ >
57
+ {alertsAction}
58
+ {settingsAction}
59
+ </nav>
60
+ </div>
61
+ </header>
62
+ );
63
+ }