fleetlens 0.2.9 → 0.3.0

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 (88) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +3 -3
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  12. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/app/apps/web/.next/server/app/page.js +3 -3
  15. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  16. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  17. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  18. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  19. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  20. package/app/apps/web/.next/server/app/projects/[slug]/page.js +3 -3
  21. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  23. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  24. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  25. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  26. package/app/apps/web/.next/server/app/sessions/[id]/page.js +3 -2
  27. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  29. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  30. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  32. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  33. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  35. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +1 -1
  36. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +3 -0
  37. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__05lbtny._.js +12 -0
  38. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ms7h0~._.js +1 -1
  39. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08cr2u9._.js → [root-of-the-server]__0p~g4m6._.js} +2 -2
  40. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +1 -1
  41. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wvzyl7._.js +3 -0
  42. package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +1 -1
  43. package/app/apps/web/.next/server/chunks/ssr/_0ph19-k._.js +12 -0
  44. package/app/apps/web/.next/server/chunks/ssr/_0u29mdd._.js +3 -0
  45. package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
  46. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +1 -1
  47. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
  48. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +1 -1
  49. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  50. package/app/apps/web/.next/server/pages/500.html +1 -1
  51. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  52. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  53. package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +6 -0
  54. package/app/apps/web/.next/static/chunks/08983lz4~hffx.js +6 -0
  55. package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +1 -0
  56. package/app/apps/web/.next/static/chunks/0p1lpoh02zh8h.js +10 -0
  57. package/app/apps/web/.next/static/chunks/{03fw7f2obcvq~.js → 14q3xsfpkvuef.js} +2 -2
  58. package/app/apps/web/app/layout.tsx +2 -0
  59. package/app/apps/web/app/projects/[slug]/page.tsx +2 -0
  60. package/app/apps/web/app/sessions/[id]/page.tsx +27 -1
  61. package/app/apps/web/app/sessions/[id]/session-view.tsx +372 -793
  62. package/app/apps/web/app/sessions/[id]/team-tab/adapter.ts +633 -0
  63. package/app/apps/web/app/sessions/[id]/team-tab/format.ts +20 -0
  64. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +682 -0
  65. package/app/apps/web/app/sessions/[id]/team-tab/team-tab-client.tsx +78 -0
  66. package/app/apps/web/app/sessions/[id]/team-tab/team-table.tsx +796 -0
  67. package/app/apps/web/app/sessions/[id]/team-tab/turn-drawer.tsx +805 -0
  68. package/app/apps/web/app/sessions/[id]/tool-cards.tsx +461 -0
  69. package/app/apps/web/app/sessions/[id]/turn-steps.tsx +306 -0
  70. package/app/apps/web/app/sessions/sessions-grid.tsx +36 -5
  71. package/app/apps/web/components/live-sessions-widget.tsx +20 -6
  72. package/app/apps/web/components/team-badge.tsx +90 -0
  73. package/app/apps/web/package.json +1 -1
  74. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  75. package/dist/index.js +71 -13
  76. package/package.json +1 -1
  77. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b..h_8._.js +0 -3
  78. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0cdd~8n._.js +0 -3
  79. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~a~v00._.js +0 -3
  80. package/app/apps/web/.next/server/chunks/ssr/_0zv~jv-._.js +0 -12
  81. package/app/apps/web/.next/server/chunks/ssr/_12dtryh._.js +0 -12
  82. package/app/apps/web/.next/static/chunks/07oyxajlp3dvb.js +0 -6
  83. package/app/apps/web/.next/static/chunks/0a24elrqoqqep.js +0 -10
  84. package/app/apps/web/.next/static/chunks/0i4zodv86~~~i.js +0 -1
  85. package/app/apps/web/.next/static/chunks/0rbm60-qyshdm.js +0 -6
  86. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → 0fPDRTdMQ-Y62h6fHNJXL}/_buildManifest.js +0 -0
  87. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → 0fPDRTdMQ-Y62h6fHNJXL}/_clientMiddlewareManifest.js +0 -0
  88. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → 0fPDRTdMQ-Y62h6fHNJXL}/_ssgManifest.js +0 -0
