@tonyclaw/agent-inspector 3.0.26 → 3.0.28

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 (86) hide show
  1. package/.output/cli.js +80 -11
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-SZRUDQc9.js → CompareDrawer-TghZr5t1.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-BJvwOrSi.js → ProxyViewerContainer-fShBAZjj.js} +201 -201
  5. package/.output/public/assets/{ReplayDialog-DgfmqiM3.js → ReplayDialog-CZMApaF4.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-CS6c7Qfn.js → RequestAnatomy-DNWccR7O.js} +1 -1
  7. package/.output/public/assets/{ResponseView-C0Ri_5gm.js → ResponseView-d25MdFhY.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-7dYcHWyZ.js → StreamingChunkSequence-DIFZEnWe.js} +1 -1
  9. package/.output/public/assets/{_sessionId-D-V84cgw.js → _sessionId-DRfvuE2k.js} +1 -1
  10. package/.output/public/assets/{index-DgFWCxOs.js → index-B3aAuqWq.js} +1 -1
  11. package/.output/public/assets/index-BsxAGqMA.css +1 -0
  12. package/.output/public/assets/{index-DrVJsks2.js → index-D4HkS7Ct.js} +1 -1
  13. package/.output/public/assets/{index-CSIL7-MK.js → index-DzARL2Fi.js} +3 -3
  14. package/.output/public/assets/{json-viewer-C9Ji3gYN.js → json-viewer-wuJSKaed.js} +1 -1
  15. package/.output/server/_libs/lucide-react.mjs +39 -39
  16. package/.output/server/{_sessionId-BQ6YQOO1.mjs → _sessionId-BJTNiP_O.mjs} +4 -2
  17. package/.output/server/_ssr/{CompareDrawer-CqNzIeMR.mjs → CompareDrawer-BMRH3N97.mjs} +5 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CMC7Vsbw.mjs → ProxyViewerContainer-AFwcHAIR.mjs} +330 -223
  19. package/.output/server/_ssr/{ReplayDialog-D9p_LaTr.mjs → ReplayDialog-CbWvRHiN.mjs} +6 -4
  20. package/.output/server/_ssr/{RequestAnatomy-B4ge3qNZ.mjs → RequestAnatomy-C4kcPyqN.mjs} +4 -2
  21. package/.output/server/_ssr/{ResponseView-BCqMKApI.mjs → ResponseView-D8VYt-gZ.mjs} +5 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-BA4zTLE4.mjs → StreamingChunkSequence-BUUo6vU0.mjs} +4 -2
  23. package/.output/server/_ssr/{index-CzxhDjlq.mjs → index-DTwMEEXO.mjs} +4 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-B3qV6iEJ.mjs → json-viewer-BqF1pSQD.mjs} +5 -3
  26. package/.output/server/_ssr/{router-CZe_R31M.mjs → router-C8BOns7y.mjs} +839 -368
  27. package/.output/server/{_tanstack-start-manifest_v-B50VTLrH.mjs → _tanstack-start-manifest_v-BBf2nRhb.mjs} +1 -1
  28. package/.output/server/index.mjs +70 -70
  29. package/README.md +8 -0
  30. package/package.json +4 -5
  31. package/src/cli.ts +43 -9
  32. package/src/components/ProxyViewer.tsx +57 -1
  33. package/src/components/clients/ClientLogo.tsx +43 -4
  34. package/src/components/inspector-pet/InspectorPet.tsx +0 -7
  35. package/src/components/providers/ProviderCard.tsx +3 -36
  36. package/src/components/providers/ProviderForm.tsx +13 -12
  37. package/src/components/providers/ProvidersPanel.tsx +2 -2
  38. package/src/components/providers/SettingsDialog.tsx +3 -3
  39. package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
  40. package/src/components/proxy-viewer/ConversationGroupList.tsx +233 -152
  41. package/src/components/proxy-viewer/ConversationHeader.tsx +29 -13
  42. package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -3
  43. package/src/contracts/anthropic.ts +0 -5
  44. package/src/contracts/index.ts +0 -4
  45. package/src/contracts/log.ts +0 -1
  46. package/src/contracts/openai.ts +0 -3
  47. package/src/knowledge/openclawClient.ts +22 -9
  48. package/src/knowledge/openclawGatewayClient.ts +20 -13
  49. package/src/lib/alertContract.ts +0 -3
  50. package/src/lib/ecosystemContract.ts +0 -3
  51. package/src/lib/export-logs.ts +1 -4
  52. package/src/lib/jsonFile.ts +46 -0
  53. package/src/lib/safeFetch.ts +100 -0
  54. package/src/lib/ssrfGuard.ts +133 -0
  55. package/src/lib/stopReason.ts +1 -4
  56. package/src/lib/unknown.ts +27 -0
  57. package/src/lib/useProviders.ts +1 -1
  58. package/src/mcp/mode.ts +3 -9
  59. package/src/mcp/server.ts +5 -14
  60. package/src/mcp/toolHandlers.ts +45 -54
  61. package/src/proxy/constants.ts +17 -0
  62. package/src/proxy/evidenceAnalysis.ts +116 -117
  63. package/src/proxy/formats/anthropic/handler.ts +5 -1
  64. package/src/proxy/formats/anthropic/schemas.ts +1 -7
  65. package/src/proxy/formats/index.ts +1 -1
  66. package/src/proxy/formats/openai/schemas.ts +0 -3
  67. package/src/proxy/formats/registry.ts +0 -3
  68. package/src/proxy/groupEvidenceExporter.ts +4 -8
  69. package/src/proxy/groupStore.ts +3 -10
  70. package/src/proxy/handler.ts +91 -12
  71. package/src/proxy/identityProxy.ts +74 -1
  72. package/src/proxy/logImporter.ts +82 -12
  73. package/src/proxy/logIndex.ts +1 -4
  74. package/src/proxy/providerSecretStore.ts +177 -0
  75. package/src/proxy/providers.ts +84 -11
  76. package/src/proxy/runFailures.ts +4 -10
  77. package/src/proxy/runStore.ts +4 -11
  78. package/src/proxy/schemas.ts +1 -8
  79. package/src/proxy/sqliteLogIndex.ts +0 -16
  80. package/src/proxy/store.ts +1 -5
  81. package/src/proxy/toolSchemaWarnings.ts +1 -9
  82. package/src/routes/api/logs.$id.replay.ts +17 -5
  83. package/src/routes/api/providers.$providerId.model-metadata.ts +11 -1
  84. package/src/routes/api/providers.$providerId.ts +18 -5
  85. package/src/routes/api/providers.ts +18 -7
  86. package/.output/public/assets/index-CmeF5XXS.css +0 -1
