@tonyclaw/agent-inspector 2.0.3 → 2.0.5

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 (68) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-D5A4bTfV.js → CompareDrawer-3nRwtk8J.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-CbW5VRER.js +101 -0
  4. package/.output/public/assets/ReplayDialog-Cl62N9PI.js +1 -0
  5. package/.output/public/assets/RequestAnatomy-DgQWGvjs.js +1 -0
  6. package/.output/public/assets/ResponseView-Cvc-ct4E.js +1 -0
  7. package/.output/public/assets/StreamingChunkSequence-BCQaCAIe.js +1 -0
  8. package/.output/public/assets/_sessionId-CcD_aLGq.js +1 -0
  9. package/.output/public/assets/index-B_dffD3u.js +1 -0
  10. package/.output/public/assets/index-CX796gvi.css +1 -0
  11. package/.output/public/assets/{json-viewer-BbU0n8eM.js → json-viewer-IXejqXB0.js} +1 -1
  12. package/.output/public/assets/{main-CZT_F-gu.js → main-2NlGzgOe.js} +2 -2
  13. package/.output/server/_libs/lucide-react.mjs +181 -114
  14. package/.output/server/{_sessionId-B-s9P7fJ.mjs → _sessionId-DWCTasJU.mjs} +3 -3
  15. package/.output/server/_ssr/{CompareDrawer-C08L3UOO.mjs → CompareDrawer-DhrN1uC2.mjs} +6 -6
  16. package/.output/server/_ssr/{ProxyViewerContainer-CMWl3Ijy.mjs → ProxyViewerContainer-DRl51s_n.mjs} +910 -186
  17. package/.output/server/_ssr/{ReplayDialog-CPDo9_G5.mjs → ReplayDialog-BQT_ygxC.mjs} +240 -14
  18. package/.output/server/_ssr/{RequestAnatomy-D9wt_K1E.mjs → RequestAnatomy-DS2tZOgq.mjs} +5 -5
  19. package/.output/server/_ssr/{ResponseView-DXaL7nY3.mjs → ResponseView-e0kL2C3x.mjs} +25 -21
  20. package/.output/server/_ssr/{StreamingChunkSequence-B_hudZyb.mjs → StreamingChunkSequence-BJG-m7xs.mjs} +3 -3
  21. package/.output/server/_ssr/{index-CuE_BN86.mjs → index-Dea3OeRw.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-Ci6kkjde.mjs → json-viewer-DDU55MLK.mjs} +3 -3
  24. package/.output/server/_ssr/{router-BemxgIg7.mjs → router-Dl7oh0zx.mjs} +164 -82
  25. package/.output/server/_tanstack-start-manifest_v-m-FJNBVf.mjs +4 -0
  26. package/.output/server/index.mjs +70 -70
  27. package/package.json +1 -1
  28. package/src/components/OnboardingBanner.tsx +11 -19
  29. package/src/components/ProxyViewer.tsx +26 -16
  30. package/src/components/ProxyViewerContainer.tsx +2 -1
  31. package/src/components/providers/ProviderCard.tsx +6 -20
  32. package/src/components/providers/SettingsDialog.tsx +140 -3
  33. package/src/components/proxy-viewer/AgentTraceSummary.tsx +731 -72
  34. package/src/components/proxy-viewer/AnswerMarkdown.tsx +16 -0
  35. package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
  36. package/src/components/proxy-viewer/ConversationGroup.tsx +12 -0
  37. package/src/components/proxy-viewer/ConversationHeader.tsx +6 -6
  38. package/src/components/proxy-viewer/LogEntry.tsx +5 -5
  39. package/src/components/proxy-viewer/LogEntryHeader.tsx +21 -36
  40. package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
  41. package/src/components/proxy-viewer/ResponseView.tsx +4 -8
  42. package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -17
  43. package/src/components/proxy-viewer/TurnGroup.tsx +18 -2
  44. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +2 -2
  45. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +6 -12
  46. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
  47. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +10 -14
  48. package/src/components/proxy-viewer/replayComparison.ts +131 -0
  49. package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
  50. package/src/components/proxy-viewer/viewerState.ts +14 -2
  51. package/src/knowledge/candidateStore.ts +32 -1
  52. package/src/lib/runtimeConfig.ts +6 -0
  53. package/src/lib/timeDisplay.ts +22 -0
  54. package/src/lib/useOnboarding.ts +2 -0
  55. package/src/lib/useStripConfig.ts +16 -0
  56. package/src/proxy/config.ts +3 -0
  57. package/src/routes/api/config.ts +5 -1
  58. package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
  59. package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
  60. package/.output/public/assets/ProxyViewerContainer-Da0jpBkp.js +0 -101
  61. package/.output/public/assets/ReplayDialog-CxUk_TF0.js +0 -1
  62. package/.output/public/assets/RequestAnatomy-DIlzjgjJ.js +0 -1
  63. package/.output/public/assets/ResponseView-DQCuKJ1G.js +0 -1
  64. package/.output/public/assets/StreamingChunkSequence-DHk4SGGL.js +0 -1
  65. package/.output/public/assets/_sessionId-dY1TTl7N.js +0 -1
  66. package/.output/public/assets/index-D7wwbwly.css +0 -1
  67. package/.output/public/assets/index-FqQZbfl2.js +0 -1
  68. package/.output/server/_tanstack-start-manifest_v--L1_b4sd.mjs +0 -4
