@tonyclaw/agent-inspector 3.0.29 → 3.0.31

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-DB2KggLE.js +1 -0
  4. package/.output/public/assets/InspectorPet-CfwDw6kD.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-PcrO96_F.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CctnITbt.js → ReplayDialog-3Pl7EKjp.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-BrN0RBbL.js → RequestAnatomy-BcPjcHLX.js} +1 -1
  8. package/.output/public/assets/ResponseView-DOx51wV3.js +2 -0
  9. package/.output/public/assets/StreamingChunkSequence-LkYJWM1A.js +1 -0
  10. package/.output/public/assets/_sessionId-BSg32vC6.js +1 -0
  11. package/.output/public/assets/{_sessionId-DKM91udC.js → _sessionId-C0CA68QV.js} +1 -1
  12. package/.output/public/assets/{index-Bq-nqolG.js → index-BpBUqdAB.js} +1 -1
  13. package/.output/public/assets/index-D93dCgAQ.js +1 -0
  14. package/.output/public/assets/{index-DPiESBo2.js → index-FV5sxqx3.js} +6 -6
  15. package/.output/public/assets/index-VUsF4nhf.js +76 -0
  16. package/.output/public/assets/index-hyQHZeQI.css +1 -0
  17. package/.output/public/assets/{json-viewer-CfaDJwx8.js → json-viewer-WctIbAh_.js} +1 -1
  18. package/.output/public/assets/jszip.min-DnSIoTix.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 +80 -80
  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-CwGXPD4C.mjs → _sessionId-BHBK3VbF.mjs} +15 -15
  25. package/.output/server/_sessionId-CyuJ6Kfl.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-27amlv_a.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-CQnGXfLs.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-DlAl0uF-.mjs} +1569 -1646
  29. package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-5m2jtlLa.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy--1WtQNY9.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-CgbqS8sr.mjs} +60 -71
  32. package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-BwpnopWx.mjs} +16 -16
  33. package/.output/server/_ssr/{index-D4DvByuW.mjs → index-CNVi1TzT.mjs} +15 -15
  34. package/.output/server/_ssr/index-DMCjH_4V.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BjoHRbjS.mjs → json-viewer-DNGN2uoe.mjs} +58 -58
  37. package/.output/server/_ssr/{router-BYlePDj8.mjs → router-CgsqLnk5.mjs} +16007 -12632
  38. package/.output/server/_tanstack-start-manifest_v-BH5f_kWV.mjs +4 -0
  39. package/.output/server/index.mjs +99 -71
  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/ecosystem/AgentLabDialog.tsx +166 -19
  50. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  51. package/src/components/pi-agent/PiAgentPanel.tsx +209 -35
  52. package/src/components/pi-agent/piAgentChatLogic.ts +263 -0
  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/LiveConnectionStatus.tsx +186 -0
  60. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  61. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  62. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  63. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  64. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  65. package/src/components/proxy-viewer/accessibility.ts +8 -0
  66. package/src/components/proxy-viewer/lazy.ts +4 -0
  67. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  68. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  69. package/src/components/proxy-viewer/viewerState.ts +2 -6
  70. package/src/components/ui/select.tsx +1 -1
  71. package/src/components/ui/tabs.tsx +3 -3
  72. package/src/components/ui/transient-toast.tsx +7 -6
  73. package/src/lib/apiClient.ts +17 -2
  74. package/src/lib/ecosystemContract.ts +34 -0
  75. package/src/lib/export-logs.ts +1 -1
  76. package/src/lib/piAgentContract.ts +80 -0
  77. package/src/lib/providerContract.ts +70 -4
  78. package/src/lib/providerImportContract.ts +27 -0
  79. package/src/lib/providerModelMetadata.ts +16 -7
  80. package/src/lib/resourceLimits.ts +152 -0
  81. package/src/lib/safeDiagnostic.ts +38 -0
  82. package/src/lib/useProviders.ts +4 -4
  83. package/src/mcp/server.ts +39 -6
  84. package/src/mcp/toolHandlers.ts +131 -4
  85. package/src/proxy/chunkStorage.ts +20 -6
  86. package/src/proxy/config.ts +20 -6
  87. package/src/proxy/dataDir.ts +3 -0
  88. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  89. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  90. package/src/proxy/ecosystemTasks.ts +48 -0
  91. package/src/proxy/evidenceExporter.ts +23 -9
  92. package/src/proxy/groupEvidenceExporter.ts +26 -5
  93. package/src/proxy/groupStore.ts +13 -3
  94. package/src/proxy/handler.ts +103 -20
  95. package/src/proxy/identityProxy.ts +333 -14
  96. package/src/proxy/logFinalizer.ts +73 -6
  97. package/src/proxy/logImportUpload.ts +128 -0
  98. package/src/proxy/logImporter.ts +321 -70
  99. package/src/proxy/logIndex.ts +16 -6
  100. package/src/proxy/logger.ts +294 -41
  101. package/src/proxy/privateDataPath.ts +183 -0
  102. package/src/proxy/providerScanStore.ts +87 -0
  103. package/src/proxy/providerSecretStore.ts +58 -19
  104. package/src/proxy/providers.ts +175 -58
  105. package/src/proxy/rawStreamCapture.ts +66 -5
  106. package/src/proxy/runStore.ts +13 -3
  107. package/src/proxy/runtimeAdmission.ts +52 -0
  108. package/src/proxy/runtimeHealth.ts +206 -0
  109. package/src/proxy/runtimeShutdown.ts +75 -0
  110. package/src/proxy/sessionArchive.ts +15 -2
  111. package/src/proxy/sessionProcess.ts +19 -0
  112. package/src/proxy/sessionRuntime.ts +7 -0
  113. package/src/proxy/shutdownCoordinator.ts +90 -0
  114. package/src/proxy/sqliteLogIndex.ts +18 -2
  115. package/src/proxy/store.ts +18 -2
  116. package/src/routes/__root.tsx +18 -1
  117. package/src/routes/api/alerts.summary.ts +12 -8
  118. package/src/routes/api/alerts.ts +27 -9
  119. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  120. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  121. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  122. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  123. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  124. package/src/routes/api/health.ts +9 -2
  125. package/src/routes/api/knowledge.project-context.ts +11 -0
  126. package/src/routes/api/knowledge.search.ts +33 -2
  127. package/src/routes/api/logs.$id.body.ts +16 -2
  128. package/src/routes/api/logs.import.ts +7 -18
  129. package/src/routes/api/logs.stream.ts +147 -69
  130. package/src/routes/api/logs.ts +55 -14
  131. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  132. package/src/routes/api/providers.$providerId.ts +37 -6
  133. package/src/routes/api/providers.export.ts +34 -16
  134. package/src/routes/api/providers.import.ts +42 -8
  135. package/src/routes/api/providers.scan.ts +13 -8
  136. package/src/routes/api/providers.ts +36 -5
  137. package/src/routes/api/runs.ts +12 -12
  138. package/src/routes/api/sessions.ts +15 -8
  139. package/src/routes/index.tsx +6 -0
  140. package/src/routes/livez.ts +13 -0
  141. package/src/routes/readyz.ts +18 -0
  142. package/src/routes/session/$sessionId.tsx +6 -0
  143. package/src/services/piAgent.ts +131 -102
  144. package/src/services/piAgentRetrieval.ts +1180 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
  148. package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
  150. package/.output/public/assets/index-DTLNVcgc.js +0 -76
  151. package/.output/public/assets/index-DliqmwUw.css +0 -1
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.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
package/README.md CHANGED
@@ -33,11 +33,27 @@ agent-inspector-mcp doctor
33
33
 