@@ -0,0 +1,805 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { ChevronDown, ChevronRight } from "lucide-react";
7
+ import type {
8
+ ContentBlock,
9
+ PresentationRow,
10
+ TurnMegaRow,
11
+ SessionEvent,
12
+ } from "@claude-lens/parser";
13
+ import { formatGap } from "@/lib/format";
14
+ import type { TeamTurn } from "./adapter";
15
+ import { formatTeammatePreview } from "./format";
16
+ import { TurnStepsList, MAX_INLINE_STEPS, shortenToolName } from "../turn-steps";
17
+ import { ToolUseCard, type ToolUseInput } from "../tool-cards";
18
+
19
+ type Props = {
20
+ turn: TeamTurn | null;
21
+ trackLabel: string;
22
+ trackColor: string;
23
+ onClose: () => void;
24
+ };
25
+
26
+ type View =
27
+ | { page: "turn" }
28
+ | { page: "step"; row: PresentationRow; index: number };
29
+
30
+ /** Widened drawer to give the full turn card room to breathe. Reads as
31
+ * roughly the same layout as the transcript's collapsed-turn row — a
32
+ * standalone modal-ish card inside a drawer rather than a grid row. */
33
+ const DRAWER_WIDTH = 620;
34
+
35
+ export function TurnDrawer({ turn, trackLabel, trackColor, onClose }: Props) {
36
+ const [view, setView] = useState<View>({ page: "turn" });
37
+
38
+ useEffect(() => {
39
+ setView({ page: "turn" });
40
+ }, [turn?.id]);
41
+
42
+ useEffect(() => {
43
+ if (!turn) return;
44
+ const onKey = (e: KeyboardEvent) => {
45
+ if (e.key === "Escape") {
46
+ if (view.page === "step") setView({ page: "turn" });
47
+ else onClose();
48
+ }
49
+ };
50
+ window.addEventListener("keydown", onKey);
51
+ return () => window.removeEventListener("keydown", onKey);
52
+ }, [turn, onClose, view.page]);
53
+
54
+ if (!turn) return null;
55
+
56
+ const startStr = new Date(turn.startMs).toLocaleString();
57
+ const endStr = new Date(turn.endMs).toLocaleTimeString();
58
+ const durationStr = formatGap(turn.durationMs);
59
+
60
+ return (
61
+ <>
62
+ <div
63
+ onClick={onClose}
64
+ style={{
65
+ position: "fixed",
66
+ inset: 0,
67
+ background: "rgba(0,0,0,0.4)",
68
+ zIndex: 1000,
69
+ }}
70
+ />
71
+ <div
72
+ style={{
73
+ position: "fixed",
74
+ top: 0,
75
+ right: 0,
76
+ bottom: 0,
77
+ width: DRAWER_WIDTH,
78
+ maxWidth: "96vw",
79
+ background: "var(--af-surface-elevated)",
80
+ borderLeft: "1px solid var(--af-border-subtle)",
81
+ zIndex: 1001,
82
+ display: "flex",
83
+ flexDirection: "column",
84
+ fontSize: 12,
85
+ color: "var(--af-text)",
86
+ }}
87
+ >
88
+ <div
89
+ style={{
90
+ padding: "12px 16px",
91
+ borderBottom: "1px solid var(--af-border-subtle)",
92
+ display: "flex",
93
+ alignItems: "center",
94
+ justifyContent: "space-between",
95
+ gap: 8,
96
+ flexShrink: 0,
97
+ }}
98
+ >
99
+ {view.page === "step" ? (
100
+ <button
101
+ onClick={() => setView({ page: "turn" })}
102
+ style={{
103
+ background: "transparent",
104
+ border: "1px solid var(--af-border-subtle)",
105
+ color: "var(--af-text-secondary)",
106
+ padding: "4px 10px",
107
+ borderRadius: 4,
108
+ cursor: "pointer",
109
+ fontSize: 11,
110
+ fontFamily: "ui-monospace, monospace",
111
+ }}
112
+ >
113
+ ← Back to turn
114
+ </button>
115
+ ) : (
116
+ <div>
117
+ <div
118
+ style={{
119
+ fontSize: 10,
120
+ fontWeight: 700,
121
+ color: trackColor,
122
+ letterSpacing: "0.08em",
123
+ fontFamily: "ui-monospace, monospace",
124
+ }}
125
+ >
126
+ {trackLabel}
127
+ </div>
128
+ <div
129
+ style={{
130
+ fontSize: 11,
131
+ color: "var(--af-text-tertiary)",
132
+ fontFamily: "ui-monospace, monospace",
133
+ marginTop: 2,
134
+ }}
135
+ >
136
+ {startStr} → {endStr} · {durationStr}
137
+ </div>
138
+ </div>
139
+ )}
140
+ <button
141
+ onClick={onClose}
142
+ aria-label="Close"
143
+ style={{
144
+ background: "transparent",
145
+ border: "1px solid var(--af-border-subtle)",
146
+ color: "var(--af-text-secondary)",
147
+ width: 28,
148
+ height: 28,
149
+ borderRadius: 4,
150
+ cursor: "pointer",
151
+ fontSize: 14,
152
+ lineHeight: 1,
153
+ flexShrink: 0,
154
+ }}
155
+ >
156
+ ×
157
+ </button>
158
+ </div>
159
+
160
+ <div
161
+ style={{
162
+ flex: 1,
163
+ overflowY: "auto",
164
+ padding: "14px 18px",
165
+ }}
166
+ >
167
+ {view.page === "turn" ? (
168
+ <FullTurnCard
169
+ turn={turn.megaRow}
170
+ userRow={turn.userPrompt}
171
+ color={trackColor}
172
+ onStepClick={(row, index) => setView({ page: "step", row, index })}
173
+ />
174
+ ) : (
175
+ <StepDetail row={view.row} color={trackColor} />
176
+ )}
177
+ </div>
178
+ </div>
179
+ </>
180
+ );
181
+ }
182
+
183
+ /* ---------------------------------------------------------------- */
184
+ /* FullTurnCard — the HUMAN + AGENT + steps + conclusion layout */
185
+ /* used inside the team drawer. Visually mirrors the transcript's */
186
+ /* collapsed-turn row but as a standalone vertical card so it fits */
187
+ /* the drawer / modal context without the grid columns used by the */
188
+ /* transcript list. */
189
+ /* ---------------------------------------------------------------- */
190
+
191
+ function FullTurnCard({
192
+ turn,
193
+ userRow,
194
+ color,
195
+ onStepClick,
196
+ }: {
197
+ turn: TurnMegaRow;
198
+ userRow?: PresentationRow;
199
+ color: string;
200
+ onStepClick: (row: PresentationRow, index: number) => void;
201
+ }) {
202
+ const s = turn.summary;
203
+ const firstAgentIdx = s.firstAgentIndex ?? -1;
204
+ const finalAgentIdx = s.finalAgentIndex ?? -1;
205
+ const firstAgentRow = firstAgentIdx >= 0 ? turn.rows[firstAgentIdx] : undefined;
206
+ const finalAgentRow =
207
+ finalAgentIdx >= 0 && finalAgentIdx !== firstAgentIdx
208
+ ? turn.rows[finalAgentIdx]
209
+ : undefined;
210
+
211
+ // Middle rows exclude the first and conclusion — they're surfaced as
212
+ // their own sections above and below the steps list.
213
+ const middleRows = turn.rows.filter((_, i) => {
214
+ if (i === firstAgentIdx) return false;
215
+ if (finalAgentRow && i === finalAgentIdx) return false;
216
+ return true;
217
+ });
218
+
219
+ return (
220
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
221
+ {userRow && userRow.kind === "user" && (() => {
222
+ const tm = userRow.event.teammateMessage;
223
+ const label = tm ? `From ${tm.teammateId}` : "Human";
224
+ // Drawer shows the full body — no truncation. For teammate
225
+ // messages the body is the complete instruction or protocol
226
+ // payload extracted from the <teammate-message> wrapper.
227
+ const text = tm
228
+ ? tm.body
229
+ : (userRow.displayPreview ?? userRow.event.preview ?? "");
230
+ return (
231
+ <ExpandableTextBlock
232
+ label={label}
233
+ color={tm ? "var(--af-text-tertiary)" : color}
234
+ text={text}
235
+ />
236
+ );
237
+ })()}
238
+
239
+ {firstAgentRow && firstAgentRow.kind === "agent" && (
240
+ <ExpandableAgentBlock
241
+ label="First message"
242
+ color={color}
243
+ row={firstAgentRow}
244
+ />
245
+ )}
246
+
247
+ <TurnStatsRow summary={s} rows={turn.rows} durationMs={turn.durationMs} />
248
+
249
+ {middleRows.length > 0 && (
250
+ <StepsSection
251
+ color={color}
252
+ rows={middleRows}
253
+ onStepClick={onStepClick}
254
+ />
255
+ )}
256
+
257
+ {finalAgentRow && finalAgentRow.kind === "agent" && (
258
+ <ExpandableAgentBlock
259
+ label="Conclusion"
260
+ color={color}
261
+ row={finalAgentRow}
262
+ arrow
263
+ />
264
+ )}
265
+ </div>
266
+ );
267
+ }
268
+
269
+ function ExpandableTextBlock({
270
+ label,
271
+ color,
272
+ text,
273
+ }: {
274
+ label: string;
275
+ color: string;
276
+ text: string;
277
+ }) {
278
+ const [expanded, setExpanded] = useState(false);
279
+ return (
280
+ <div
281
+ onClick={() => setExpanded((v) => !v)}
282
+ style={{
283
+ padding: "8px 12px",
284
+ borderRadius: 6,
285
+ background: expanded ? "var(--af-surface-hover)" : "transparent",
286
+ border: `1px solid ${expanded ? "var(--af-border-subtle)" : "transparent"}`,
287
+ cursor: "pointer",
288
+ transition: "all 0.12s",
289
+ }}
290
+ >
291
+ <div
292
+ style={{
293
+ display: "flex",
294
+ alignItems: "center",
295
+ gap: 6,
296
+ fontSize: 10,
297
+ color: "var(--af-text-tertiary)",
298
+ textTransform: "uppercase",
299
+ letterSpacing: "0.05em",
300
+ fontWeight: 600,
301
+ fontFamily: "ui-monospace, monospace",
302
+ }}
303
+ >
304
+ {expanded ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
305
+ <span style={{ color }}>{label}</span>
306
+ </div>
307
+ {expanded ? (
308
+ <div className="sl-prose" style={{ fontSize: 13, marginTop: 6 }}>
309
+ <ReactMarkdown
310
+ remarkPlugins={[remarkGfm]}
311
+ components={{
312
+ a: (props) => (
313
+ <a
314
+ {...props}
315
+ target="_blank"
316
+ rel="noopener noreferrer"
317
+ style={{ color: "var(--af-accent)", textDecoration: "underline" }}
318
+ />
319
+ ),
320
+ }}
321
+ >
322
+ {text}
323
+ </ReactMarkdown>
324
+ </div>
325
+ ) : (
326
+ <div
327
+ style={{
328
+ marginTop: 4,
329
+ fontSize: 13,
330
+ lineHeight: 1.45,
331
+ color: "var(--af-text)",
332
+ display: "-webkit-box",
333
+ WebkitLineClamp: 2,
334
+ WebkitBoxOrient: "vertical",
335
+ overflow: "hidden",
336
+ }}
337
+ >
338
+ {text}
339
+ </div>
340
+ )}
341
+ </div>
342
+ );
343
+ }
344
+
345
+ function StepsSection({
346
+ color,
347
+ rows,
348
+ onStepClick,
349
+ }: {
350
+ color: string;
351
+ rows: PresentationRow[];
352
+ onStepClick: (row: PresentationRow, index: number) => void;
353
+ }) {
354
+ const [showAll, setShowAll] = useState(false);
355
+ const overflow = rows.length - MAX_INLINE_STEPS;
356
+ return (
357
+ <div>
358
+ <SectionLabel color={color}>STEPS</SectionLabel>
359
+ <div style={{ marginTop: 4 }}>
360
+ <TurnStepsList
361
+ rows={rows}
362
+ onStepClick={onStepClick}
363
+ showAll={showAll}
364
+ />
365
+ </div>
366
+ {overflow > 0 && !showAll && (
367
+ <button
368
+ onClick={() => setShowAll(true)}
369
+ style={{
370
+ display: "flex",
371
+ alignItems: "center",
372
+ justifyContent: "center",
373
+ gap: 6,
374
+ width: "100%",
375
+ padding: "6px 10px",
376
+ marginTop: 6,
377
+ background: "var(--af-surface-hover)",
378
+ border: "1px solid var(--af-border-subtle)",
379
+ borderRadius: 6,
380
+ fontSize: 11,
381
+ fontWeight: 500,
382
+ color: "var(--af-text-secondary)",
383
+ fontFamily: "inherit",
384
+ cursor: "pointer",
385
+ }}
386
+ >
387
+ <ChevronDown size={12} />
388
+ Show all {rows.length} steps
389
+ </button>
390
+ )}
391
+ </div>
392
+ );
393
+ }
394
+
395
+ function ExpandableAgentBlock({
396
+ label,
397
+ color,
398
+ row,
399
+ arrow,
400
+ }: {
401
+ label: string;
402
+ color: string;
403
+ row: Extract<PresentationRow, { kind: "agent" }>;
404
+ arrow?: boolean;
405
+ }) {
406
+ const [expanded, setExpanded] = useState(false);
407
+ return (
408
+ <div
409
+ onClick={() => setExpanded((v) => !v)}
410
+ style={{
411
+ padding: "8px 12px",
412
+ borderRadius: 6,
413
+ background: expanded ? "var(--af-surface-hover)" : "transparent",
414
+ border: `1px solid ${expanded ? "var(--af-border-subtle)" : "transparent"}`,
415
+ cursor: "pointer",
416
+ transition: "all 0.12s",
417
+ borderTop:
418
+ arrow && !expanded ? "1px dashed var(--af-border-subtle)" : undefined,
419
+ }}
420
+ >
421
+ <div
422
+ style={{
423
+ display: "flex",
424
+ alignItems: "center",
425
+ gap: 6,
426
+ fontSize: 10,
427
+ color: "var(--af-text-tertiary)",
428
+ textTransform: "uppercase",
429
+ letterSpacing: "0.05em",
430
+ fontWeight: 600,
431
+ fontFamily: "ui-monospace, monospace",
432
+ }}
433
+ >
434
+ {expanded ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
435
+ <span style={{ color }}>{label}</span>
436
+ </div>
437
+ {expanded ? (
438
+ <div
439
+ className="sl-prose"
440
+ style={{
441
+ fontSize: 13,
442
+ marginTop: 6,
443
+ color: "var(--af-text)",
444
+ lineHeight: 1.5,
445
+ }}
446
+ >
447
+ {row.event.blocks
448
+ .filter((b): b is { type: "text"; text: string } => b.type === "text")
449
+ .map((b, i) => (
450
+ <ReactMarkdown
451
+ key={i}
452
+ remarkPlugins={[remarkGfm]}
453
+ components={{
454
+ a: (props) => (
455
+ <a
456
+ {...props}
457
+ target="_blank"
458
+ rel="noopener noreferrer"
459
+ style={{
460
+ color: "var(--af-accent)",
461
+ textDecoration: "underline",
462
+ }}
463
+ />
464
+ ),
465
+ }}
466
+ >
467
+ {b.text}
468
+ </ReactMarkdown>
469
+ ))}
470
+ </div>
471
+ ) : (
472
+ <div
473
+ style={{
474
+ marginTop: 4,
475
+ display: "flex",
476
+ gap: 6,
477
+ alignItems: "flex-start",
478
+ fontSize: 13,
479
+ lineHeight: 1.45,
480
+ color: "var(--af-text)",
481
+ }}
482
+ >
483
+ {arrow && (
484
+ <span
485
+ style={{
486
+ color: "var(--af-text-tertiary)",
487
+ marginTop: 1,
488
+ fontSize: 11,
489
+ }}
490
+ >
491
+
492
+ </span>
493
+ )}
494
+ <span
495
+ style={{
496
+ overflow: "hidden",
497
+ display: "-webkit-box",
498
+ WebkitLineClamp: 3,
499
+ WebkitBoxOrient: "vertical",
500
+ }}
501
+ >
502
+ {row.event.preview}
503
+ </span>
504
+ </div>
505
+ )}
506
+ </div>
507
+ );
508
+ }
509
+
510
+ function SectionLabel({
511
+ children,
512
+ color,
513
+ }: {
514
+ children: React.ReactNode;
515
+ color: string;
516
+ }) {
517
+ return (
518
+ <div
519
+ style={{
520
+ fontSize: 9,
521
+ color,
522
+ fontWeight: 700,
523
+ textTransform: "uppercase",
524
+ letterSpacing: "0.06em",
525
+ fontFamily: "ui-monospace, monospace",
526
+ }}
527
+ >
528
+ {children}
529
+ </div>
530
+ );
531
+ }
532
+
533
+ /** Activity summary line — Ghostty-style aggregation of tool use
534
+ * categories. A slimmer cousin of session-view's TurnStatsLine. */
535
+ function TurnStatsRow({
536
+ summary,
537
+ rows,
538
+ durationMs,
539
+ }: {
540
+ summary: TurnMegaRow["summary"];
541
+ rows: PresentationRow[];
542
+ durationMs?: number;
543
+ }) {
544
+ let editCount = 0;
545
+ let writeCount = 0;
546
+ let readCount = 0;
547
+ let bashCount = 0;
548
+ let searchCount = 0;
549
+ let agentCount = 0;
550
+ let otherToolCount = 0;
551
+
552
+ for (const r of rows) {
553
+ if (r.kind !== "tool-group") continue;
554
+ for (const ev of r.events) {
555
+ const name = ev.toolName ?? "";
556
+ switch (name) {
557
+ case "Edit":
558
+ editCount++;
559
+ break;
560
+ case "Write":
561
+ writeCount++;
562
+ break;
563
+ case "Read":
564
+ readCount++;
565
+ break;
566
+ case "Bash":
567
+ bashCount++;
568
+ break;
569
+ case "Grep":
570
+ case "Glob":
571
+ searchCount++;
572
+ break;
573
+ case "Agent":
574
+ agentCount++;
575
+ break;
576
+ default:
577
+ otherToolCount++;
578
+ break;
579
+ }
580
+ }
581
+ }
582
+
583
+ const phrases: string[] = [];
584
+ if (editCount + writeCount > 0) {
585
+ const n = editCount + writeCount;
586
+ phrases.push(`Edited ${n} file${n === 1 ? "" : "s"}`);
587
+ }
588
+ if (readCount > 0) phrases.push(`read ${readCount} file${readCount === 1 ? "" : "s"}`);
589
+ if (bashCount > 0) phrases.push(`${bashCount} command${bashCount === 1 ? "" : "s"}`);
590
+ if (searchCount > 0) phrases.push(`${searchCount} search${searchCount === 1 ? "" : "es"}`);
591
+ if (agentCount > 0)
592
+ phrases.push(`${agentCount} sub-agent${agentCount === 1 ? "" : "s"}`);
593
+ if (otherToolCount > 0 && phrases.length === 0)
594
+ phrases.push(`${otherToolCount} tool call${otherToolCount === 1 ? "" : "s"}`);
595
+ if (phrases.length === 0 && summary.agentMessages > 0) {
596
+ phrases.push(
597
+ `${summary.agentMessages} message${summary.agentMessages === 1 ? "" : "s"}`,
598
+ );
599
+ }
600
+
601
+ return (
602
+ <div
603
+ style={{
604
+ fontSize: 11,
605
+ color: "var(--af-text-tertiary)",
606
+ lineHeight: 1.5,
607
+ fontFamily: "ui-monospace, monospace",
608
+ }}
609
+ >
610
+ {phrases.join(", ")}
611
+ {summary.errors > 0 && (
612
+ <span style={{ color: "var(--af-danger)" }}>
613
+ {phrases.length > 0 ? ", " : ""}
614
+ {summary.errors} error{summary.errors === 1 ? "" : "s"}
615
+ </span>
616
+ )}
617
+ {durationMs !== undefined && durationMs > 0 && (
618
+ <>
619
+ <span style={{ opacity: 0.5, marginLeft: 6 }}>·</span>
620
+ <span style={{ marginLeft: 6 }}>{formatGap(durationMs)}</span>
621
+ </>
622
+ )}
623
+ </div>
624
+ );
625
+ }
626
+
627
+ /* ---------------------------------------------------------------- */
628
+ /* Step detail page — unchanged from the previous drawer iteration */
629
+ /* ---------------------------------------------------------------- */
630
+
631
+ function StepDetail({ row, color }: { row: PresentationRow; color: string }) {
632
+ if (row.kind === "user") {
633
+ return (
634
+ <DetailCard
635
+ label="HUMAN"
636
+ color={color}
637
+ body={row.displayPreview ?? row.event.preview ?? ""}
638
+ />
639
+ );
640
+ }
641
+ if (row.kind === "agent") {
642
+ const text = blocksText(row.event.blocks) || row.event.preview || "";
643
+ return <DetailCard label="AGENT" color={color} body={text} />;
644
+ }
645
+ if (row.kind === "tool-group") {
646
+ const toolLabel = row.toolNames
647
+ .slice(0, 4)
648
+ .map((t) => {
649
+ const short = shortenToolName(t.name);
650
+ return t.count > 1 ? `${short} ×${t.count}` : short;
651
+ })
652
+ .join(" · ");
653
+ const overflow = row.toolNames.length > 4 ? ` +${row.toolNames.length - 4}` : "";
654
+ return (
655
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
656
+ <div
657
+ style={{
658
+ display: "flex",
659
+ alignItems: "center",
660
+ gap: 8,
661
+ padding: "8px 0",
662
+ borderBottom: "1px solid var(--af-border-subtle)",
663
+ }}
664
+ >
665
+ <span
666
+ style={{
667
+ fontSize: 11,
668
+ fontWeight: 600,
669
+ padding: "3px 10px",
670
+ borderRadius: 4,
671
+ background: "rgba(138, 133, 128, 0.16)",
672
+ color: "#44403C",
673
+ }}
674
+ >
675
+ Tool
676
+ </span>
677
+ <span
678
+ style={{
679
+ fontSize: 12,
680
+ color: "var(--af-text-secondary)",
681
+ fontWeight: 500,
682
+ }}
683
+ >
684
+ Tool use · {toolLabel}{overflow}
685
+ </span>
686
+ </div>
687
+
688
+ {row.events.map((ev, i) => {
689
+ const toolUse = (ev.blocks ?? []).find(
690
+ (b) => b.type === "tool_use",
691
+ ) as { name?: string; input?: ToolUseInput } | undefined;
692
+ if (!toolUse) return null;
693
+ const name = toolUse.name ?? "?";
694
+ const input = toolUse.input ?? {};
695
+ return (
696
+ <div key={i}>
697
+ <div
698
+ style={{
699
+ fontSize: 10,
700
+ color: "var(--af-text-tertiary)",
701
+ fontFamily: "ui-monospace, monospace",
702
+ marginBottom: 4,
703
+ }}
704
+ >
705
+ #{i + 1}
706
+ </div>
707
+ <ToolUseCard name={name} input={input} />
708
+ </div>
709
+ );
710
+ })}
711
+ </div>
712
+ );
713
+ }
714
+ if (row.kind === "interrupt") {
715
+ return (
716
+ <DetailCard
717
+ label="INTERRUPT"
718
+ color="#f85149"
719
+ body={row.event.preview ?? "[interrupted]"}
720
+ />
721
+ );
722
+ }
723
+ if (row.kind === "error") {
724
+ return <DetailCard label="ERROR" color="#f85149" body={row.message} />;
725
+ }
726
+ if (row.kind === "model") {
727
+ return (
728
+ <DetailCard
729
+ label="MODEL CHANGE"
730
+ color={color}
731
+ body={row.event.preview ?? ""}
732
+ />
733
+ );
734
+ }
735
+ if (row.kind === "task-notification") {
736
+ return (
737
+ <DetailCard
738
+ label={`TASK ${row.status.toUpperCase()}`}
739
+ color={color}
740
+ body={row.summary}
741
+ />
742
+ );
743
+ }
744
+ return null;
745
+ }
746
+
747
+ function DetailCard({
748
+ label,
749
+ color,
750
+ body,
751
+ }: {
752
+ label: string;
753
+ color: string;
754
+ body: string;
755
+ }) {
756
+ return (
757
+ <div
758
+ style={{
759
+ padding: "10px 12px",
760
+ background: "var(--af-surface-hover)",
761
+ borderLeft: `3px solid ${color}`,
762
+ borderRadius: 4,
763
+ marginBottom: 6,
764
+ }}
765
+ >
766
+ <div
767
+ style={{
768
+ fontSize: 9,
769
+ color,
770
+ fontWeight: 600,
771
+ textTransform: "uppercase",
772
+ letterSpacing: "0.04em",
773
+ marginBottom: 4,
774
+ fontFamily: "ui-monospace, monospace",
775
+ }}
776
+ >
777
+ {label}
778
+ </div>
779
+ {body && (
780
+ <div
781
+ style={{
782
+ fontSize: 12,
783
+ color: "var(--af-text)",
784
+ lineHeight: 1.5,
785
+ whiteSpace: "pre-wrap",
786
+ wordBreak: "break-word",
787
+ }}
788
+ >
789
+ {body}
790
+ </div>
791
+ )}
792
+ </div>
793
+ );
794
+ }
795
+
796
+
797
+ function blocksText(blocks: ContentBlock[] | undefined): string {
798
+ if (!blocks) return "";
799
+ const parts: string[] = [];
800
+ for (const b of blocks) {
801
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
802
+ }
803
+ return parts.join("\n\n");
804
+ }
805
+