@@ -1,12 +1,20 @@
1
- import { type JSX, useCallback, useMemo, useState } from "react";
1
+ import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { ChevronDown, ChevronRight, Cpu, FolderOpen, Loader2, Monitor } from "lucide-react";
3
3
  import type { CapturedLog } from "../../contracts";
4
4
  import { cn, formatTokens } from "../../lib/utils";
5
- import { ClientLogo, clientAppLabel, detectClientApp, type ClientApp } from "../clients/ClientLogo";
5
+ import {
6
+ ClientLogo,
7
+ clientAppLabel,
8
+ clientAppTypeLabel,
9
+ detectClientApp,
10
+ type ClientApp,
11
+ } from "../clients/ClientLogo";
6
12
  import { ConversationGroup, type ConversationGroupProps } from "./ConversationGroup";
7
13
  import type { ConversationGroupData } from "./ConversationHeader";
8
14
 
9
15
  const VIRTUALIZE_GROUP_THRESHOLD = 30;
16
+ const COLLAPSED_CLIENTS_STORAGE_KEY = "agent-inspector.hierarchy.collapsedClients";
17
+ const COLLAPSED_PROCESSES_STORAGE_KEY = "agent-inspector.hierarchy.collapsedProcesses";
10
18
 
11
19
  type ConversationGroupListProps = Omit<
12
20
  ConversationGroupProps,
@@ -23,7 +31,7 @@ export type ProcessGroupData = {
23
31
  logs: CapturedLog[];
24
32
  };
25
33
 