34
34
  Agent Inspector requires Node.js 22 or newer. Node.js 24 LTS is recommended for production use.
35
35
 
36
- By default, Agent Inspector binds to `127.0.0.1`. With `--host 0.0.0.0`, remote coding tools may
37
- use `/proxy`, while the UI, REST API, and MCP control plane remain local-only. Set
38
- `AGENT_INSPECTOR_CONTROL_TOKEN` for authenticated remote API/MCP clients, or explicitly set
39
- `AGENT_INSPECTOR_ALLOW_REMOTE_CONTROL=1` only behind a trusted firewall or loopback-only port
40
- mapping.
36
+ By default, Agent Inspector binds to `127.0.0.1` and local tools need no Inspector credential.
37
+ With `--host 0.0.0.0`, non-loopback coding tools must authenticate `/proxy` with a dedicated
38
+ `AGENT_INSPECTOR_PROXY_TOKEN`, supplied as `Proxy-Authorization: Bearer <token>` or
39
+ `X-Agent-Inspector-Proxy-Token: <token>`. Provider `Authorization` remains separate and is never
40
+ accepted as the Inspector proxy credential. The UI, REST API, and MCP control plane remain
41
+ local-only unless `AGENT_INSPECTOR_CONTROL_TOKEN` is configured for authenticated remote API/MCP
42
+ clients. `AGENT_INSPECTOR_ALLOW_UNAUTHENTICATED_REMOTE_PROXY=1` and
43
+ `AGENT_INSPECTOR_ALLOW_REMOTE_CONTROL=1` are temporary compatibility overrides intended only for a
44
+ trusted firewall or loopback-only port mapping.
45
+
46
+ The packaged CLI keeps the protected identity ingress enabled even on loopback because it also owns
47
+ Host validation, CSRF enforcement, credential scoping, and first-hop byte budgets. Disabling it with
48
+ `--no-identity-proxy` requires the explicit diagnostic-only
49
+ `AGENT_INSPECTOR_ALLOW_UNPROTECTED_INGRESS=1` override.
50
+
51
+ When exposing Inspector through a named reverse proxy, add its public hostname to the comma-separated
52
+ `AGENT_INSPECTOR_TRUSTED_HOSTS` allowlist. Browser mutations require same-origin CSRF proof; the
53
+ built-in UI supplies it automatically. The public ingress streams request bodies with backpressure
54
+ and defaults to a 64 MiB hard limit and a 120 second body deadline; override these with positive
55
+ integer byte/millisecond values in `AGENT_INSPECTOR_MAX_REQUEST_BYTES` and
56
+ `AGENT_INSPECTOR_REQUEST_TIMEOUT_MS` when a trusted workload requires different bounds.
41
57
 
