@tonyclaw/agent-inspector 2.1.15 → 2.1.16

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 (65) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-Ccrrcx1j.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-C7axhr-l.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-WOptg8j_.js} +1 -1
  6. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-CSAcxh_M.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-DJS5KhPx.js} +1 -1
  8. package/.output/public/assets/_sessionId-DUSalzKH.js +1 -0
  9. package/.output/public/assets/index-BPpA21dY.css +1 -0
  10. package/.output/public/assets/index-Bt0Az2I2.js +1 -0
  11. package/.output/public/assets/{index-DjKt8XKe.js → index-DMPNh46t.js} +1 -1
  12. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-inX5QSa3.js} +1 -1
  13. package/.output/public/assets/{main-DpD1N0S8.js → main-aCAKYGSD.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +207 -171
  15. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-CO-Ti5sg.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +807 -386
  18. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-J1VxC0In.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
  22. package/.output/server/_ssr/{index-47XVPghS.mjs → index-CStymIc0.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
  25. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-CQ4Zxhmr.mjs} +112 -46
  26. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
  27. package/.output/server/index.mjs +72 -72
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +146 -105
  30. package/src/components/ProxyViewerContainer.tsx +77 -67
  31. package/src/components/alerts/AlertsDialog.tsx +2 -1
  32. package/src/components/clients/ClientLogo.tsx +1 -1
  33. package/src/components/groups/GroupsDialog.tsx +19 -1
  34. package/src/components/providers/ProviderCard.tsx +1 -1
  35. package/src/components/providers/ProviderForm.tsx +18 -18
  36. package/src/components/providers/ProviderLogo.tsx +6 -1
  37. package/src/components/providers/ProvidersPanel.tsx +6 -6
  38. package/src/components/providers/SettingsDialog.tsx +13 -6
  39. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  40. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  41. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  42. package/src/components/proxy-viewer/ConversationHeader.tsx +44 -32
  43. package/src/components/proxy-viewer/LogEntry.tsx +21 -17
  44. package/src/components/proxy-viewer/LogEntryHeader.tsx +38 -45
  45. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  46. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  47. package/src/components/proxy-viewer/ThreadConnector.tsx +4 -4
  48. package/src/components/proxy-viewer/TurnGroup.tsx +16 -11
  49. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  50. package/src/components/proxy-viewer/viewerState.ts +0 -4
  51. package/src/components/ui/icon-trigger.ts +2 -0
  52. package/src/components/ui/transient-toast.tsx +104 -0
  53. package/src/lib/sessionInfoContract.ts +4 -0
  54. package/src/mcp/server.ts +71 -27
  55. package/src/mcp/toolHandlers.ts +23 -4
  56. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  57. package/src/proxy/sessionInfo.ts +31 -2
  58. package/src/proxy/sessionSupervisor.ts +20 -1
  59. package/src/proxy/store.ts +1 -1
  60. package/src/routes/api/providers.$providerId.test.log.ts +8 -4
  61. package/styles/globals.css +180 -180
  62. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  63. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  64. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  65. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -1,20 +1,52 @@
1
- import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
2
- import { useWindowVirtualizer } from "@tanstack/react-virtual";
1
+ import { type JSX, useCallback, useMemo, useState } from "react";
2
+ import { ChevronDown, ChevronRight, Cpu, FolderOpen, Monitor } from "lucide-react";
3
3
  import type { CapturedLog } from "../../contracts";
4
+ import { cn, formatTokens } from "../../lib/utils";
5
+ import { ClientLogo, clientAppLabel, detectClientApp, type ClientApp } from "../clients/ClientLogo";
4
6
  import { ConversationGroup, type ConversationGroupProps } from "./ConversationGroup";
5
7
  import type { ConversationGroupData } from "./ConversationHeader";
6
- import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
7
8
 
8
9
  const VIRTUALIZE_GROUP_THRESHOLD = 30;
9
- const ESTIMATED_GROUP_HEIGHT = 112;
10
- const VIRTUAL_OVERSCAN = 6;
11
10
 
12
11
  type ConversationGroupListProps = Omit<
13
12
  ConversationGroupProps,
14
- "group" | "standalone" | "expanded" | "onExpandedChange"
13
+ "group" | "expanded" | "onExpandedChange"
15
14
  > & {
16
15
  groups: ConversationGroupData[];
17
- standalone: boolean;
16
+ };
17
+
18
+ export type ProcessGroupData = {
19
+ id: string;
20
+ clientPid: number | null;
21
+ clientProjectFolder: string | null;
22
+ sessions: ConversationGroupData[];
23
+ logs: CapturedLog[];
24
+ };
25
+
26
+ export type IdeGroupData = {
27
+ id: string;
28
+ client: ClientApp;
29
+ label: string;
30
+ userAgent: string | null;
31
+ processes: ProcessGroupData[];
32
+ logs: CapturedLog[];
33
+ };
34
+
35
+ type MutableProcessGroup = {
36
+ id: string;
37
+ clientPid: number | null;
38
+ clientProjectFolder: string | null;
39
+ sessions: ConversationGroupData[];
40
+ logs: CapturedLog[];
41
+ };
42
+
43
+ type MutableIdeGroup = {
44
+ id: string;
45
+ client: ClientApp;
46
+ label: string;
47
+ userAgent: string | null;
48
+ processes: Map<string, MutableProcessGroup>;
49
+ logs: CapturedLog[];
18
50
  };
19
51
 
20
52
  export function shouldVirtualizeConversationGroups(groupCount: number): boolean {
@@ -34,111 +66,442 @@ export function findConversationGroupIndexForLog(
34
66
  return null;
35
67
  }
36
68
 
37
- export function ConversationGroupList({
38
- groups,
39
- standalone,
40
- ...groupProps
41
- }: ConversationGroupListProps): JSX.Element {
42
- if (!shouldVirtualizeConversationGroups(groups.length)) {
43
- return (
44
- <>
45
- {groups.map((group) => (
46
- <ConversationGroup key={group.id} group={group} standalone={standalone} {...groupProps} />
47
- ))}
48
- </>
69
+ function firstUserAgent(logs: readonly CapturedLog[]): string | null {
70
+ for (const log of logs) {
71
+ const userAgent = log.userAgent;
72
+ if (userAgent !== null && userAgent !== undefined && userAgent !== "") return userAgent;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function firstClientPid(logs: readonly CapturedLog[]): number | null {
78
+ for (const log of logs) {
79
+ const clientPid = log.clientPid;
80
+ if (clientPid !== null && clientPid !== undefined) return clientPid;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function firstClientProjectFolder(logs: readonly CapturedLog[]): string | null {
86
+ for (const log of logs) {
87
+ const projectFolder = log.clientProjectFolder;
88
+ if (projectFolder !== null && projectFolder !== undefined && projectFolder !== "") {
89
+ return projectFolder;
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function groupClient(group: ConversationGroupData): ClientApp {
96
+ return detectClientApp({
97
+ userAgent: firstUserAgent(group.logs),
98
+ conversationId: group.conversationId,
99
+ clientProjectFolder: firstClientProjectFolder(group.logs),
100
+ });
101
+ }
102
+
103
+ function fallbackClientLabel(userAgent: string | null): string {
104
+ if (userAgent === null || userAgent === "") return "Unknown IDE";
105
+ const firstToken = userAgent.split(/[ /;]/)[0] ?? "";
106
+ if (firstToken === "") return "Unknown IDE";
107
+ return firstToken;
108
+ }
109
+
110
+ function clientLabel(client: ClientApp, userAgent: string | null): string {
111
+ const knownLabel = clientAppLabel(client);
112
+ if (knownLabel !== null) return knownLabel;
113
+ return fallbackClientLabel(userAgent);
114
+ }
115
+
116
+ function bucketKey(value: string | number | null): string {
117
+ if (value === null || value === "") return "unknown";
118
+ return String(value);
119
+ }
120
+
121
+ function ideKey(client: ClientApp, userAgent: string | null, projectFolder: string | null): string {
122
+ if (client !== "unknown") return client;
123
+ return `unknown:${bucketKey(userAgent)}:${bucketKey(projectFolder)}`;
124
+ }
125
+
126
+ function processKey(clientPid: number | null, projectFolder: string | null): string {
127
+ return `${bucketKey(clientPid)}:${bucketKey(projectFolder)}`;
128
+ }
129
+
130
+ function sortLogsByTimestamp(logs: readonly CapturedLog[]): CapturedLog[] {
131
+ return [...logs].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
132
+ }
133
+
134
+ function firstTimestamp(logs: readonly CapturedLog[]): string {
135
+ return sortLogsByTimestamp(logs)[0]?.timestamp ?? "";
136
+ }
137
+
138
+ function sortByFirstLogTime<T extends { logs: readonly CapturedLog[] }>(items: readonly T[]): T[] {
139
+ return [...items].sort((a, b) => firstTimestamp(a.logs).localeCompare(firstTimestamp(b.logs)));
140
+ }
141
+
142
+ export function buildConversationHierarchy(
143
+ groups: readonly ConversationGroupData[],
144
+ ): IdeGroupData[] {
145
+ const ideGroups = new Map<string, MutableIdeGroup>();
146
+
147
+ for (const group of groups) {
148
+ const userAgent = firstUserAgent(group.logs);
149
+ const projectFolder = firstClientProjectFolder(group.logs);
150
+ const client = groupClient(group);
151
+ const currentIdeKey = ideKey(client, userAgent, projectFolder);
152
+ let ideGroup = ideGroups.get(currentIdeKey);
153
+
154
+ if (ideGroup === undefined) {
155
+ ideGroup = {
156
+ id: currentIdeKey,
157
+ client,
158
+ label: clientLabel(client, userAgent),
159
+ userAgent,
160
+ processes: new Map(),
161
+ logs: [],
162
+ };
163
+ ideGroups.set(currentIdeKey, ideGroup);
164
+ }
165
+
166
+ ideGroup.logs.push(...group.logs);
167
+
168
+ const clientPid = firstClientPid(group.logs);
169
+ const currentProcessKey = `${currentIdeKey}:${processKey(clientPid, projectFolder)}`;
170
+ let processGroup = ideGroup.processes.get(currentProcessKey);
171
+
172
+ if (processGroup === undefined) {
173
+ processGroup = {
174
+ id: currentProcessKey,
175
+ clientPid,
176
+ clientProjectFolder: projectFolder,
177
+ sessions: [],
178
+ logs: [],
179
+ };
180
+ ideGroup.processes.set(currentProcessKey, processGroup);
181
+ }
182
+
183
+ processGroup.sessions.push(group);
184
+ processGroup.logs.push(...group.logs);
185
+ }
186
+
187
+ const hierarchy: IdeGroupData[] = [];
188
+
189
+ for (const ideGroup of ideGroups.values()) {
190
+ const processes: ProcessGroupData[] = sortByFirstLogTime(
191
+ [...ideGroup.processes.values()].map((processGroup) => ({
192
+ id: processGroup.id,
193
+ clientPid: processGroup.clientPid,
194
+ clientProjectFolder: processGroup.clientProjectFolder,
195
+ sessions: sortByFirstLogTime(processGroup.sessions),
196
+ logs: sortLogsByTimestamp(processGroup.logs),
197
+ })),
49
198
  );
199
+
200
+ hierarchy.push({
201
+ id: ideGroup.id,
202
+ client: ideGroup.client,
203
+ label: ideGroup.label,
204
+ userAgent: ideGroup.userAgent,
205
+ processes,
206
+ logs: sortLogsByTimestamp(ideGroup.logs),
207
+ });
208
+ }
209
+
210
+ return sortByFirstLogTime(hierarchy);
211
+ }
212
+
213
+ function formatTokenCount(count: number): string {
214
+ if (count <= 0) return "0";
215
+ return formatTokens(count);
216
+ }
217
+
218
+ function tokenTotals(logs: readonly CapturedLog[]): {
219
+ inputTokens: number;
220
+ outputTokens: number;
221
+ } {
222
+ let inputTokens = 0;
223
+ let outputTokens = 0;
224
+
225
+ for (const log of logs) {
226
+ if (log.inputTokens !== null && log.inputTokens !== undefined) inputTokens += log.inputTokens;
227
+ if (log.outputTokens !== null && log.outputTokens !== undefined) {
228
+ outputTokens += log.outputTokens;
229
+ }
50
230
  }
51
231
 
232
+ return { inputTokens, outputTokens };
233
+ }
234
+
235
+ function countSessions(processes: readonly ProcessGroupData[]): number {
236
+ let sessions = 0;
237
+ for (const processGroup of processes) {
238
+ sessions += processGroup.sessions.length;
239
+ }
240
+ return sessions;
241
+ }
242
+
243
+ function shortProjectPath(projectFolder: string | null): string | null {
244
+ if (projectFolder === null || projectFolder === "") return null;
245
+ const parts = projectFolder
246
+ .replaceAll("\\", "/")
247
+ .split("/")
248
+ .filter((part) => part !== "");
249
+ const leaf = parts[parts.length - 1];
250
+ if (leaf === undefined) return projectFolder;
251
+ const parent = parts[parts.length - 2];
252
+ if (parent === undefined) return leaf;
253
+ return `${parent}/${leaf}`;
254
+ }
255
+
256
+ function SummaryChip({
257
+ label,
258
+ value,
259
+ className,
260
+ }: {
261
+ label: string;
262
+ value: string;
263
+ className?: string;
264
+ }): JSX.Element {
52
265
  return (
53
- <VirtualizedConversationGroupList groups={groups} standalone={standalone} {...groupProps} />
266
+ <span
267
+ className={cn(
268
+ "border border-border bg-muted/40 inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs text-muted-foreground",
269
+ className,
270
+ )}
271
+ >
272
+ <span className="font-mono text-[10px] uppercase tracking-[0.12em] text-white/35">
273
+ {label}
274
+ </span>
275
+ <span className="font-mono tabular-nums text-slate-100">{value}</span>
276
+ </span>
54
277
  );
55
278
  }
56
279
 
57
- function VirtualizedConversationGroupList({
280
+ export function ConversationGroupList({
58
281
  groups,
59
- standalone,
60
282
  ...groupProps
61
283
  }: ConversationGroupListProps): JSX.Element {
62
- const [expandedByGroupId, setExpandedByGroupId] = useState<ReadonlyMap<string, boolean>>(
63
- () => new Map(),
284
+ const hierarchy = useMemo(() => buildConversationHierarchy(groups), [groups]);
285
+ const [collapsedIdeIds, setCollapsedIdeIds] = useState<ReadonlySet<string>>(() => new Set());
286
+ const [collapsedProcessIds, setCollapsedProcessIds] = useState<ReadonlySet<string>>(
287
+ () => new Set(),
64
288
  );
65
289
 
66
- const virtualizer = useWindowVirtualizer({
67
- count: groups.length,
68
- estimateSize: () => ESTIMATED_GROUP_HEIGHT,
69
- overscan: VIRTUAL_OVERSCAN,
70
- getItemKey: (index) => groups[index]?.id ?? index,
71
- });
72
-
73
- const setGroupExpanded = useCallback((groupId: string, expanded: boolean) => {
74
- setExpandedByGroupId((previous) => {
75
- const next = new Map(previous);
76
- if (expanded) {
77
- next.set(groupId, true);
290
+ const toggleIde = useCallback((id: string) => {
291
+ setCollapsedIdeIds((previous) => {
292
+ const next = new Set(previous);
293
+ if (next.has(id)) {
294
+ next.delete(id);
78
295
  } else {
79
- next.delete(groupId);
296
+ next.add(id);
80
297
  }
81
298
  return next;
82
299
  });
83
300
  }, []);
84
301
 
85
- const indexByLogId = useMemo(() => {
86
- const result = new Map<number, number>();
87
- for (let index = 0; index < groups.length; index++) {
88
- const group = groups[index];
89
- if (group === undefined) continue;
90
- for (const log of group.logs) {
91
- result.set(log.id, index);
302
+ const toggleProcess = useCallback((id: string) => {
303
+ setCollapsedProcessIds((previous) => {
304
+ const next = new Set(previous);
305
+ if (next.has(id)) {
306
+ next.delete(id);
307
+ } else {
308
+ next.add(id);
92
309
  }
93
- }
94
- return result;
95
- }, [groups]);
96
-
97
- useEffect(() => {
98
- const handleLogFocusRequest = (event: Event): void => {
99
- const request = readLogFocusRequest(event);
100
- if (request === null) return;
101
- const index = indexByLogId.get(request.logId);
102
- if (index === undefined) return;
103
- const group = groups[index];
104
- if (group === undefined) return;
105
- setGroupExpanded(group.id, true);
106
- virtualizer.scrollToIndex(index, { align: "center" });
107
- };
108
-
109
- window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
110
- return () => {
111
- window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
112
- };
113
- }, [groups, indexByLogId, setGroupExpanded, virtualizer]);
310
+ return next;
311
+ });
312
+ }, []);
114
313
 
115
314
  return (
116
- <div
117
- className="relative w-full"
118
- style={{ height: `${virtualizer.getTotalSize()}px` }}
119
- data-virtualized-conversation-list="true"
315
+ <div className="space-y-4">
316
+ {hierarchy.map((ideGroup) => (
317
+ <IdeGroupSection
318
+ key={ideGroup.id}
319
+ group={ideGroup}
320
+ collapsed={collapsedIdeIds.has(ideGroup.id)}
321
+ collapsedProcessIds={collapsedProcessIds}
322
+ onToggleIde={() => toggleIde(ideGroup.id)}
323
+ onToggleProcess={toggleProcess}
324
+ conversationProps={groupProps}
325
+ />
326
+ ))}
327
+ </div>
328
+ );
329
+ }
330
+
331
+ function IdeGroupSection({
332
+ group,
333
+ collapsed,
334
+ collapsedProcessIds,
335
+ onToggleIde,
336
+ onToggleProcess,
337
+ conversationProps,
338
+ }: {
339
+ group: IdeGroupData;
340
+ collapsed: boolean;
341
+ collapsedProcessIds: ReadonlySet<string>;
342
+ onToggleIde: () => void;
343
+ onToggleProcess: (id: string) => void;
344
+ conversationProps: Omit<ConversationGroupProps, "group" | "expanded" | "onExpandedChange">;
345
+ }): JSX.Element {
346
+ const totals = tokenTotals(group.logs);
347
+ const sessionCount = countSessions(group.processes);
348
+ const logCount = group.logs.length;
349
+
350
+ return (
351
+ <section
352
+ className={cn("border border-border bg-card shadow-sm relative isolate rounded-[8px]")}
353
+ data-ide-group={group.id}
120
354
  >
121
- {virtualizer.getVirtualItems().map((virtualItem) => {
122
- const group = groups[virtualItem.index];
123
- if (group === undefined) return null;
124
- return (
125
- <div
126
- key={virtualItem.key}
127
- data-index={virtualItem.index}
128
- ref={virtualizer.measureElement}
129
- className="absolute left-0 top-0 w-full"
130
- style={{ transform: `translateY(${virtualItem.start}px)` }}
131
- >
132
- <ConversationGroup
133
- group={group}
134
- standalone={standalone}
135
- expanded={expandedByGroupId.get(group.id) ?? false}
136
- onExpandedChange={(expanded) => setGroupExpanded(group.id, expanded)}
137
- {...groupProps}
355
+ <button
356
+ type="button"
357
+ className="flex w-full min-w-0 items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-white/[0.03] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300/45"
358
+ onClick={onToggleIde}
359
+ aria-expanded={!collapsed}
360
+ >
361
+ {group.client === "unknown" ? (
362
+ <span className="border border-border bg-muted/40 inline-flex size-10 shrink-0 items-center justify-center rounded-[8px] text-slate-200">
363
+ <Monitor className="size-5" />
364
+ </span>
365
+ ) : (
366
+ <ClientLogo client={group.client} className="size-10 rounded-[8px] text-cyan-100" />
367
+ )}
368
+
369
+ <span className="min-w-0 flex-1">
370
+ <span className="flex min-w-0 items-center gap-2">
371
+ <span
372
+ className="truncate text-sm font-semibold text-slate-50"
373
+ title={group.userAgent ?? group.label}
374
+ >
375
+ {group.label}
376
+ </span>
377
+ <span className="rounded border border-cyan-200/15 bg-cyan-300/[0.06] px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-cyan-100/75">
378
+ IDE
379
+ </span>
380
+ </span>
381
+ <span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
382
+ <span>
383
+ {group.processes.length} process{group.processes.length !== 1 ? "es" : ""}
384
+ </span>
385
+ <span>
386
+ {sessionCount} session{sessionCount !== 1 ? "s" : ""}
387
+ </span>
388
+ <span>
389
+ {logCount} log{logCount !== 1 ? "s" : ""}
390
+ </span>
391
+ </span>
392
+ </span>
393
+
394
+ <span className="hidden shrink-0 items-center gap-2 md:flex">
395
+ <SummaryChip label="IN" value={formatTokenCount(totals.inputTokens)} />
396
+ <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
397
+ </span>
398
+
399
+ <span className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md text-slate-200">
400
+ {collapsed ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
401
+ </span>
402
+ </button>
403
+
404
+ {!collapsed && (
405
+ <div className="space-y-3 border-t border-white/10 px-3 pb-3 pt-3 sm:px-4">
406
+ {group.processes.map((processGroup) => (
407
+ <ProcessGroupSection
408
+ key={processGroup.id}
409
+ group={processGroup}
410
+ collapsed={collapsedProcessIds.has(processGroup.id)}
411
+ onToggle={() => onToggleProcess(processGroup.id)}
412
+ conversationProps={conversationProps}
138
413
  />
414
+ ))}
415
+ </div>
416
+ )}
417
+ </section>
418
+ );
419
+ }
420
+
421
+ function ProcessGroupSection({
422
+ group,
423
+ collapsed,
424
+ onToggle,
425
+ conversationProps,
426
+ }: {
427
+ group: ProcessGroupData;
428
+ collapsed: boolean;
429
+ onToggle: () => void;
430
+ conversationProps: Omit<ConversationGroupProps, "group" | "expanded" | "onExpandedChange">;
431
+ }): JSX.Element {
432
+ const totals = tokenTotals(group.logs);
433
+ const projectLabel = shortProjectPath(group.clientProjectFolder);
434
+ const sessionCount = group.sessions.length;
435
+ const logCount = group.logs.length;
436
+ const processLabel =
437
+ group.clientPid !== null && group.clientPid !== undefined
438
+ ? `PID ${group.clientPid}`
439
+ : "process";
440
+
441
+ return (
442
+ <section className="relative pl-5">
443
+ <span className="absolute bottom-0 left-2 top-0 w-px bg-white/12" aria-hidden="true" />
444
+ <div className="border border-border bg-card shadow-sm overflow-hidden rounded-[8px]">
445
+ <button
446
+ type="button"
447
+ className="flex w-full min-w-0 items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-white/[0.035] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300/40"
448
+ onClick={onToggle}
449
+ aria-expanded={!collapsed}
450
+ >
451
+ <span className="border border-border bg-muted/40 inline-flex size-8 shrink-0 items-center justify-center rounded-md text-amber-100">
452
+ <Cpu className="size-4" />
453
+ </span>
454
+
455
+ <span className="min-w-0 flex-1">
456
+ <span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
457
+ <span className="font-mono text-xs font-semibold tabular-nums text-slate-100">
458
+ {processLabel}
459
+ </span>
460
+ {projectLabel !== null && (
461
+ <span
462
+ className="inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground"
463
+ title={group.clientProjectFolder ?? projectLabel}
464
+ >
465
+ <FolderOpen className="size-3 shrink-0" />
466
+ <span className="truncate font-mono">{projectLabel}</span>
467
+ </span>
468
+ )}
469
+ </span>
470
+ <span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
471
+ <span>
472
+ {sessionCount} session{sessionCount !== 1 ? "s" : ""}
473
+ </span>
474
+ <span>
475
+ {logCount} log{logCount !== 1 ? "s" : ""}
476
+ </span>
477
+ </span>
478
+ </span>
479
+
480
+ <span className="hidden shrink-0 items-center gap-2 md:flex">
481
+ <SummaryChip label="IN" value={formatTokenCount(totals.inputTokens)} />
482
+ <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
483
+ </span>
484
+
485
+ <span className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-7 shrink-0 items-center justify-center rounded-md text-slate-200">
486
+ {collapsed ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
487
+ </span>
488
+ </button>
489
+
490
+ {!collapsed && (
491
+ <div className="space-y-2 border-t border-white/10 p-2 sm:p-3">
492
+ {group.sessions.map((session) => (
493
+ <ConversationGroup
494
+ key={session.id}
495
+ group={session}
496
+ {...conversationProps}
497
+ showClientIdentity={false}
498
+ showLogClientMetadata={false}
499
+ showProcessMetadata={false}
500
+ />
501
+ ))}
139
502
  </div>
140
- );
141
- })}
142
- </div>
503
+ )}
504
+ </div>
505
+ </section>
143
506
  );
144
507
  }