@@ -38,107 +38,107 @@ const assets = {
38
38
  "/assets/alibaba-TTwafVwX.svg": {
39
39
  "type": "image/svg+xml",
40
40
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
- "mtime": "2026-06-20T07:49:57.206Z",
41
+ "mtime": "2026-06-21T02:55:38.892Z",
42
42
  "size": 5915,
43
43
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
44
  },
45
- "/assets/CompareDrawer-D5A4bTfV.js": {
45
+ "/assets/minimax-BPMzvuL-.jpeg": {
46
+ "type": "image/jpeg",
47
+ "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
48
+ "mtime": "2026-06-21T02:55:38.892Z",
49
+ "size": 6918,
50
+ "path": "../public/assets/minimax-BPMzvuL-.jpeg"
51
+ },
52
+ "/assets/CompareDrawer-3nRwtk8J.js": {
46
53
  "type": "text/javascript; charset=utf-8",
47
- "etag": '"4a1f-mEPVKgxEcmIrRzyfYSrf+TEFNes"',
48
- "mtime": "2026-06-20T07:49:57.206Z",
49
- "size": 18975,
50
- "path": "../public/assets/CompareDrawer-D5A4bTfV.js"
54
+ "etag": '"4a25-urgvq4zVkJNkh48bDzRCZpmV0XU"',
55
+ "mtime": "2026-06-21T02:55:38.893Z",
56
+ "size": 18981,
57
+ "path": "../public/assets/CompareDrawer-3nRwtk8J.js"
51
58
  },
52
- "/assets/index-FqQZbfl2.js": {
59
+ "/assets/index-B_dffD3u.js": {
53
60
  "type": "text/javascript; charset=utf-8",
54
- "etag": '"74-el2/zDM0d+yANqsiilX18eIScpc"',
55
- "mtime": "2026-06-20T07:49:57.206Z",
61
+ "etag": '"74-G34R32wRb35yFy01Xx5+tisQpbk"',
62
+ "mtime": "2026-06-21T02:55:38.892Z",
56
63
  "size": 116,
57
- "path": "../public/assets/index-FqQZbfl2.js"
64
+ "path": "../public/assets/index-B_dffD3u.js"
58
65
  },
59
- "/assets/index-D7wwbwly.css": {
66
+ "/assets/index-CX796gvi.css": {
60
67
  "type": "text/css; charset=utf-8",
61
- "etag": '"17293-5C6kMCq9PaxOjrbr7Or2ITIT6Lo"',
62
- "mtime": "2026-06-20T07:49:57.206Z",
63
- "size": 94867,
64
- "path": "../public/assets/index-D7wwbwly.css"
65
- },
66
- "/assets/minimax-BPMzvuL-.jpeg": {
67
- "type": "image/jpeg",
68
- "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
69
- "mtime": "2026-06-20T07:49:57.206Z",
70
- "size": 6918,
71
- "path": "../public/assets/minimax-BPMzvuL-.jpeg"
68
+ "etag": '"17a41-dqXsKD+MttGurME/qf9nmjJckRY"',
69
+ "mtime": "2026-06-21T02:55:38.892Z",
70
+ "size": 96833,
71
+ "path": "../public/assets/index-CX796gvi.css"
72
72
  },
73
- "/assets/json-viewer-BbU0n8eM.js": {
73
+ "/assets/json-viewer-IXejqXB0.js": {
74
74
  "type": "text/javascript; charset=utf-8",
75
- "etag": '"1e652-M43x58m2bH0hBJMNDpGZ/fDfT5s"',
76
- "mtime": "2026-06-20T07:49:57.207Z",
77
- "size": 124498,
78
- "path": "../public/assets/json-viewer-BbU0n8eM.js"
75
+ "etag": '"1e653-JGlILG827/WNy4+hkJi+2gTndaU"',
76
+ "mtime": "2026-06-21T02:55:38.893Z",
77
+ "size": 124499,
78
+ "path": "../public/assets/json-viewer-IXejqXB0.js"
79
79
  },
80
- "/assets/main-CZT_F-gu.js": {
80
+ "/assets/ResponseView-Cvc-ct4E.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"5138c-VrHslavJjlr9YgDZ13bVuWsHMqQ"',
83
- "mtime": "2026-06-20T07:49:57.207Z",
84
- "size": 332684,
85
- "path": "../public/assets/main-CZT_F-gu.js"
82
+ "etag": '"6aab-UwVVOTtKRLo51GriiJxR9m6Feak"',
83
+ "mtime": "2026-06-21T02:55:38.893Z",
84
+ "size": 27307,
85
+ "path": "../public/assets/ResponseView-Cvc-ct4E.js"
86
86
  },
87
- "/assets/ReplayDialog-CxUk_TF0.js": {
87
+ "/assets/ReplayDialog-Cl62N9PI.js": {
88
88
  "type": "text/javascript; charset=utf-8",
89
- "etag": '"11c0-esa6w7Upc98DpnzYsJtOduS9W7w"',
90
- "mtime": "2026-06-20T07:49:57.207Z",
91
- "size": 4544,
92
- "path": "../public/assets/ReplayDialog-CxUk_TF0.js"
89
+ "etag": '"2383-flKSjr0F8tecuRkIDNpAYjivJcI"',
90
+ "mtime": "2026-06-21T02:55:38.893Z",
91
+ "size": 9091,
92
+ "path": "../public/assets/ReplayDialog-Cl62N9PI.js"
93
93
  },
94
- "/assets/RequestAnatomy-DIlzjgjJ.js": {
94
+ "/assets/RequestAnatomy-DgQWGvjs.js": {
95
95
  "type": "text/javascript; charset=utf-8",
96
- "etag": '"142a-Say/U0wBSXeJXbMdzIFCiD2Yq8o"',
97
- "mtime": "2026-06-20T07:49:57.207Z",
98
- "size": 5162,
99
- "path": "../public/assets/RequestAnatomy-DIlzjgjJ.js"
96
+ "etag": '"1426-61x287FrHKfb80GSEu4faAyIsQQ"',
97
+ "mtime": "2026-06-21T02:55:38.893Z",
98
+ "size": 5158,
99
+ "path": "../public/assets/RequestAnatomy-DgQWGvjs.js"
100
100
  },
101
- "/assets/ResponseView-DQCuKJ1G.js": {
101
+ "/assets/StreamingChunkSequence-BCQaCAIe.js": {
102
102
  "type": "text/javascript; charset=utf-8",
103
- "etag": '"6c88-pdMfEcfub7CjHnge4PzxOHEcbPE"',
104
- "mtime": "2026-06-20T07:49:57.207Z",
105
- "size": 27784,
106
- "path": "../public/assets/ResponseView-DQCuKJ1G.js"
103
+ "etag": '"d82-mxAzpHhDD3SrHO5SmP2+22ql54A"',
104
+ "mtime": "2026-06-21T02:55:38.893Z",
105
+ "size": 3458,
106
+ "path": "../public/assets/StreamingChunkSequence-BCQaCAIe.js"
107
107
  },
108
- "/assets/zhipuai-BPNAnxo-.svg": {
109
- "type": "image/svg+xml",
110
- "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
111
- "mtime": "2026-06-20T07:49:57.206Z",
112
- "size": 11256,
113
- "path": "../public/assets/zhipuai-BPNAnxo-.svg"
108
+ "/assets/ProxyViewerContainer-CbW5VRER.js": {
109
+ "type": "text/javascript; charset=utf-8",
110
+ "etag": '"7d8fb-FLtU4XIXbbkqJROwzR9ncAvk86E"',
111
+ "mtime": "2026-06-21T02:55:38.893Z",
112
+ "size": 514299,
113
+ "path": "../public/assets/ProxyViewerContainer-CbW5VRER.js"
114
114
  },
115
115
  "/assets/qwen-CONDcHqt.png": {
116
116
  "type": "image/png",
117
117
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
118
- "mtime": "2026-06-20T07:49:57.206Z",
118
+ "mtime": "2026-06-21T02:55:38.892Z",
119
119
  "size": 357059,
120
120
  "path": "../public/assets/qwen-CONDcHqt.png"
121
121
  },
122
- "/assets/_sessionId-dY1TTl7N.js": {
123
- "type": "text/javascript; charset=utf-8",
124
- "etag": '"d2-gUdCO5tW0loAHOM6KOFd73K/+Es"',
125
- "mtime": "2026-06-20T07:49:57.206Z",
126
- "size": 210,
127
- "path": "../public/assets/_sessionId-dY1TTl7N.js"
122
+ "/assets/zhipuai-BPNAnxo-.svg": {
123
+ "type": "image/svg+xml",
124
+ "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
125
+ "mtime": "2026-06-21T02:55:38.892Z",
126
+ "size": 11256,
127
+ "path": "../public/assets/zhipuai-BPNAnxo-.svg"
128
128
  },
129
- "/assets/ProxyViewerContainer-Da0jpBkp.js": {
129
+ "/assets/main-2NlGzgOe.js": {
130
130
  "type": "text/javascript; charset=utf-8",
131
- "etag": '"79801-1JDGhvSsOIGys3Enbh6ed4oyMoE"',
132
- "mtime": "2026-06-20T07:49:57.207Z",
133
- "size": 497665,
134
- "path": "../public/assets/ProxyViewerContainer-Da0jpBkp.js"
131
+ "etag": '"5138c-nIshtdU9NYoVXvBBEN1D74xi9gk"',
132
+ "mtime": "2026-06-21T02:55:38.892Z",
133
+ "size": 332684,
134
+ "path": "../public/assets/main-2NlGzgOe.js"
135
135
  },
136
- "/assets/StreamingChunkSequence-DHk4SGGL.js": {
136
+ "/assets/_sessionId-CcD_aLGq.js": {
137
137
  "type": "text/javascript; charset=utf-8",
138
- "etag": '"d81-q+WustPR2MRDE/q2wIQ/H/VHcAc"',
139
- "mtime": "2026-06-20T07:49:57.207Z",
140
- "size": 3457,
141
- "path": "../public/assets/StreamingChunkSequence-DHk4SGGL.js"
138
+ "etag": '"d2-ELNVvbTmYkGjU2wEF0iGQm6YfmM"',
139
+ "mtime": "2026-06-21T02:55:38.892Z",
140
+ "size": 210,
141
+ "path": "../public/assets/_sessionId-CcD_aLGq.js"
142
142
  }
143
143
  };
144
144
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/agent-inspector",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "description": "Agent observability and knowledge capture layer for AI coding tools.",
6
6
  "license": "MIT",
@@ -3,13 +3,8 @@ import 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 (or
7
- * any user who hasn't yet dismissed it), explains the per-tab data
8
- * shapes the proxy captures, and disappears forever on dismissal.
9
- *
10
- * The "seen" state lives in the server's runtime config — see
11
- * `useOnboarding` — so the dismissal persists across browser sessions
12
- * and devices for the same install.
6
+ * First-launch onboarding banner. Shows once on a fresh install and persists
7
+ * dismissal through the server runtime config.
13
8
  */
14
9
  export function OnboardingBanner(): JSX.Element | null {
15
10
  const { hasSeenOnboarding, isLoading, markSeen } = useOnboarding();
@@ -22,22 +17,19 @@ export function OnboardingBanner(): JSX.Element | null {
22
17
  aria-label="Onboarding tip"
23
18
  className="mx-4 mt-2 mb-1 flex items-start gap-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-sm"
24
19
  >
25
- <div className="flex-1 min-w-0">
26
- <div className="font-medium text-amber-600 dark:text-amber-400 mb-1">
27
- Quick tour of the log tabs
20
+ <div className="min-w-0 flex-1">
21
+ <div className="mb-1 font-medium text-amber-600 dark:text-amber-400">
22
+ Agent Inspector is ready
28
23
  </div>
29
- <ul className="space-y-0.5 text-muted-foreground text-xs leading-relaxed">
24
+ <ul className="space-y-0.5 text-xs leading-relaxed text-muted-foreground">
30
25
  <li>
31
- <strong>Request</strong> / <strong>Response</strong> structured views of what the
32
- proxy sent and received.
26
+ <strong>Trace</strong>: requests, responses, streaming chunks, tools, and timing.
33
27
  </li>
34
28
  <li>
35
- <strong>Headers</strong> request and response headers after proxy processing.
29
+ <strong>Replay</strong>: resend captured requests and compare provider behavior.
36
30
  </li>
37
31
  <li>
38
- <strong>Raw Headers</strong> / <strong>Raw Request</strong> /{" "}
39
- <strong>Raw Response</strong> — exact bytes from the upstream provider (visible in Full
40
- mode).
32
+ <strong>Memory</strong>: create reviewable candidates before promotion to OpenClaw.
41
33
  </li>
42
34
  </ul>
43
35
  </div>
@@ -46,7 +38,7 @@ export function OnboardingBanner(): JSX.Element | null {
46
38
  onClick={() => {
47
39
  void markSeen();
48
40
  }}
49
- className="inline-flex items-center gap-1.5 text-xs h-8 px-3 rounded-md border border-amber-500/40 text-amber-700 dark:text-amber-300 hover:bg-amber-500/10 transition-colors shrink-0"
41
+ className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-amber-500/40 px-3 text-xs text-amber-700 transition-colors hover:bg-amber-500/10 dark:text-amber-300"
50
42
  aria-label="Dismiss onboarding tip"
51
43
  >
52
44
  <Check className="size-3.5" />
@@ -57,7 +49,7 @@ export function OnboardingBanner(): JSX.Element | null {
57
49
  onClick={() => {
58
50
  void markSeen();
59
51
  }}
60
- className="text-muted-foreground hover:text-foreground transition-colors shrink-0 p-1 -m-1"
52
+ className="-m-1 shrink-0 p-1 text-muted-foreground transition-colors hover:text-foreground"
61
53
  aria-label="Dismiss"
62
54
  >
63
55
  <X className="size-3.5" />
@@ -3,6 +3,8 @@ import { ArrowLeft, Check, Copy, Download, Plus } from "lucide-react";
3
3
 
4
4
  import type { CapturedLog } from "../proxy/schemas";
5
5
  import { exportLogsAsZip } from "../lib/export-logs";
6
+ import type { TimeDisplayFormat } from "../lib/runtimeConfig";
7
+ import { formatTimestampRange } from "../lib/timeDisplay";
6
8
  import { formatTokens } from "../lib/utils";
7
9
  import packageJson from "../../package.json";
8
10
  import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";
@@ -33,17 +35,11 @@ function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut:
33
35
  return { totalIn, totalOut };
34
36
  }
35
37
 
36
- function formatTimeRange(logs: CapturedLog[]): string | null {
38
+ function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
37
39
  const first = logs[0];
38
40
  const last = logs[logs.length - 1];
39
41
  if (first === undefined || last === undefined) return null;
40
- const format = (iso: string): string =>
41
- new Date(iso).toLocaleTimeString([], {
42
- hour: "2-digit",
43
- minute: "2-digit",
44
- second: "2-digit",
45
- });
46
- return `${format(first.timestamp)} - ${format(last.timestamp)}`;
42
+ return formatTimestampRange(first.timestamp, last.timestamp, timeDisplayFormat);
47
43
  }
48
44
 
49
45
  function getFirstUserAgent(logs: CapturedLog[]): string | null {
@@ -132,14 +128,19 @@ function SessionContextBar({
132
128
  logs,
133
129
  totalIn,
134
130
  totalOut,
131
+ timeDisplayFormat,
135
132
  }: {
136
133
  sessionId: string;
137
134
  logs: CapturedLog[];
138
135
  totalIn: number;
139
136
  totalOut: number;
137
+ timeDisplayFormat: TimeDisplayFormat;
140
138
  }): JSX.Element {
141
139
  const [copied, setCopied] = useState(false);
142
- const timeRange = useMemo(() => formatTimeRange(logs), [logs]);
140
+ const timeRange = useMemo(
141
+ () => formatTimeRange(logs, timeDisplayFormat),
142
+ [logs, timeDisplayFormat],
143
+ );
143
144
  const userAgent = useMemo(() => getFirstUserAgent(logs), [logs]);
144
145
 
145
146
  const handleCopyLink = useCallback(() => {
@@ -216,6 +217,8 @@ export type ProxyViewerProps = {
216
217
  strip: boolean;
217
218
  /** Slow-response threshold in seconds. `0` disables the warning indicator. */
218
219
  slowResponseThresholdSeconds: number;
220
+ /** Controls whether timestamps render as compact local time or full ISO strings. */
221
+ timeDisplayFormat: TimeDisplayFormat;
219
222
  /** Hide the session filter dropdown. Used on `/session/$id` routes where
220
223
  * the session is already pinned by the URL and the dropdown would just
221
224
  * fight the URL state. */
@@ -238,6 +241,7 @@ export function ProxyViewer({
238
241
  onViewModeChange,
239
242
  strip,
240
243
  slowResponseThresholdSeconds,
244
+ timeDisplayFormat,
241
245
  hideSessionFilter = false,
242
246
  pinnedSessionId,
243
247
  }: ProxyViewerProps): JSX.Element {
@@ -249,7 +253,7 @@ export function ProxyViewer({
249
253
  );
250
254
  const logListRef = useRef<HTMLDivElement>(null);
251
255
  const logListWrapperRef = useRef<HTMLDivElement>(null);
252
- useKeyboardNavigation(logListRef, logListWrapperRef);
256
+ useKeyboardNavigation(logListRef, logListWrapperRef, { pageWide: true });
253
257
 
254
258
  useEffect(() => {
255
259
  const perCrabDuration = 400;
@@ -291,6 +295,7 @@ export function ProxyViewer({
291
295
  }, []);
292
296
 
293
297
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
298
+ const hasPinnedSessionContext = pinnedSessionId !== undefined;
294
299
  const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
295
300
  const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
296
301
  const handleCompareWithPrevious = useCallback(
@@ -380,6 +385,7 @@ export function ProxyViewer({
380
385
  logs={logs}
381
386
  totalIn={totalIn}
382
387
  totalOut={totalOut}
388
+ timeDisplayFormat={timeDisplayFormat}
383
389
  />
384
390
  )}
385
391
 
@@ -438,12 +444,14 @@ export function ProxyViewer({
438
444
  </button>
439
445
  </div>
440
446
  <div className="flex-1" />
441
- <span className="text-muted-foreground text-xs font-mono">
442
- {logs.length} request{logs.length !== 1 ? "s" : ""}
443
- {totalIn > 0 || totalOut > 0
444
- ? ` · ${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`
445
- : ""}
446
- </span>
447
+ {!hasPinnedSessionContext && (
448
+ <span className="text-muted-foreground text-xs font-mono">
449
+ {logs.length} request{logs.length !== 1 ? "s" : ""}
450
+ {totalIn > 0 || totalOut > 0
451
+ ? ` · ${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`
452
+ : ""}
453
+ </span>
454
+ )}
447
455
  {logs.length > 0 && (
448
456
  <button
449
457
  type="button"
@@ -527,6 +535,8 @@ export function ProxyViewer({
527
535
  comparisonPredecessors={comparisonPredecessors}
528
536
  onClearGroup={onClearGroup}
529
537
  standalone={groups.length === 1}
538
+ hasPinnedSessionContext={hasPinnedSessionContext}
539
+ timeDisplayFormat={timeDisplayFormat}
530
540
  />
531
541
  ))}
532
542
  </div>
@@ -330,7 +330,7 @@ export function ProxyViewerContainer({
330
330
 
331
331
  // Read the strip config once at the container so the virtualized list does
332
332
  // not need N independent SWR subscriptions per row.
333
- const { strip, slowResponseThresholdSeconds } = useStripConfig();
333
+ const { strip, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
334
334
 
335
335
  return (
336
336
  <>
@@ -354,6 +354,7 @@ export function ProxyViewerContainer({
354
354
  onViewModeChange={setViewMode}
355
355
  strip={strip}
356
356
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
357
+ timeDisplayFormat={timeDisplayFormat}
357
358
  // Session filter is the URL's job when `initialSessionId` was given.
358
359
  hideSessionFilter={initialSessionId !== undefined}
359
360
  pinnedSessionId={initialSessionId}
@@ -110,30 +110,16 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
110
110
  }
111
111
  if (result.cacheCreationInputTokens !== undefined && result.cacheCreationInputTokens > 0) {
112
112
  tokenParts.push(
113
- <TooltipProvider key="cache-create">
114
- <Tooltip>
115
- <TooltipTrigger asChild>
116
- <span className="font-mono tabular-nums text-emerald-400">
117
- +{result.cacheCreationInputTokens} cache
118
- </span>
119
- </TooltipTrigger>
120
- <TooltipContent>Tokens cached for reuse, reducing future API cost</TooltipContent>
121
- </Tooltip>
122
- </TooltipProvider>,
113
+ <span key="cache-create" className="font-mono tabular-nums text-emerald-400">
114
+ KV Cache +{result.cacheCreationInputTokens}
115
+ </span>,
123
116
  );
124
117
  }
125
118
  if (result.cacheReadInputTokens !== undefined && result.cacheReadInputTokens > 0) {
126
119
  tokenParts.push(
127
- <TooltipProvider key="cache-read">
128
- <Tooltip>
129
- <TooltipTrigger asChild>
130
- <span className="font-mono tabular-nums text-purple-400">
131
- ~{result.cacheReadInputTokens} cached
132
- </span>
133
- </TooltipTrigger>
134
- <TooltipContent>Tokens served from cache, reducing API cost</TooltipContent>
135
- </Tooltip>
136
- </TooltipProvider>,
120
+ <span key="cache-read" className="font-mono tabular-nums text-purple-400">
121
+ KV Cache ~{result.cacheReadInputTokens}
122
+ </span>,
137
123
  );
138
124
  }
139
125
  const displayTokens: ReactNode[] = [];
@@ -1,12 +1,16 @@
1
- import { type JSX, useState, useCallback } from "react";
2
- import { Settings } from "lucide-react";
1
+ import { type JSX, useState, useCallback, useMemo } from "react";
2
+ import { Check, Copy, Settings, Terminal } from "lucide-react";
3
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
4
4
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
5
5
  import { Button } from "../ui/button";
6
6
  import { ProvidersPanel } from "./ProvidersPanel";
7
7
  import { useProviders } from "../../lib/useProviders";
8
8
  import { useStripConfig } from "../../lib/useStripConfig";
9
- import { MAX_SLOW_RESPONSE_THRESHOLD_SECONDS } from "../../lib/runtimeConfig";
9
+ import {
10
+ MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
11
+ TimeDisplayFormatSchema,
12
+ type TimeDisplayFormat,
13
+ } from "../../lib/runtimeConfig";
10
14
 
11
15
  export function SettingsDialog(): JSX.Element {
12
16
  const [open, setOpen] = useState(false);
@@ -68,6 +72,7 @@ export function SettingsDialog(): JSX.Element {
68
72
  <TabsList>
69
73
  <TabsTrigger value="providers">Providers</TabsTrigger>
70
74
  <TabsTrigger value="proxy">Proxy</TabsTrigger>
75
+ <TabsTrigger value="onboarding">Onboarding</TabsTrigger>
71
76
  </TabsList>
72
77
 
73
78
  <div className="mt-4 overflow-y-auto flex-1 pr-3">
@@ -89,6 +94,9 @@ export function SettingsDialog(): JSX.Element {
89
94
  <TabsContent value="proxy">
90
95
  <ProxySettingsTab />
91
96
  </TabsContent>
97
+ <TabsContent value="onboarding">
98
+ <OnboardingSettingsTab />
99
+ </TabsContent>
92
100
  </div>
93
101
  </Tabs>
94
102
  </DialogContent>
@@ -96,13 +104,104 @@ export function SettingsDialog(): JSX.Element {
96
104
  );
97
105
  }
98
106
 
107
+ function CopyableSetupValue({
108
+ id,
109
+ label,
110
+ value,
111
+ copiedId,
112
+ onCopy,
113
+ }: {
114
+ id: string;
115
+ label: string;
116
+ value: string;
117
+ copiedId: string | null;
118
+ onCopy: (id: string, value: string) => void;
119
+ }): JSX.Element {
120
+ const copied = copiedId === id;
121
+ return (
122
+ <div className="rounded-md border border-border bg-muted/20 px-3 py-2">
123
+ <div className="mb-1 text-xs font-medium text-muted-foreground">{label}</div>
124
+ <div className="flex min-w-0 items-center gap-2">
125
+ <code className="min-w-0 flex-1 truncate font-mono text-xs text-foreground">{value}</code>
126
+ <Button
127
+ type="button"
128
+ variant="ghost"
129
+ size="icon"
130
+ className="size-7 shrink-0"
131
+ onClick={() => onCopy(id, value)}
132
+ aria-label={copied ? `Copied ${label}` : `Copy ${label}`}
133
+ >
134
+ {copied ? <Check className="size-3.5 text-emerald-500" /> : <Copy className="size-3.5" />}
135
+ </Button>
136
+ </div>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ function OnboardingSettingsTab(): JSX.Element {
142
+ const [copiedId, setCopiedId] = useState<string | null>(null);
143
+ const origin = useMemo(() => {
144
+ if (typeof window === "undefined") return "http://localhost:25947";
145
+ return window.location.origin;
146
+ }, []);
147
+ const values = useMemo(
148
+ () => [
149
+ { id: "skill", label: "Codex skill", value: "agent-inspector onboard --force" },
150
+ { id: "mcp", label: "MCP URL", value: `${origin}/api/mcp` },
151
+ { id: "proxy", label: "Proxy URL", value: `${origin}/proxy` },
152
+ { id: "anthropic", label: "Anthropic base", value: `ANTHROPIC_BASE_URL=${origin}/proxy` },
153
+ ],
154
+ [origin],
155
+ );
156
+
157
+ const handleCopy = useCallback((id: string, value: string) => {
158
+ void window.navigator.clipboard.writeText(value).then(() => {
159
+ setCopiedId(id);
160
+ setTimeout(() => setCopiedId(null), 1600);
161
+ });
162
+ }, []);
163
+
164
+ return (
165
+ <div className="space-y-4">
166
+ <div className="flex items-center gap-2">
167
+ <Terminal className="size-4 text-muted-foreground" />
168
+ <h3 className="text-sm font-semibold">Agent onboarding</h3>
169
+ </div>
170
+ <div className="grid gap-2">
171
+ {values.map((item) => (
172
+ <CopyableSetupValue
173
+ key={item.id}
174
+ id={item.id}
175
+ label={item.label}
176
+ value={item.value}
177
+ copiedId={copiedId}
178
+ onCopy={handleCopy}
179
+ />
180
+ ))}
181
+ </div>
182
+ <div className="grid gap-2 rounded-md border border-border bg-background px-3 py-2 text-xs text-muted-foreground">
183
+ <div className="flex items-center gap-2">
184
+ <Check className="size-3.5 text-emerald-500" />
185
+ <span>Provider test creates a traceable memory probe session.</span>
186
+ </div>
187
+ <div className="flex items-center gap-2">
188
+ <Check className="size-3.5 text-emerald-500" />
189
+ <span>Captured sessions can produce reviewable memory candidates.</span>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
99
196
  function ProxySettingsTab(): JSX.Element {
100
197
  const {
101
198
  strip,
102
199
  slowResponseThresholdSeconds,
200
+ timeDisplayFormat,
103
201
  isLoading,
104
202
  setStrip,
105
203
  setSlowResponseThresholdSeconds,
204
+ setTimeDisplayFormat,
106
205
  } = useStripConfig();
107
206
  const [error, setError] = useState<string | null>(null);
108
207
  const [pending, setPending] = useState(false);
@@ -137,6 +236,21 @@ function ProxySettingsTab(): JSX.Element {
137
236
  [setSlowResponseThresholdSeconds],
138
237
  );
139
238
 
239
+ const handleTimeDisplayFormatChange = useCallback(
240
+ async (next: TimeDisplayFormat) => {
241
+ setError(null);
242
+ setPending(true);
243
+ try {
244
+ await setTimeDisplayFormat(next);
245
+ } catch (err) {
246
+ setError(err instanceof Error ? err.message : String(err));
247
+ } finally {
248
+ setPending(false);
249
+ }
250
+ },
251
+ [setTimeDisplayFormat],
252
+ );
253
+
140
254
  return (
141
255
  <div className="space-y-4">
142
256
  <div className="space-y-1">
@@ -196,6 +310,29 @@ function ProxySettingsTab(): JSX.Element {
196
310
  </div>
197
311
  </div>
198
312
 
313
+ <div className="space-y-1">
314
+ <label htmlFor="time-display-format" className="text-sm font-semibold">
315
+ Time display
316
+ </label>
317
+ <p className="text-xs text-muted-foreground">
318
+ Controls timestamps in session summaries, conversation headers, and log rows.
319
+ </p>
320
+ <select
321
+ id="time-display-format"
322
+ value={timeDisplayFormat}
323
+ disabled={isLoading || pending}
324
+ onChange={(event) => {
325
+ const parsed = TimeDisplayFormatSchema.safeParse(event.currentTarget.value);
326
+ if (!parsed.success) return;
327
+ void handleTimeDisplayFormatChange(parsed.data);
328
+ }}
329
+ className="h-8 rounded-md border border-input bg-background px-2 text-sm disabled:cursor-not-allowed disabled:opacity-50"
330
+ >
331
+ <option value="time">Time only</option>
332
+ <option value="full">Full ISO</option>
333
+ </select>
334
+ </div>
335
+
199
336
  {error !== null && <p className="text-xs text-destructive">Failed to save: {error}</p>}
200
337
  </div>
201
338
  );