@tonyclaw/agent-inspector 2.1.21 → 2.1.23

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 (48) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Pii_cIqn.js → CompareDrawer-Cvgl_Jmi.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-CNLLZq37.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-BqnYC8ze.js → ReplayDialog-DeEELx0X.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-I8iBshz1.js → RequestAnatomy-CrLSqdiA.js} +1 -1
  6. package/.output/public/assets/ResponseView-CFIqDL5E.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-BrSx21IQ.js → StreamingChunkSequence-DSRjbgqb.js} +1 -1
  8. package/.output/public/assets/_sessionId-ZuH6-ZX5.js +1 -0
  9. package/.output/public/assets/index-B26q0-ch.js +1 -0
  10. package/.output/public/assets/{index-Dw-jXKKY.js → index-BZxLBVYd.js} +1 -1
  11. package/.output/public/assets/index-CxszCIFm.css +1 -0
  12. package/.output/public/assets/{json-viewer-D5mzV0oV.js → json-viewer-DERQNZRV.js} +1 -1
  13. package/.output/public/assets/{main-B7OIQMeA.js → main-BoshTI0W.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +42 -42
  15. package/.output/server/{_sessionId-DeU_JdSX.mjs → _sessionId-BYGtH1pK.mjs} +18 -3
  16. package/.output/server/_ssr/{CompareDrawer-DgDbG4oF.mjs → CompareDrawer-OchnKiac.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-BOszhOJD.mjs → ProxyViewerContainer-C2lwYwDu.mjs} +2747 -2639
  18. package/.output/server/_ssr/{ReplayDialog-Cj4gWQv5.mjs → ReplayDialog-BYwaF14u.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-CVzNAA03.mjs → RequestAnatomy-DTueiqT5.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-DLNpai6P.mjs → ResponseView-CqB9NKHB.mjs} +202 -55
  21. package/.output/server/_ssr/{StreamingChunkSequence-BiH8EEQB.mjs → StreamingChunkSequence-DENB9VUj.mjs} +2 -2
  22. package/.output/server/_ssr/{index-DaWXoUgf.mjs → index-BR3ea3Zb.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-CqqNYbmS.mjs → json-viewer-BU9ZUaHK.mjs} +3 -3
  25. package/.output/server/_ssr/{router-Di7D0Bcm.mjs → router-DT6BcrEg.mjs} +4 -4
  26. package/.output/server/{_tanstack-start-manifest_v-BR0YvW29.mjs → _tanstack-start-manifest_v-xQ1BRoZw.mjs} +1 -1
  27. package/.output/server/index.mjs +75 -75
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +71 -15
  30. package/src/components/ProxyViewerContainer.tsx +1 -1
  31. package/src/components/proxy-viewer/ConversationGroupList.tsx +23 -7
  32. package/src/components/proxy-viewer/ConversationHeader.tsx +14 -7
  33. package/src/components/proxy-viewer/LogEntry.tsx +10 -0
  34. package/src/components/proxy-viewer/ResponseView.tsx +5 -1
  35. package/src/components/proxy-viewer/ToolTraceEvents.tsx +19 -31
  36. package/src/components/proxy-viewer/TurnGroup.tsx +21 -7
  37. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +37 -4
  38. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +17 -3
  39. package/src/components/proxy-viewer/formats/index.tsx +8 -3
  40. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +122 -16
  41. package/src/components/proxy-viewer/logFocus.ts +11 -3
  42. package/src/components/proxy-viewer/viewerState.ts +22 -7
  43. package/src/routes/session/$sessionId.tsx +27 -1
  44. package/.output/public/assets/ProxyViewerContainer-BxselkhN.js +0 -106
  45. package/.output/public/assets/ResponseView-wZzskUas.js +0 -3
  46. package/.output/public/assets/_sessionId-ByxbBMbE.js +0 -1
  47. package/.output/public/assets/index-BjzVMXJq.css +0 -1
  48. package/.output/public/assets/index-DRCRDo_E.js +0 -1
@@ -38,114 +38,114 @@ const assets = {
38
38
  "/assets/alibaba-TTwafVwX.svg": {
39
39
  "type": "image/svg+xml",
40
40
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
- "mtime": "2026-07-06T08:04:09.445Z",
41
+ "mtime": "2026-07-06T23:30:24.816Z",
42
42
  "size": 5915,
43
43
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
44
  },
45
- "/assets/json-viewer-D5mzV0oV.js": {
45
+ "/assets/index-B26q0-ch.js": {
46
46
  "type": "text/javascript; charset=utf-8",
47
- "etag": '"1ae6-lHZ9n4b2qU1XEccJknQFdUwXJyk"',
48
- "mtime": "2026-07-06T08:04:09.445Z",
49
- "size": 6886,
50
- "path": "../public/assets/json-viewer-D5mzV0oV.js"
51
- },
52
- "/assets/index-Dw-jXKKY.js": {
53
- "type": "text/javascript; charset=utf-8",
54
- "etag": '"1cd2c-qf0ccCcShwlpGimq6sOIhXqC/KY"',
55
- "mtime": "2026-07-06T08:04:09.445Z",
56
- "size": 118060,
57
- "path": "../public/assets/index-Dw-jXKKY.js"
58
- },
59
- "/assets/CompareDrawer-Pii_cIqn.js": {
60
- "type": "text/javascript; charset=utf-8",
61
- "etag": '"4bbf-N5NvXj/0Nfg4Lu/Fx8Wnk0zctKM"',
62
- "mtime": "2026-07-06T08:04:09.445Z",
63
- "size": 19391,
64
- "path": "../public/assets/CompareDrawer-Pii_cIqn.js"
65
- },
66
- "/assets/index-DRCRDo_E.js": {
67
- "type": "text/javascript; charset=utf-8",
68
- "etag": '"74-Jdw+P8c3q4x8zEb79oDR7U/GJ+w"',
69
- "mtime": "2026-07-06T08:04:09.445Z",
47
+ "etag": '"74-AZvnGEhntbiyIDS+wwwbTZjCuCw"',
48
+ "mtime": "2026-07-06T23:30:24.817Z",
70
49
  "size": 116,
71
- "path": "../public/assets/index-DRCRDo_E.js"
50
+ "path": "../public/assets/index-B26q0-ch.js"
72
51
  },
73
- "/assets/index-BjzVMXJq.css": {
74
- "type": "text/css; charset=utf-8",
75
- "etag": '"21738-gXfp17wcRouxMwG65GOcFYAr+WQ"',
76
- "mtime": "2026-07-06T08:04:09.445Z",
77
- "size": 137016,
78
- "path": "../public/assets/index-BjzVMXJq.css"
52
+ "/assets/json-viewer-DERQNZRV.js": {
53
+ "type": "text/javascript; charset=utf-8",
54
+ "etag": '"1ae6-A6B2jiKFX+jryATLfM15t7QIsvM"',
55
+ "mtime": "2026-07-06T23:30:24.817Z",
56
+ "size": 6886,
57
+ "path": "../public/assets/json-viewer-DERQNZRV.js"
79
58
  },
80
59
  "/assets/minimax-BPMzvuL-.jpeg": {
81
60
  "type": "image/jpeg",
82
61
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
83
- "mtime": "2026-07-06T08:04:09.445Z",
62
+ "mtime": "2026-07-06T23:30:24.816Z",
84
63
  "size": 6918,
85
64
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
86
65
  },
87
- "/assets/StreamingChunkSequence-BrSx21IQ.js": {
88
- "type": "text/javascript; charset=utf-8",
89
- "etag": '"dd6-IWeACKaGgb4U4nokqp1reXLkggk"',
90
- "mtime": "2026-07-06T08:04:09.445Z",
91
- "size": 3542,
92
- "path": "../public/assets/StreamingChunkSequence-BrSx21IQ.js"
66
+ "/assets/index-CxszCIFm.css": {
67
+ "type": "text/css; charset=utf-8",
68
+ "etag": '"219d3-hGkO4OXmpghyDnxd7KIL9unRxwQ"',
69
+ "mtime": "2026-07-06T23:30:24.817Z",
70
+ "size": 137683,
71
+ "path": "../public/assets/index-CxszCIFm.css"
93
72
  },
94
- "/assets/main-B7OIQMeA.js": {
73
+ "/assets/CompareDrawer-Cvgl_Jmi.js": {
95
74
  "type": "text/javascript; charset=utf-8",
96
- "etag": '"512fd-gnVzFUWaADF+U7ewMLeXswLgfbs"',
97
- "mtime": "2026-07-06T08:04:09.445Z",
98
- "size": 332541,
99
- "path": "../public/assets/main-B7OIQMeA.js"
75
+ "etag": '"4bbf-qdrjZBBOyzn/JHMYJ2UAiNDDbFQ"',
76
+ "mtime": "2026-07-06T23:30:24.817Z",
77
+ "size": 19391,
78
+ "path": "../public/assets/CompareDrawer-Cvgl_Jmi.js"
100
79
  },
101
- "/assets/ReplayDialog-BqnYC8ze.js": {
80
+ "/assets/index-BZxLBVYd.js": {
102
81
  "type": "text/javascript; charset=utf-8",
103
- "etag": '"31d6-5kfsBNTWL1po+1YwnLQiVcjuScI"',
104
- "mtime": "2026-07-06T08:04:09.445Z",
105
- "size": 12758,
106
- "path": "../public/assets/ReplayDialog-BqnYC8ze.js"
82
+ "etag": '"1cd2c-eSvMM8zbdBG02FS4JJNg0vJ028w"',
83
+ "mtime": "2026-07-06T23:30:24.817Z",
84
+ "size": 118060,
85
+ "path": "../public/assets/index-BZxLBVYd.js"
107
86
  },
108
- "/assets/RequestAnatomy-I8iBshz1.js": {
87
+ "/assets/RequestAnatomy-CrLSqdiA.js": {
109
88
  "type": "text/javascript; charset=utf-8",
110
- "etag": '"43f2-LkSmr545/CcQXJegGJryyDFVlKs"',
111
- "mtime": "2026-07-06T08:04:09.445Z",
89
+ "etag": '"43f2-YwOCt5e63SibXMhWqzHRxd1nNC8"',
90
+ "mtime": "2026-07-06T23:30:24.817Z",
112
91
  "size": 17394,
113
- "path": "../public/assets/RequestAnatomy-I8iBshz1.js"
92
+ "path": "../public/assets/RequestAnatomy-CrLSqdiA.js"
114
93
  },
115
- "/assets/qwen-CONDcHqt.png": {
116
- "type": "image/png",
117
- "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
118
- "mtime": "2026-07-06T08:04:09.445Z",
119
- "size": 357059,
120
- "path": "../public/assets/qwen-CONDcHqt.png"
94
+ "/assets/ReplayDialog-DeEELx0X.js": {
95
+ "type": "text/javascript; charset=utf-8",
96
+ "etag": '"31d6-hWOAjkfta2prr+9EmQOXzNOAzTU"',
97
+ "mtime": "2026-07-06T23:30:24.817Z",
98
+ "size": 12758,
99
+ "path": "../public/assets/ReplayDialog-DeEELx0X.js"
121
100
  },
122
- "/assets/_sessionId-ByxbBMbE.js": {
101
+ "/assets/ResponseView-CFIqDL5E.js": {
123
102
  "type": "text/javascript; charset=utf-8",
124
- "etag": '"d2-bjGWPi/hOvCUGg3eqEhfiUwW/YU"',
125
- "mtime": "2026-07-06T08:04:09.445Z",
126
- "size": 210,
127
- "path": "../public/assets/_sessionId-ByxbBMbE.js"
103
+ "etag": '"47fa-+VN13C6cvB2BYNKPjLEy9ibIswM"',
104
+ "mtime": "2026-07-06T23:30:24.817Z",
105
+ "size": 18426,
106
+ "path": "../public/assets/ResponseView-CFIqDL5E.js"
107
+ },
108
+ "/assets/main-BoshTI0W.js": {
109
+ "type": "text/javascript; charset=utf-8",
110
+ "etag": '"512fd-NlLbmALn+PuYHWA4Dj6BVyzFIuM"',
111
+ "mtime": "2026-07-06T23:30:24.817Z",
112
+ "size": 332541,
113
+ "path": "../public/assets/main-BoshTI0W.js"
128
114
  },
129
115
  "/assets/zhipuai-BPNAnxo-.svg": {
130
116
  "type": "image/svg+xml",
131
117
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
132
- "mtime": "2026-07-06T08:04:09.445Z",
118
+ "mtime": "2026-07-06T23:30:24.816Z",
133
119
  "size": 11256,
134
120
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
135
121
  },
136
- "/assets/ResponseView-wZzskUas.js": {
122
+ "/assets/_sessionId-ZuH6-ZX5.js": {
137
123
  "type": "text/javascript; charset=utf-8",
138
- "etag": '"3fbb-bdXf95SAw1FOCQV45uO2SEehsJw"',
139
- "mtime": "2026-07-06T08:04:09.445Z",
140
- "size": 16315,
141
- "path": "../public/assets/ResponseView-wZzskUas.js"
124
+ "etag": '"36c-yfRDDJm0mM1r+nbDh5oXBDpciWQ"',
125
+ "mtime": "2026-07-06T23:30:24.817Z",
126
+ "size": 876,
127
+ "path": "../public/assets/_sessionId-ZuH6-ZX5.js"
128
+ },
129
+ "/assets/StreamingChunkSequence-DSRjbgqb.js": {
130
+ "type": "text/javascript; charset=utf-8",
131
+ "etag": '"dd6-phWzlhQq/JJk54G5Z2rU0kzp2as"',
132
+ "mtime": "2026-07-06T23:30:24.817Z",
133
+ "size": 3542,
134
+ "path": "../public/assets/StreamingChunkSequence-DSRjbgqb.js"
135
+ },
136
+ "/assets/qwen-CONDcHqt.png": {
137
+ "type": "image/png",
138
+ "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
139
+ "mtime": "2026-07-06T23:30:24.816Z",
140
+ "size": 357059,
141
+ "path": "../public/assets/qwen-CONDcHqt.png"
142
142
  },
143
- "/assets/ProxyViewerContainer-BxselkhN.js": {
143
+ "/assets/ProxyViewerContainer-CNLLZq37.js": {
144
144
  "type": "text/javascript; charset=utf-8",
145
- "etag": '"ae583-F8VHf7p3x5cLC6mForh8s/N9d3A"',
146
- "mtime": "2026-07-06T08:04:09.445Z",
147
- "size": 714115,
148
- "path": "../public/assets/ProxyViewerContainer-BxselkhN.js"
145
+ "etag": '"aece0-rsojOMYS6lexxfh1G0dXZ/InUpE"',
146
+ "mtime": "2026-07-06T23:30:24.817Z",
147
+ "size": 716e3,
148
+ "path": "../public/assets/ProxyViewerContainer-CNLLZq37.js"
149
149
  }
150
150
  };
151
151
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/agent-inspector",
3
- "version": "2.1.21",
3
+ "version": "2.1.23",
4
4
  "type": "module",
5
5
  "description": "Agent observability and knowledge capture layer for AI coding tools.",
6
6
  "license": "MIT",
@@ -27,10 +27,12 @@ import {
27
27
  Gauge,
28
28
  Flag,
29
29
  Plus,
30
+ Search,
30
31
  Siren,
31
32
  Timer,
32
33
  Trash2,
33
34
  Upload,
35
+ X,
34
36
  } from "lucide-react";
35
37
 
36
38
  import type { CapturedLog } from "../contracts";
@@ -963,8 +965,13 @@ export function formatPaginationStatus({
963
965
  const last = logs[logs.length - 1];
964
966
  const range =
965
967
  first === undefined || last === undefined ? "Current page" : `#${first.id}-#${last.id}`;
966
- const total = pagination.total === null ? "logs" : `${pagination.total} logs`;
967
- return `Showing ${range} of ${total}`;
968
+ if (pagination.total === null) {
969
+ return `Showing ${logs.length} logs (${range})`;
970
+ }
971
+ if (pagination.total <= logs.length) {
972
+ return `Showing all ${pagination.total} logs`;
973
+ }
974
+ return `Showing ${logs.length} of ${pagination.total} logs (${range})`;
968
975
  }
969
976
 
970
977
  function LogPaginationButton({
@@ -1823,6 +1830,11 @@ export function ProxyViewer({
1823
1830
  const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
1824
1831
  const exportActionVisibility = getExportActionVisibility(captureMode, logs.length);
1825
1832
  const isPinnedSessionPage = pinnedSessionId !== undefined;
1833
+ const sessionInputValue = selectedSession === "__all__" ? "" : selectedSession;
1834
+ const sessionSelectValue =
1835
+ selectedSession === "__all__" || sessions.includes(selectedSession)
1836
+ ? selectedSession
1837
+ : "__typed__";
1826
1838
  const activeScopeLabel =
1827
1839
  hasSessionContext && sessionContextScope !== null
1828
1840
  ? truncateSessionId(sessionContextScope.label)
@@ -1847,6 +1859,20 @@ export function ProxyViewer({
1847
1859
  },
1848
1860
  [comparisonPredecessors],
1849
1861
  );
1862
+ const handleSessionInputChange = useCallback(
1863
+ (event: ChangeEvent<HTMLInputElement>) => {
1864
+ const value = event.currentTarget.value.trim();
1865
+ onSessionChange(value === "" ? "__all__" : value);
1866
+ },
1867
+ [onSessionChange],
1868
+ );
1869
+ const handleSessionSelectChange = useCallback(
1870
+ (value: string) => {
1871
+ if (value === "__typed__") return;
1872
+ onSessionChange(value);
1873
+ },
1874
+ [onSessionChange],
1875
+ );
1850
1876
 
1851
1877
  return (
1852
1878
  <div className="bg-background min-h-screen w-full text-foreground">
@@ -1904,19 +1930,49 @@ export function ProxyViewer({
1904
1930
  <SidebarPanel label="Filters">
1905
1931
  <div className="space-y-2">
1906
1932
  {!hideSessionFilter && (
1907
- <Select value={selectedSession} onValueChange={onSessionChange}>
1908
- <SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
1909
- <SelectValue placeholder="All sessions" />
1910
- </SelectTrigger>
1911
- <SelectContent>
1912
- <SelectItem value="__all__">All sessions</SelectItem>
1913
- {sessions.map((s) => (
1914
- <SelectItem key={s} value={s}>
1915
- {truncateSessionId(s)}
1916
- </SelectItem>
1917
- ))}
1918
- </SelectContent>
1919
- </Select>
1933
+ <>
1934
+ <div className="relative">
1935
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
1936
+ <input
1937
+ type="search"
1938
+ value={sessionInputValue}
1939
+ onChange={handleSessionInputChange}
1940
+ placeholder="Paste session ID"
1941
+ className="border border-input bg-background hover:bg-accent focus-visible:ring-ring h-9 w-full rounded-md px-8 font-mono text-xs text-foreground outline-none transition-colors placeholder:font-sans placeholder:text-muted-foreground focus-visible:ring-1"
1942
+ aria-label="Filter by session ID"
1943
+ title="Filter by exact session ID"
1944
+ />
1945
+ {sessionInputValue !== "" && (
1946
+ <button
1947
+ type="button"
1948
+ onClick={() => onSessionChange("__all__")}
1949
+ className="absolute right-1.5 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
1950
+ aria-label="Clear session filter"
1951
+ title="Clear session filter"
1952
+ >
1953
+ <X className="size-3.5" />
1954
+ </button>
1955
+ )}
1956
+ </div>
1957
+ <Select value={sessionSelectValue} onValueChange={handleSessionSelectChange}>
1958
+ <SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
1959
+ <SelectValue placeholder="All sessions" />
1960
+ </SelectTrigger>
1961
+ <SelectContent>
1962
+ <SelectItem value="__all__">All sessions</SelectItem>
1963
+ {sessionSelectValue === "__typed__" && (
1964
+ <SelectItem value="__typed__">
1965
+ Typed: {truncateSessionId(selectedSession)}
1966
+ </SelectItem>
1967
+ )}
1968
+ {sessions.map((s) => (
1969
+ <SelectItem key={s} value={s}>
1970
+ {truncateSessionId(s)}
1971
+ </SelectItem>
1972
+ ))}
1973
+ </SelectContent>
1974
+ </Select>
1975
+ </>
1920
1976
  )}
1921
1977
  <Select value={selectedModel} onValueChange={onModelChange}>
1922
1978
  <SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
@@ -97,7 +97,7 @@ function extractModels(logs: CapturedLog[]): string[] {
97
97
  * pass through. Returns the input array reference when no log matches the
98
98
  * filter, so referential equality is preserved for empty results.
99
99
  */
100
- function filterLogs(
100
+ export function filterLogs(
101
101
  logs: CapturedLog[],
102
102
  selectedSession: string,
103
103
  selectedModel: string,
@@ -57,6 +57,10 @@ export function hasPendingLogs(logs: readonly CapturedLog[]): boolean {
57
57
  return logs.some((log) => log.responseStatus === null);
58
58
  }
59
59
 
60
+ export function countPendingTurns(logs: readonly CapturedLog[]): number {
61
+ return logs.filter((log) => log.responseStatus === null).length;
62
+ }
63
+
60
64
  export function findConversationGroupIndexForLog(
61
65
  groups: readonly ConversationGroupData[],
62
66
  logId: number,
@@ -281,11 +285,21 @@ function SummaryChip({
281
285
  );
282
286
  }
283
287
 
284
- function WorkingBadge({ label = "Working" }: { label?: string }): JSX.Element {
288
+ function PendingTurnBadge({
289
+ count,
290
+ label = "Turn open",
291
+ }: {
292
+ count: number;
293
+ label?: string;
294
+ }): JSX.Element {
285
295
  return (
286
- <span className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-cyan-300/[0.08] px-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-cyan-100/85">
296
+ <span
297
+ className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-amber-300/[0.10] px-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-amber-100/90 ring-1 ring-amber-200/10"
298
+ title={`${count} turn${count !== 1 ? "s" : ""} not finished yet`}
299
+ >
287
300
  <Loader2 className="size-3 animate-spin" />
288
301
  {label}
302
+ {count > 1 && <span className="tabular-nums">x{count}</span>}
289
303
  </span>
290
304
  );
291
305
  }
@@ -296,7 +310,7 @@ function LoadingSweep(): JSX.Element {
296
310
  className="pointer-events-none absolute inset-x-3 bottom-0 h-px overflow-hidden rounded-full bg-white/[0.06]"
297
311
  aria-hidden="true"
298
312
  >
299
- <span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-cyan-200/55 shadow-[0_0_14px_rgba(103,232,249,0.35)]" />
313
+ <span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-amber-200/65 shadow-[0_0_14px_rgba(252,211,77,0.32)]" />
300
314
  </span>
301
315
  );
302
316
  }
@@ -370,7 +384,8 @@ function IdeGroupSection({
370
384
  const totals = tokenTotals(group.logs);
371
385
  const sessionCount = countSessions(group.processes);
372
386
  const logCount = group.logs.length;
373
- const isWorking = hasPendingLogs(group.logs);
387
+ const pendingTurnCount = countPendingTurns(group.logs);
388
+ const isWorking = pendingTurnCount > 0;
374
389
 
375
390
  return (
376
391
  <section
@@ -405,7 +420,7 @@ function IdeGroupSection({
405
420
  <span className="rounded bg-cyan-300/[0.06] px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-cyan-100/75">
406
421
  IDE
407
422
  </span>
408
- {isWorking && <WorkingBadge />}
423
+ {isWorking && <PendingTurnBadge count={pendingTurnCount} label="Turn open" />}
409
424
  </span>
410
425
  <span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
411
426
  <span>
@@ -463,7 +478,8 @@ function ProcessGroupSection({
463
478
  const projectLabel = shortProjectPath(group.clientProjectFolder);
464
479
  const sessionCount = group.sessions.length;
465
480
  const logCount = group.logs.length;
466
- const isWorking = hasPendingLogs(group.logs);
481
+ const pendingTurnCount = countPendingTurns(group.logs);
482
+ const isWorking = pendingTurnCount > 0;
467
483
  const processLabel =
468
484
  group.clientPid !== null && group.clientPid !== undefined
469
485
  ? `PID ${group.clientPid}`
@@ -488,7 +504,7 @@ function ProcessGroupSection({
488
504
  <span className="font-mono text-xs font-semibold tabular-nums text-slate-100">
489
505
  {processLabel}
490
506
  </span>
491
- {isWorking && <WorkingBadge label="Active" />}
507
+ {isWorking && <PendingTurnBadge count={pendingTurnCount} label="Turn open" />}
492
508
  {projectLabel !== null && (
493
509
  <span
494
510
  className="inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground"
@@ -49,6 +49,18 @@ function HeaderMetric({
49
49
  );
50
50
  }
51
51
 
52
+ function PendingTurnBadge(): JSX.Element {
53
+ return (
54
+ <span
55
+ className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-amber-300/[0.10] px-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-amber-100/90 ring-1 ring-amber-200/10"
56
+ title="This session has a turn that has not finished yet"
57
+ >
58
+ <Loader2 className="size-3 animate-spin" />
59
+ Turn open
60
+ </span>
61
+ );
62
+ }
63
+
52
64
  export type ConversationHeaderProps = {
53
65
  conversationId: string;
54
66
  startTime: string;
@@ -234,12 +246,7 @@ export function ConversationHeader({
234
246
  </span>
235
247
  )}
236
248
 
237
- {isLoading && (
238
- <span className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-cyan-300/[0.08] px-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-cyan-100/85">
239
- <Loader2 className="size-3 animate-spin" />
240
- Working
241
- </span>
242
- )}
249
+ {isLoading && <PendingTurnBadge />}
243
250
  </span>
244
251
 
245
252
  <span className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
@@ -307,7 +314,7 @@ export function ConversationHeader({
307
314
  className="pointer-events-none absolute inset-x-3 bottom-0 h-px overflow-hidden rounded-full bg-white/[0.06]"
308
315
  aria-hidden="true"
309
316
  >
310
- <span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-cyan-200/55 shadow-[0_0_14px_rgba(103,232,249,0.35)]" />
317
+ <span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-amber-200/65 shadow-[0_0_14px_rgba(252,211,77,0.32)]" />
311
318
  </span>
312
319
  )}
313
320
 
@@ -60,6 +60,8 @@ function resolveFocusedTab(tab: LogFocusTab, anatomySegments: AnatomySegment[] |
60
60
  return anatomySegments === null ? "request" : "anatomy";
61
61
  case "request":
62
62
  return "request";
63
+ case "response":
64
+ return "parsed";
63
65
  }
64
66
  }
65
67
 
@@ -429,6 +431,8 @@ export const LogEntry = memo(function ({
429
431
  const [headersDiff, setHeadersDiff] = useState<boolean>(false);
430
432
  const [requestDiff, setRequestDiff] = useState<boolean>(false);
431
433
  const [activeTab, setActiveTab] = useState("request");
434
+ const [focusedToolIndex, setFocusedToolIndex] = useState<number | null>(null);
435
+ const [toolFocusNonce, setToolFocusNonce] = useState(0);
432
436
  const [expandToPath, setExpandToPath] = useState<string | null>(null);
433
437
  const [hydratedLog, setHydratedLog] = useState<CapturedLog | null>(null);
434
438
  const [bodyHydrationStatus, setBodyHydrationStatus] = useState<BodyHydrationStatus>("idle");
@@ -779,6 +783,10 @@ export const LogEntry = memo(function ({
779
783
  if (request.logId !== log.id) return;
780
784
  setExpanded(true);
781
785
  setActiveTab(resolveFocusedTab(request.tab, anatomySegments));
786
+ if (request.tab === "response" && request.toolCallIndex !== undefined) {
787
+ setFocusedToolIndex(request.toolCallIndex);
788
+ setToolFocusNonce((current) => current + 1);
789
+ }
782
790
  };
783
791
 
784
792
  window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
@@ -1120,6 +1128,8 @@ export const LogEntry = memo(function ({
1120
1128
  cacheReadInputTokens={displayLog.cacheReadInputTokens}
1121
1129
  apiFormat={resolvedFormat}
1122
1130
  error={displayLog.error}
1131
+ focusedToolIndex={focusedToolIndex}
1132
+ toolFocusNonce={toolFocusNonce}
1123
1133
  />
1124
1134
  </Suspense>
1125
1135
  )}
@@ -19,6 +19,8 @@ export type ResponseViewProps = {
19
19
  apiFormat?: ApiFormat;
20
20
  /** SSE error message from streaming response */
21
21
  error?: string | null;
22
+ focusedToolIndex?: number | null;
23
+ toolFocusNonce?: number;
22
24
  };
23
25
 
24
26
  function getStatusClasses(category: StatusCategory): string {
@@ -74,6 +76,8 @@ export const ResponseView = memo(function ResponseView({
74
76
  cacheReadInputTokens,
75
77
  apiFormat,
76
78
  error,
79
+ focusedToolIndex = null,
80
+ toolFocusNonce = 0,
77
81
  }: ResponseViewProps): JSX.Element {
78
82
  const resolvedFormat = apiFormat ?? "unknown";
79
83
  const parsed = useMemo(
@@ -128,7 +132,7 @@ export const ResponseView = memo(function ResponseView({
128
132
  return (
129
133
  <div className="space-y-2">
130
134
  <StatusIndicator status={responseStatus} />
131
- {formatViewFor(resolvedFormat, parsed)}
135
+ {formatViewFor(resolvedFormat, parsed, { focusedToolIndex, toolFocusNonce })}
132
136
  </div>
133
137
  );
134
138
  }
@@ -1,52 +1,40 @@
1
1
  import { type JSX } from "react";
2
- import { Check, ChevronRight, Copy, Wrench } from "lucide-react";
2
+ import { Wrench } from "lucide-react";
3
3
  import type { ToolTraceEvent } from "./viewerState";
4
- import { useCopyFeedback } from "./useCopyFeedback";
4
+ import { dispatchLogFocusRequest } from "./logFocus";
5
5
 
6
6
  type ToolTraceEventsProps = {
7
7
  events: ToolTraceEvent[];
8
8
  };
9
9
 
10
- function ToolTraceEventRow({ event }: { event: ToolTraceEvent }): JSX.Element {
11
- const argumentCopy = useCopyFeedback(event.argumentsText);
12
- const canCopyArguments = event.argumentsText !== null;
13
-
10
+ function ToolTraceEventChip({ event }: { event: ToolTraceEvent }): JSX.Element {
14
11
  return (
15
- <div
12
+ <button
13
+ type="button"
16
14
  key={event.id}
17
- className="group/tool-trace flex min-w-0 items-center gap-2 rounded-md bg-white/[0.035] px-2.5 py-1.5 text-xs"
15
+ className="inline-flex min-w-0 items-center gap-1.5 rounded-md bg-white/[0.035] px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-sky-400/[0.08] hover:text-sky-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
16
+ onClick={() =>
17
+ dispatchLogFocusRequest({
18
+ logId: event.logId,
19
+ tab: "response",
20
+ toolCallIndex: event.index,
21
+ })
22
+ }
23
+ aria-label={`Open response tool call ${event.name}`}
24
+ title={`Open ${event.name} in Response`}
18
25
  >
19
26
  <Wrench className="size-3.5 shrink-0 text-sky-400/70" />
20
- <span className="font-mono font-semibold text-foreground/80">{event.name}</span>
21
- {event.argumentsPreview !== null && (
22
- <>
23
- <ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
24
- <span className="min-w-0 truncate font-mono text-muted-foreground">
25
- {event.argumentsPreview}
26
- </span>
27
- </>
28
- )}
29
- {canCopyArguments && (
30
- <button
31
- type="button"
32
- className="ml-auto inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-background/80 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/tool-trace:opacity-100 group-focus-within/tool-trace:opacity-100"
33
- onClick={argumentCopy.copy}
34
- aria-label={argumentCopy.copied ? "Copied tool arguments" : "Copy tool arguments"}
35
- title={argumentCopy.copied ? "Copied tool arguments" : "Copy tool arguments"}
36
- >
37
- {argumentCopy.copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
38
- </button>
39
- )}
40
- </div>
27
+ <span className="min-w-0 truncate font-mono font-semibold">{event.name}</span>
28
+ </button>
41
29
  );
42
30
  }
43
31
 
44
32
  export function ToolTraceEvents({ events }: ToolTraceEventsProps): JSX.Element | null {
45
33
  if (events.length === 0) return null;
46
34
  return (
47
- <div className="mx-3 mb-2 grid gap-1.5">
35
+ <div className="mx-3 mb-2 flex flex-wrap gap-1.5">
48
36
  {events.map((event) => (
49
- <ToolTraceEventRow key={event.id} event={event} />
37
+ <ToolTraceEventChip key={event.id} event={event} />
50
38
  ))}
51
39
  </div>
52
40
  );
@@ -14,6 +14,7 @@ import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
14
14
  import { cn, formatTokens } from "../../lib/utils";
15
15
  import type { CapturedLog } from "../../contracts";
16
16
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
17
+ import { getCrabVariant } from "../ui/crab-variants";
17
18
  import type { CacheTrendEntry } from "./cacheTrend";
18
19
  import { LogEntry } from "./LogEntry";
19
20
  import {
@@ -177,6 +178,7 @@ export const TurnGroup = memo(function TurnGroup({
177
178
 
178
179
  const firstLogId = entries[0]?.log.id ?? turnIndex;
179
180
  const turnLabel = `Turn ${String(turnIndex + 1)}`;
181
+ const CollapsedCrab = useMemo(() => getCrabVariant(firstLogId % 12), [firstLogId]);
180
182
  const containmentStyle = useMemo<CSSProperties>(
181
183
  () => ({
182
184
  contentVisibility: "auto",
@@ -224,16 +226,28 @@ export const TurnGroup = memo(function TurnGroup({
224
226
 
225
227
  <div className="w-6 shrink-0 flex flex-col items-center pt-1.5 pb-1.5">
226
228
  <div className="flex justify-center h-[calc(0.75rem-8px)]" />
227
- <span className="size-2 rounded-full bg-cyan-300/45" />
228
- <div className="flex-1 flex justify-center min-h-0">
229
- <div className="w-px bg-muted-foreground/14 h-full" />
230
- </div>
231
- <span
229
+ <CollapsedCrab
232
230
  className={cn(
233
- "size-2 rounded-full",
234
- isComplete ? "bg-amber-300/45" : "bg-muted-foreground/25",
231
+ "size-3.5",
232
+ isPending
233
+ ? "animate-crab-crawl text-amber-300/80"
234
+ : "animate-crab-appear text-emerald-400",
235
+ isPending
236
+ ? "drop-shadow-[0_0_4px_rgba(252,211,77,0.42)]"
237
+ : "drop-shadow-[0_0_4px_rgba(52,211,153,0.45)]",
235
238
  )}
236
239
  />
240
+ <div className="flex-1 flex justify-center min-h-0">
241
+ <div className="w-px bg-muted-foreground/14 h-full" />
242
+ </div>
243
+ {isComplete && (
244
+ <CollapsedCrab
245
+ className={cn(
246
+ "size-3.5 animate-crab-settle text-amber-400",
247
+ "drop-shadow-[0_0_4px_rgba(251,191,36,0.45)]",
248
+ )}
249
+ />
250
+ )}
237
251
  </div>
238
252
 
239
253
  {/* Summary content - hidden for single-log turns. */}