42
58
  `@tonyclaw/agent-inspector` runs the local UI, proxy, REST API, storage, and built-in
43
59
  `/api/mcp` endpoint. `@tonyclaw/agent-inspector-mcp` is the enhancement package for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/agent-inspector",
3
- "version": "3.0.29",
3
+ "version": "3.0.31",
4
4
  "type": "module",
5
5
  "description": "Agent observability and knowledge capture layer for AI coding tools.",
6
6
  "license": "MIT",
@@ -54,7 +54,11 @@
54
54
  "scripts": {
55
55
  "dev": "vite dev",
56
56
  "start": "node .output/cli.js",
57
- "build": "vite build && bun build:cli && bun build:mcp",
57
+ "build": "vite build && bun build:workers && bun build:cli && bun build:mcp && bun check:bundle",
58
+ "build:workers": "npx esbuild src/proxy/logFinalizer.worker.ts src/proxy/sessionWorkerEntry.ts --bundle --platform=node --target=node22 --format=esm --outdir=.output/workers --external:better-sqlite3",
59
+ "smoke:workers": "node scripts/smoke-production-workers.mjs",
60
+ "smoke:packed-workers": "node scripts/smoke-packed-workers.mjs",
61
+ "check:bundle": "node scripts/check-client-bundle.mjs",
58
62
  "build:cli": "npx esbuild src/cli.ts --bundle --platform=node --target=node22 --format=esm --outfile=.output/cli.js",
59
63
  "build:mcp": "npx esbuild packages/agent-inspector-mcp/src/cli.ts --bundle --platform=node --target=node22 --format=esm --outfile=packages/agent-inspector-mcp/dist/cli.js",
60
64
  "postinstall": "node scripts/setup-windows-runtime.mjs && node scripts/setup-agent-skills.mjs",
Binary file
package/src/cli.ts CHANGED
@@ -19,7 +19,12 @@ import {
19
19
  shouldSuppressServerOutputLine,
20
20
  type ServerOutputFilterContext,
21
21
  } from "./cli/startupOutput.js";
22
- import { startIdentityProxy } from "./proxy/identityProxy.js";
22
+ import {
23
+ closeIdentityProxyGracefully,
24
+ forceCloseIdentityProxy,
25
+ startIdentityProxy,
26
+ stopIdentityProxyAdmission,
27
+ } from "./proxy/identityProxy.js";
23
28
  import { findPidsByPort, killPid, openUrlCommand } from "./proxy/platformCommands.js";
24
29
 
25
30
  const __filename = fileURLToPath(import.meta.url);
@@ -36,6 +41,8 @@ const DEFAULT_BIND_HOST = "127.0.0.1";
36
41
  const LOCAL_PROBE_TIMEOUT_MS = 2000;
37
42
  const BRANDED_WINDOWS_RUNTIME_EXE = "agent-inspector.exe";
38
43
  const DEFAULT_CAPTURE_MODE = "simple";
44
+ const DEFAULT_SHUTDOWN_DEADLINE_MS = 15_000;
45
+ const SUPERVISOR_SHUTDOWN_GRACE_MS = 2_000;
39
46
 
40
47
  type CaptureMode = "simple" | "full";
41
48
 
@@ -232,10 +239,20 @@ function printAccessHints(port: number, host?: string): void {
232
239
  console.log(line);
233
240
  }
234
241
  if (!isLoopbackHost(host)) {
242
+ const proxyTokenConfigured = (process.env["AGENT_INSPECTOR_PROXY_TOKEN"] ?? "") !== "";
243
+ const legacyProxyEnabled =
244
+ process.env["AGENT_INSPECTOR_ALLOW_UNAUTHENTICATED_REMOTE_PROXY"] === "1";
245
+ if (proxyTokenConfigured) {
246
+ console.log(" Remote proxy: dedicated token authentication enabled");
247
+ } else if (legacyProxyEnabled) {
248
+ console.warn(" Remote proxy: UNAUTHENTICATED compatibility mode enabled");
249
+ } else {
250
+ console.warn(" Remote proxy: blocked until AGENT_INSPECTOR_PROXY_TOKEN is configured");
251
+ }
235
252
  if (process.env["AGENT_INSPECTOR_ALLOW_REMOTE_CONTROL"] === "1") {
236
253
  console.warn(" Control plane: REMOTE ACCESS ENABLED by administrator override");
237
254
  } else {
238
- console.log(" Control plane: local only; remote clients may use /proxy");
255
+ console.log(" Control plane: local only by default");
239
256
  if ((process.env["AGENT_INSPECTOR_CONTROL_TOKEN"] ?? "") !== "") {
240
257
  console.log(" Remote API: Bearer token authentication enabled");
241
258
  }
@@ -243,6 +260,15 @@ function printAccessHints(port: number, host?: string): void {
243
260
  }
244
261
  }
245
262
 
263
+ function readPositiveIntegerEnv(name: string): number | undefined {
264
+ const raw = process.env[name];
265
+ if (raw === undefined || raw.trim().length === 0) return undefined;
266
+ const parsed = Number(raw);
267
+ if (Number.isSafeInteger(parsed) && parsed > 0) return parsed;
268
+ console.warn(`Ignoring invalid ${name}=${raw}; expected a positive integer.`);
269
+ return undefined;
270
+ }
271
+
246
272
  type BackgroundSupervisorOptions = {
247
273
  port: number;
248
274
  host: string | undefined;
@@ -313,6 +339,15 @@ async function tryStartIdentityProxy(
313
339
  restrictRemoteControl: !isLoopbackHost(listenHost),
314
340
  allowRemoteControl: process.env["AGENT_INSPECTOR_ALLOW_REMOTE_CONTROL"] === "1",
315
341
  controlToken: process.env["AGENT_INSPECTOR_CONTROL_TOKEN"],
342
+ proxyToken: process.env["AGENT_INSPECTOR_PROXY_TOKEN"],
343
+ allowUnauthenticatedRemoteProxy:
344
+ process.env["AGENT_INSPECTOR_ALLOW_UNAUTHENTICATED_REMOTE_PROXY"] === "1",
345
+ trustedHosts: (process.env["AGENT_INSPECTOR_TRUSTED_HOSTS"] ?? "")
346
+ .split(",")
347
+ .map((host) => host.trim())
348
+ .filter((host) => host.length > 0),
349
+ maxRequestBytes: readPositiveIntegerEnv("AGENT_INSPECTOR_MAX_REQUEST_BYTES"),
350
+ requestTimeoutMs: readPositiveIntegerEnv("AGENT_INSPECTOR_REQUEST_TIMEOUT_MS"),
316
351
  });
317
352
  return server;
318
353
  } catch (err) {
@@ -475,13 +510,9 @@ async function runStart(args: string[]): Promise<void> {
475
510
  return;
476
511
  }
477
512
 
478
- if (
479
- !enableIdentityProxy &&
480
- !isLoopbackHost(host) &&
481
- process.env["AGENT_INSPECTOR_ALLOW_REMOTE_CONTROL"] !== "1"
482
- ) {
513
+ if (!enableIdentityProxy && process.env["AGENT_INSPECTOR_ALLOW_UNPROTECTED_INGRESS"] !== "1") {
483
514
  console.error(
484
- "Refusing an unprotected remote bind with --no-identity-proxy. Keep the identity proxy enabled or set AGENT_INSPECTOR_ALLOW_REMOTE_CONTROL=1 explicitly.",
515
+ "Refusing --no-identity-proxy because it also disables Host, CSRF, credential-scope, and first-hop budget enforcement. Keep the protected ingress enabled or set AGENT_INSPECTOR_ALLOW_UNPROTECTED_INGRESS=1 explicitly for temporary diagnostics.",
485
516
  );
486
517
  process.exitCode = 1;
487
518
  return;
@@ -663,6 +694,10 @@ async function runStart(args: string[]): Promise<void> {
663
694
  serverEnv["AGENT_INSPECTOR_PUBLIC_PORT"] = String(port);
664
695
  serverEnv["PROXY_PORT"] = String(port);
665
696
  serverEnv["AGENT_INSPECTOR_CAPTURE_MODE"] = captureMode;
697
+ const workerDir = join(__dirname, "workers");
698
+ if (existsSync(workerDir)) {
699
+ serverEnv["AGENT_INSPECTOR_WORKER_DIR"] = workerDir;
700
+ }
666
701
  const serverProcess = spawn(serverCommand.command, serverCommand.args, {
667
702
  stdio: background ? ["ignore", "ignore", "ignore"] : ["ignore", "pipe", "pipe"],
668
703
  detached: background,
@@ -692,6 +727,41 @@ async function runStart(args: string[]): Promise<void> {
692
727
  port: number;
693
728
  server: import("node:http").Server;
694
729
  }> = [];
730
+ let supervisorShutdownPromise: Promise<void> | null = null;
731
+ const requestSupervisorShutdown = (signal: "SIGINT" | "SIGTERM"): void => {
732
+ if (supervisorShutdownPromise !== null) return;
733
+ for (const proxy of identityProxies) stopIdentityProxyAdmission(proxy.server);
734
+ serverProcess.kill(signal);
735
+
736
+ const childDeadline =
737
+ readPositiveIntegerEnv("AGENT_INSPECTOR_SHUTDOWN_TIMEOUT_MS") ?? DEFAULT_SHUTDOWN_DEADLINE_MS;
738
+ let deadline: ReturnType<typeof setTimeout> | null = null;
739
+ let forced = false;
740
+ const graceful = Promise.all([
741
+ serverExit.then(() => undefined),
742
+ ...identityProxies.map(async (proxy) => await closeIdentityProxyGracefully(proxy.server)),
743
+ ]).then(() => undefined);
744
+ const timedOut = new Promise<void>((resolve) => {
745
+ deadline = setTimeout(() => {
746
+ forced = true;
747
+ for (const proxy of identityProxies) forceCloseIdentityProxy(proxy.server);
748
+ if (serverProcess.exitCode === null) serverProcess.kill("SIGKILL");
749
+ resolve();
750
+ }, childDeadline + SUPERVISOR_SHUTDOWN_GRACE_MS);
751
+ });
752
+ supervisorShutdownPromise = Promise.race([graceful, timedOut])
753
+ .finally(() => {
754
+ if (deadline !== null) clearTimeout(deadline);
755
+ })
756
+ .then(() => {
757
+ if (forced) process.exitCode = 1;
758
+ });
759
+ };
760
+ const handleSigint = (): void => requestSupervisorShutdown("SIGINT");
761
+ const handleSigterm = (): void => requestSupervisorShutdown("SIGTERM");
762
+ const currentSupervisorShutdown = (): Promise<void> | null => supervisorShutdownPromise;
763
+ process.on("SIGINT", handleSigint);
764
+ process.on("SIGTERM", handleSigterm);
695
765
  if (enableIdentityProxy) {
696
766
  const primaryProxy = await tryStartIdentityProxy(port, upstreamPort, host, upstreamHost);
697
767
  if (primaryProxy !== null) {
@@ -759,7 +829,12 @@ async function runStart(args: string[]): Promise<void> {
759
829
 
760
830
  process.exitCode = await serverExit;
761
831
 
762
- for (const proxy of identityProxies) {
763
- await new Promise<void>((resolve) => proxy.server.close(() => resolve()));
832
+ const activeSupervisorShutdown = currentSupervisorShutdown();
833
+ if (activeSupervisorShutdown !== null) {
834
+ await activeSupervisorShutdown;
835
+ } else {
836
+ await Promise.all(identityProxies.map((proxy) => closeIdentityProxyGracefully(proxy.server)));
764
837
  }
838
+ process.off("SIGINT", handleSigint);
839
+ process.off("SIGTERM", handleSigterm);
765
840
  }
@@ -3,28 +3,31 @@ import { useState, type JSX } from "react";
3
3
  import { useOnboarding } from "../lib/useOnboarding";
4
4
 
5
5
  /**
6
- * First-launch onboarding banner. Shows once on a fresh install and persists
7
- * dismissal through the server runtime config.
6
+ * Presentational onboarding surface, exported so responsive and accessibility
7
+ * behavior can be verified without runtime configuration state.
8
8
  */
9
- export function OnboardingBanner(): JSX.Element | null {
10
- const { hasSeenOnboarding, isLoading, markSeen } = useOnboarding();
11
- const [expanded, setExpanded] = useState(false);
12
-
13
- if (isLoading || hasSeenOnboarding) return null;
14
-
9
+ export function OnboardingTip({
10
+ expanded,
11
+ onToggle,
12
+ onDismiss,
13
+ }: {
14
+ expanded: boolean;
15
+ onToggle: () => void;
16
+ onDismiss: () => void;
17
+ }): JSX.Element {
15
18
  const detailClass = expanded ? "grid" : "hidden sm:grid";
16
19
 
17
20
  return (
18
21
  <div
19
22
  role="note"
20
23
  aria-label="Onboarding tip"
21
- className="mx-4 mt-2 mb-1 rounded-md border border-amber-500/30 bg-[#130d04] px-3 py-2 text-sm shadow-[0_12px_40px_rgba(0,0,0,0.16)]"
24
+ className="mx-3 mt-2 mb-1 rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-sm shadow-sm sm:mx-4"
22
25
  >
23
26
  <div className="flex items-start gap-3">
24
- <Info className="mt-0.5 size-4 shrink-0 text-amber-300" aria-hidden="true" />
27
+ <Info className="mt-0.5 size-4 shrink-0 text-warning" aria-hidden="true" />
25
28
  <div className="min-w-0 flex-1">
26
29
  <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
27
- <span className="font-medium text-amber-200">Inspector ready</span>
30
+ <span className="font-medium text-foreground">Inspector ready</span>
28
31
  <span className="hidden truncate text-xs text-muted-foreground sm:inline">
29
32
  Request, response, tool, timing, token, replay, and memory signals are active.
30
33
  </span>
@@ -45,8 +48,8 @@ export function OnboardingBanner(): JSX.Element | null {
45
48
  </div>
46
49
  <button
47
50
  type="button"
48
- onClick={() => setExpanded((value) => !value)}
49
- className="inline-flex h-8 shrink-0 items-center gap-1 rounded-md border border-amber-500/30 px-2 text-xs text-amber-200 transition-colors hover:bg-amber-500/10 sm:hidden"
51
+ onClick={onToggle}
52
+ className="inline-flex h-8 shrink-0 items-center gap-1 rounded-md border border-warning/35 px-2 text-xs font-medium text-foreground transition-colors hover:bg-warning/15 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none sm:hidden"
50
53
  aria-expanded={expanded}
51
54
  aria-label={expanded ? "Hide onboarding details" : "Show onboarding details"}
52
55
  >
@@ -56,10 +59,8 @@ export function OnboardingBanner(): JSX.Element | null {
56
59
  </button>
57
60
  <button
58
61
  type="button"
59
- onClick={() => {
60
- void markSeen();
61
- }}
62
- className="hidden h-8 shrink-0 items-center gap-1.5 rounded-md border border-amber-500/40 px-3 text-xs text-amber-200 transition-colors hover:bg-amber-500/10 sm:inline-flex"
62
+ onClick={onDismiss}
63
+ className="hidden h-8 shrink-0 items-center gap-1.5 rounded-md border border-warning/40 px-3 text-xs font-medium text-foreground transition-colors hover:bg-warning/15 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none sm:inline-flex"
63
64
  aria-label="Dismiss onboarding tip"
64
65
  >
65
66
  <Check className="size-3.5" />
@@ -67,10 +68,8 @@ export function OnboardingBanner(): JSX.Element | null {
67
68
  </button>
68
69
  <button
69
70
  type="button"
70
- onClick={() => {
71
- void markSeen();
72
- }}
73
- className="-m-1 shrink-0 p-1 text-muted-foreground transition-colors hover:text-foreground"
71
+ onClick={onDismiss}
72
+ className="-m-1 shrink-0 rounded-sm p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
74
73
  aria-label="Dismiss"
75
74
  >
76
75
  <X className="size-3.5" />
@@ -79,3 +78,24 @@ export function OnboardingBanner(): JSX.Element | null {
79
78
  </div>
80
79
  );
81
80
  }
81
+
82
+ /**
83
+ * First-launch onboarding banner. Shows once on a fresh install and persists
84
+ * dismissal through the server runtime config.
85
+ */
86
+ export function OnboardingBanner(): JSX.Element | null {
87
+ const { hasSeenOnboarding, isLoading, markSeen } = useOnboarding();
88
+ const [expanded, setExpanded] = useState(false);
89
+
90
+ if (isLoading || hasSeenOnboarding) return null;
91
+
92
+ return (
93
+ <OnboardingTip
94
+ expanded={expanded}
95
+ onToggle={() => setExpanded((value) => !value)}
96
+ onDismiss={() => {
97
+ void markSeen();
98
+ }}
99
+ />
100
+ );
101
+ }