26
- export type IdeGroupData = {
34
+ export type ClientGroupData = {
27
35
  id: string;
28
36
  client: ClientApp;
29
37
  label: string;
@@ -40,7 +48,7 @@ type MutableProcessGroup = {
40
48
  logs: CapturedLog[];
41
49
  };
42
50
 
43
- type MutableIdeGroup = {
51
+ type MutableClientGroup = {
44
52
  id: string;
45
53
  client: ClientApp;
46
54
  label: string;
@@ -109,9 +117,9 @@ function groupClient(group: ConversationGroupData): ClientApp {
109
117
  }
110
118
 
111
119
  function fallbackClientLabel(userAgent: string | null): string {
112
- if (userAgent === null || userAgent === "") return "Unknown IDE";
120
+ if (userAgent === null || userAgent === "") return "Unknown client";
113
121
  const firstToken = userAgent.split(/[ /;]/)[0] ?? "";
114
- if (firstToken === "") return "Unknown IDE";
122
+ if (firstToken === "") return "Unknown client";
115
123
  return firstToken;
116
124
  }
117
125
 
@@ -139,28 +147,56 @@ function sortLogsByTimestamp(logs: readonly CapturedLog[]): CapturedLog[] {
139
147
  return [...logs].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
140
148
  }
141
149
 
142
- function firstTimestamp(logs: readonly CapturedLog[]): string {
143
- return sortLogsByTimestamp(logs)[0]?.timestamp ?? "";
150
+ function latestTimestamp(logs: readonly CapturedLog[]): string {
151
+ let latest = "";
152
+ for (const log of logs) {
153
+ if (log.timestamp > latest) latest = log.timestamp;
154
+ }
155
+ return latest;
144
156
  }
145
157
 
146
- function sortByFirstLogTime<T extends { logs: readonly CapturedLog[] }>(items: readonly T[]): T[] {
147
- return [...items].sort((a, b) => firstTimestamp(a.logs).localeCompare(firstTimestamp(b.logs)));
158
+ export function operationalPriority(logs: readonly CapturedLog[]): number {
159
+ if (hasPendingLogs(logs)) return 0;
160
+ if (
161
+ logs.some(
162
+ (log) =>
163
+ (log.error !== null && log.error !== undefined) ||
164
+ (log.responseStatus !== null &&
165
+ log.responseStatus !== undefined &&
166
+ log.responseStatus >= 400),
167
+ )
168
+ ) {
169
+ return 1;
170
+ }
171
+ return 2;
172
+ }
173
+
174
+ function sortByOperationalPriority<T extends { id: string; logs: readonly CapturedLog[] }>(
175
+ items: readonly T[],
176
+ ): T[] {
177
+ return [...items].sort((left, right) => {
178
+ const priorityDifference = operationalPriority(left.logs) - operationalPriority(right.logs);
179
+ if (priorityDifference !== 0) return priorityDifference;
180
+ const recencyDifference = latestTimestamp(right.logs).localeCompare(latestTimestamp(left.logs));
181
+ if (recencyDifference !== 0) return recencyDifference;
182
+ return left.id.localeCompare(right.id);
183
+ });
148
184
  }
149
185
 
150
186
  export function buildConversationHierarchy(
151
187
  groups: readonly ConversationGroupData[],
152
- ): IdeGroupData[] {
153
- const ideGroups = new Map<string, MutableIdeGroup>();
188
+ ): ClientGroupData[] {
189
+ const clientGroups = new Map<string, MutableClientGroup>();
154
190
 
155
191
  for (const group of groups) {
156
192
  const userAgent = firstUserAgent(group.logs);
157
193
  const projectFolder = firstClientProjectFolder(group.logs);
158
194
  const client = groupClient(group);
159
195
  const currentIdeKey = ideKey(client, userAgent, projectFolder);
160
- let ideGroup = ideGroups.get(currentIdeKey);
196
+ let clientGroup = clientGroups.get(currentIdeKey);
161
197
 
162
- if (ideGroup === undefined) {
163
- ideGroup = {
198
+ if (clientGroup === undefined) {
199
+ clientGroup = {
164
200
  id: currentIdeKey,
165
201
  client,
166
202
  label: clientLabel(client, userAgent),
@@ -168,14 +204,14 @@ export function buildConversationHierarchy(
168
204
  processes: new Map(),
169
205
  logs: [],
170
206
  };
171
- ideGroups.set(currentIdeKey, ideGroup);
207
+ clientGroups.set(currentIdeKey, clientGroup);
172
208
  }
173
209
 
174
- ideGroup.logs.push(...group.logs);
210
+ clientGroup.logs.push(...group.logs);
175
211
 
176
212
  const clientPid = firstClientPid(group.logs);
177
213
  const currentProcessKey = `${currentIdeKey}:${processKey(clientPid, projectFolder)}`;
178
- let processGroup = ideGroup.processes.get(currentProcessKey);
214
+ let processGroup = clientGroup.processes.get(currentProcessKey);
179
215
 
180
216
  if (processGroup === undefined) {
181
217
  processGroup = {
@@ -185,37 +221,37 @@ export function buildConversationHierarchy(
185
221
  sessions: [],
186
222
  logs: [],
187
223
  };
188
- ideGroup.processes.set(currentProcessKey, processGroup);
224
+ clientGroup.processes.set(currentProcessKey, processGroup);
189
225
  }
190
226
 
191
227
  processGroup.sessions.push(group);
192
228
  processGroup.logs.push(...group.logs);
193
229
  }
194
230
 
195
- const hierarchy: IdeGroupData[] = [];
231
+ const hierarchy: ClientGroupData[] = [];
196
232
 
197
- for (const ideGroup of ideGroups.values()) {
198
- const processes: ProcessGroupData[] = sortByFirstLogTime(
199
- [...ideGroup.processes.values()].map((processGroup) => ({
233
+ for (const clientGroup of clientGroups.values()) {
234
+ const processes: ProcessGroupData[] = sortByOperationalPriority(
235
+ [...clientGroup.processes.values()].map((processGroup) => ({
200
236
  id: processGroup.id,
201
237
  clientPid: processGroup.clientPid,
202
238
  clientProjectFolder: processGroup.clientProjectFolder,
203
- sessions: sortByFirstLogTime(processGroup.sessions),
239
+ sessions: sortByOperationalPriority(processGroup.sessions),
204
240
  logs: sortLogsByTimestamp(processGroup.logs),
205
241
  })),
206
242
  );
207
243
 
208
244
  hierarchy.push({
209
- id: ideGroup.id,
210
- client: ideGroup.client,
211
- label: ideGroup.label,
212
- userAgent: ideGroup.userAgent,
245
+ id: clientGroup.id,
246
+ client: clientGroup.client,
247
+ label: clientGroup.label,
248
+ userAgent: clientGroup.userAgent,
213
249
  processes,
214
- logs: sortLogsByTimestamp(ideGroup.logs),
250
+ logs: sortLogsByTimestamp(clientGroup.logs),
215
251
  });
216
252
  }
217
253
 
218
- return sortByFirstLogTime(hierarchy);
254
+ return sortByOperationalPriority(hierarchy);
219
255
  }
220
256
 
221
257
  function formatTokenCount(count: number): string {
@@ -248,6 +284,34 @@ function countSessions(processes: readonly ProcessGroupData[]): number {
248
284
  return sessions;
249
285
  }
250
286
 
287
+ function readStoredIdSet(storageKey: string): ReadonlySet<string> {
288
+ if (typeof window === "undefined") return new Set();
289
+ try {
290
+ const parsed: unknown = JSON.parse(window.localStorage.getItem(storageKey) ?? "[]");
291
+ if (!Array.isArray(parsed)) return new Set();
292
+ return new Set(parsed.filter((value): value is string => typeof value === "string"));
293
+ } catch {
294
+ return new Set();
295
+ }
296
+ }
297
+
298
+ function persistIdSet(storageKey: string, values: ReadonlySet<string>): void {
299
+ try {
300
+ window.localStorage.setItem(storageKey, JSON.stringify([...values]));
301
+ } catch {
302
+ // Local storage can be unavailable in hardened browser profiles.
303
+ }
304
+ }
305
+
306
+ function removeIds(values: ReadonlySet<string>, ids: ReadonlySet<string>): ReadonlySet<string> {
307
+ let changed = false;
308
+ const next = new Set(values);
309
+ for (const id of ids) {
310
+ if (next.delete(id)) changed = true;
311
+ }
312
+ return changed ? next : values;
313
+ }
314
+
251
315
  function shortProjectPath(projectFolder: string | null): string | null {
252
316
  if (projectFolder === null || projectFolder === "") return null;
253
317
  const parts = projectFolder
@@ -273,44 +337,30 @@ function SummaryChip({
273
337
  return (
274
338
  <span
275
339
  className={cn(
276
- "bg-white/[0.045] inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs text-muted-foreground",
340
+ "bg-white/[0.04] inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs text-muted-foreground",
277
341
  className,
278
342
  )}
279
343
  >
280
- <span className="font-mono text-[10px] uppercase tracking-[0.12em] text-white/35">
281
- {label}
282
- </span>
344
+ <span className="font-mono text-[10px] uppercase text-white/35">{label}</span>
283
345
  <span className="font-mono tabular-nums text-slate-100">{value}</span>
284
346
  </span>
285
347
  );
286
348
  }
287
349
 
288
- function PendingTurnBadge({
289
- count,
290
- label = "Turn open",
291
- }: {
292
- count: number;
293
- label?: string;
294
- }): JSX.Element {
350
+ function AncestorActivityIndicator({ count }: { count: number }): JSX.Element {
351
+ const label = `${count} open turn${count === 1 ? "" : "s"}`;
295
352
  return (
296
353
  <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`}
354
+ className="relative inline-flex size-6 shrink-0 items-center justify-center rounded-md bg-amber-300/[0.08] text-amber-100/90 ring-1 ring-amber-200/10"
355
+ aria-label={label}
356
+ title={label}
299
357
  >
300
358
  <Loader2 className="size-3 animate-spin" />
301
- {label}
302
- {count > 1 && <span className="tabular-nums">x{count}</span>}
303
- </span>
304
- );
305
- }
306
-
307
- function LoadingSweep(): JSX.Element {
308
- return (
309
- <span
310
- className="pointer-events-none absolute inset-x-3 bottom-0 h-px overflow-hidden rounded-full bg-white/[0.06]"
311
- aria-hidden="true"
312
- >
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)]" />
359
+ {count > 1 && (
360
+ <span className="absolute translate-x-2 -translate-y-2 rounded bg-background px-0.5 font-mono text-[8px] tabular-nums text-amber-100">
361
+ {count}
362
+ </span>
363
+ )}
314
364
  </span>
315
365
  );
316
366
  }
@@ -320,13 +370,43 @@ export function ConversationGroupList({
320
370
  ...groupProps
321
371
  }: ConversationGroupListProps): JSX.Element {
322
372
  const hierarchy = useMemo(() => buildConversationHierarchy(groups), [groups]);
323
- const [collapsedIdeIds, setCollapsedIdeIds] = useState<ReadonlySet<string>>(() => new Set());
373
+ const [collapsedClientIds, setCollapsedClientIds] = useState<ReadonlySet<string>>(
374
+ () => new Set(),
375
+ );
324
376
  const [collapsedProcessIds, setCollapsedProcessIds] = useState<ReadonlySet<string>>(
325
377
  () => new Set(),
326
378
  );
379
+ const [storedCollapseStateLoaded, setStoredCollapseStateLoaded] = useState(false);
380
+
381
+ useEffect(() => {
382
+ setCollapsedClientIds(readStoredIdSet(COLLAPSED_CLIENTS_STORAGE_KEY));
383
+ setCollapsedProcessIds(readStoredIdSet(COLLAPSED_PROCESSES_STORAGE_KEY));
384
+ setStoredCollapseStateLoaded(true);
385
+ }, []);
386
+
387
+ useEffect(() => {
388
+ if (!storedCollapseStateLoaded) return;
389
+ persistIdSet(COLLAPSED_CLIENTS_STORAGE_KEY, collapsedClientIds);
390
+ persistIdSet(COLLAPSED_PROCESSES_STORAGE_KEY, collapsedProcessIds);
391
+ }, [collapsedClientIds, collapsedProcessIds, storedCollapseStateLoaded]);
327
392
 
328
- const toggleIde = useCallback((id: string) => {
329
- setCollapsedIdeIds((previous) => {
393
+ useEffect(() => {
394
+ const activeClientIds = new Set(
395
+ hierarchy.filter((group) => hasPendingLogs(group.logs)).map((group) => group.id),
396
+ );
397
+ const activeProcessIds = new Set(
398
+ hierarchy.flatMap((group) =>
399
+ group.processes
400
+ .filter((process) => hasPendingLogs(process.logs))
401
+ .map((process) => process.id),
402
+ ),
403
+ );
404
+ setCollapsedClientIds((previous) => removeIds(previous, activeClientIds));
405
+ setCollapsedProcessIds((previous) => removeIds(previous, activeProcessIds));
406
+ }, [hierarchy]);
407
+
408
+ const toggleClient = useCallback((id: string) => {
409
+ setCollapsedClientIds((previous) => {
330
410
  const next = new Set(previous);
331
411
  if (next.has(id)) {
332
412
  next.delete(id);
@@ -350,14 +430,14 @@ export function ConversationGroupList({
350
430
  }, []);
351
431
 
352
432
  return (
353
- <div className="space-y-4">
354
- {hierarchy.map((ideGroup) => (
355
- <IdeGroupSection
356
- key={ideGroup.id}
357
- group={ideGroup}
358
- collapsed={collapsedIdeIds.has(ideGroup.id)}
433
+ <div className="space-y-2">
434
+ {hierarchy.map((clientGroup) => (
435
+ <ClientGroupSection
436
+ key={clientGroup.id}
437
+ group={clientGroup}
438
+ collapsed={collapsedClientIds.has(clientGroup.id)}
359
439
  collapsedProcessIds={collapsedProcessIds}
360
- onToggleIde={() => toggleIde(ideGroup.id)}
440
+ onToggleClient={() => toggleClient(clientGroup.id)}
361
441
  onToggleProcess={toggleProcess}
362
442
  conversationProps={groupProps}
363
443
  />
@@ -366,18 +446,18 @@ export function ConversationGroupList({
366
446
  );
367
447
  }
368
448
 
369
- function IdeGroupSection({
449
+ function ClientGroupSection({
370
450
  group,
371
451
  collapsed,
372
452
  collapsedProcessIds,
373
- onToggleIde,
453
+ onToggleClient,
374
454
  onToggleProcess,
375
455
  conversationProps,
376
456
  }: {
377
- group: IdeGroupData;
457
+ group: ClientGroupData;
378
458
  collapsed: boolean;
379
459
  collapsedProcessIds: ReadonlySet<string>;
380
- onToggleIde: () => void;
460
+ onToggleClient: () => void;
381
461
  onToggleProcess: (id: string) => void;
382
462
  conversationProps: Omit<ConversationGroupProps, "group" | "expanded" | "onExpandedChange">;
383
463
  }): JSX.Element {
@@ -390,26 +470,30 @@ function IdeGroupSection({
390
470
  return (
391
471
  <section
392
472
  className={cn(
393
- "bg-card/75 shadow-sm relative isolate overflow-hidden rounded-[8px]",
394
- isWorking && "bg-card/85",
473
+ "bg-card/60 relative isolate overflow-hidden rounded-[8px] shadow-sm",
474
+ isWorking && "bg-card/70",
395
475
  )}
396
476
  data-ide-group={group.id}
397
477
  >
398
478
  <button
399
479
  type="button"
400
- 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"
401
- onClick={onToggleIde}
480
+ className="grid w-full min-w-0 grid-cols-[2.25rem_minmax(0,1fr)_auto] items-center gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-white/[0.025] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-300/40"
481
+ onClick={onToggleClient}
402
482
  aria-expanded={!collapsed}
403
483
  >
404
484
  {group.client === "unknown" ? (
405
- <span className="bg-white/[0.045] inline-flex size-10 shrink-0 items-center justify-center rounded-[8px] text-slate-200">
406
- <Monitor className="size-5" />
485
+ <span className="bg-white/[0.04] inline-flex size-9 shrink-0 items-center justify-center rounded-md text-slate-300">
486
+ <Monitor className="size-4" />
407
487
  </span>
408
488
  ) : (
409
- <ClientLogo client={group.client} className="size-10 rounded-[8px] text-cyan-100" />
489
+ <ClientLogo
490
+ client={group.client}
491
+ className="size-9 rounded-md text-cyan-100"
492
+ decorative={true}
493
+ />
410
494
  )}
411
495
 
412
- <span className="min-w-0 flex-1">
496
+ <span className="min-w-0">
413
497
  <span className="flex min-w-0 items-center gap-2">
414
498
  <span
415
499
  className="truncate text-sm font-semibold text-slate-50"
@@ -417,37 +501,37 @@ function IdeGroupSection({
417
501
  >
418
502
  {group.label}
419
503
  </span>
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">
421
- IDE
504
+ <span className="rounded bg-cyan-300/[0.05] px-1.5 py-0.5 font-mono text-[9px] uppercase text-cyan-100/70">
505
+ {clientAppTypeLabel(group.client)}
422
506
  </span>
423
- {isWorking && <PendingTurnBadge count={pendingTurnCount} label="Turn open" />}
507
+ {isWorking && <AncestorActivityIndicator count={pendingTurnCount} />}
424
508
  </span>
425
- <span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
509
+ <span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
426
510
  <span>
427
511
  {group.processes.length} process{group.processes.length !== 1 ? "es" : ""}
428
512
  </span>
429
513
  <span>
430
514
  {sessionCount} session{sessionCount !== 1 ? "s" : ""}
431
515
  </span>
432
- <span>
433
- {logCount} log{logCount !== 1 ? "s" : ""}
434
- </span>
516
+ <span>{logCount} req</span>
435
517
  </span>
436
518
  </span>
437
519
 
438
- <span className="hidden shrink-0 items-center gap-2 md:flex">
439
- <SummaryChip label="IN" value={formatTokenCount(totals.inputTokens)} />
440
- <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
441
- </span>
442
-
443
- <span className="bg-white/[0.045] hover:bg-white/[0.075] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-slate-200">
444
- {collapsed ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
520
+ <span className="flex shrink-0 items-center gap-2">
521
+ {collapsed && (
522
+ <span className="hidden shrink-0 items-center gap-2 md:flex">
523
+ <SummaryChip label="IN" value={formatTokenCount(totals.inputTokens)} />
524
+ <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
525
+ </span>
526
+ )}
527
+ <span className="bg-white/[0.04] hover:bg-white/[0.07] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-slate-300">
528
+ {collapsed ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
529
+ </span>
445
530
  </span>
446
531
  </button>
447
- {isWorking && <LoadingSweep />}
448
532
 
449
533
  {!collapsed && (
450
- <div className="space-y-3 px-3 pb-3 pt-2 sm:px-4">
534
+ <div className="space-y-1 px-2 pb-2 pt-1">
451
535
  {group.processes.map((processGroup) => (
452
536
  <ProcessGroupSection
453
537
  key={processGroup.id}
@@ -483,74 +567,71 @@ function ProcessGroupSection({
483
567
  const processLabel =
484
568
  group.clientPid !== null && group.clientPid !== undefined
485
569
  ? `PID ${group.clientPid}`
486
- : "process";
570
+ : "PID unavailable";
487
571
 
488
572
  return (
489
- <section className="relative pl-5">
490
- <span className="absolute bottom-1 left-2 top-1 w-px bg-white/[0.055]" aria-hidden="true" />
491
- <div className="bg-black/[0.10] relative overflow-hidden rounded-[8px]">
492
- <button
493
- type="button"
494
- 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"
495
- onClick={onToggle}
496
- aria-expanded={!collapsed}
497
- >
498
- <span className="bg-white/[0.045] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-amber-100">
499
- <Cpu className="size-4" />
500
- </span>
573
+ <section className="relative pl-4">
574
+ <button
575
+ type="button"
576
+ className="bg-black/[0.07] grid w-full min-w-0 grid-cols-[2rem_minmax(0,1fr)_auto] items-center gap-2.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-white/[0.03] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-300/35"
577
+ onClick={onToggle}
578
+ aria-expanded={!collapsed}
579
+ >
580
+ <span className="bg-white/[0.04] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-amber-100/85">
581
+ <Cpu className="size-4" />
582
+ </span>
501
583
 
502
- <span className="min-w-0 flex-1">
503
- <span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
504
- <span className="font-mono text-xs font-semibold tabular-nums text-slate-100">
505
- {processLabel}
506
- </span>
507
- {isWorking && <PendingTurnBadge count={pendingTurnCount} label="Turn open" />}
508
- {projectLabel !== null && (
509
- <span
510
- className="inline-flex min-w-0 items-center gap-1 text-xs text-muted-foreground"
511
- title={group.clientProjectFolder ?? projectLabel}
512
- >
513
- <FolderOpen className="size-3 shrink-0" />
514
- <span className="truncate font-mono">{projectLabel}</span>
515
- </span>
516
- )}
584
+ <span className="min-w-0">
585
+ <span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
586
+ <span className="font-mono text-xs font-semibold tabular-nums text-slate-100">
587
+ {processLabel}
517
588
  </span>
518
- <span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
519
- <span>
520
- {sessionCount} session{sessionCount !== 1 ? "s" : ""}
521
- </span>
522
- <span>
523
- {logCount} log{logCount !== 1 ? "s" : ""}
589
+ {isWorking && <AncestorActivityIndicator count={pendingTurnCount} />}
590
+ {projectLabel !== null && (
591
+ <span
592
+ className="inline-flex min-w-0 items-center gap-1 text-[11px] text-muted-foreground"
593
+ title={group.clientProjectFolder ?? projectLabel}
594
+ >
595
+ <FolderOpen className="size-3 shrink-0" />
596
+ <span className="truncate font-mono">{projectLabel}</span>
524
597
  </span>
525
- </span>
598
+ )}
526
599
  </span>
527
-
528
- <span className="hidden shrink-0 items-center gap-2 md:flex">
529
- <SummaryChip label="IN" value={formatTokenCount(totals.inputTokens)} />
530
- <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
600
+ <span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
601
+ <span>
602
+ {sessionCount} session{sessionCount !== 1 ? "s" : ""}
603
+ </span>
604
+ <span>{logCount} req</span>
531
605
  </span>
606
+ </span>
532
607
 
533
- <span className="bg-white/[0.045] hover:bg-white/[0.075] inline-flex size-7 shrink-0 items-center justify-center rounded-md text-slate-200">
608
+ <span className="flex shrink-0 items-center gap-2">
609
+ {collapsed && (
610
+ <span className="hidden shrink-0 items-center gap-2 md:flex">
611
+ <SummaryChip label="IN" value={formatTokenCount(totals.inputTokens)} />
612
+ <SummaryChip label="OUT" value={formatTokenCount(totals.outputTokens)} />
613
+ </span>
614
+ )}
615
+ <span className="bg-white/[0.04] hover:bg-white/[0.07] inline-flex size-7 shrink-0 items-center justify-center rounded-md text-slate-300">
534
616
  {collapsed ? <ChevronRight className="size-4" /> : <ChevronDown className="size-4" />}
535
617
  </span>
536
- </button>
537
- {isWorking && <LoadingSweep />}
538
-
539
- {!collapsed && (
540
- <div className="space-y-2 p-2 pt-1 sm:p-3 sm:pt-2">
541
- {group.sessions.map((session) => (
542
- <ConversationGroup
543
- key={session.id}
544
- group={session}
545
- {...conversationProps}
546
- showClientIdentity={false}
547
- showLogClientMetadata={false}
548
- showProcessMetadata={false}
549
- />
550
- ))}
551
- </div>
552
- )}
553
- </div>
618
+ </span>
619
+ </button>
620
+
621
+ {!collapsed && (
622
+ <div className="space-y-1.5 pb-1 pl-4 pr-1 pt-1.5">
623
+ {group.sessions.map((session) => (
624
+ <ConversationGroup
625
+ key={session.id}
626
+ group={session}
627
+ {...conversationProps}
628
+ showClientIdentity={false}
629
+ showLogClientMetadata={false}
630
+ showProcessMetadata={false}
631
+ />
632
+ ))}
633
+ </div>
634
+ )}
554
635
  </section>
555
636
  );
556
637
  }