fleetlens 0.2.8 → 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.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/build-manifest.json +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page.js +3 -3
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +3 -3
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +3 -2
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__05lbtny._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ms7h0~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08cr2u9._.js → [root-of-the-server]__0p~g4m6._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wvzyl7._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0ph19-k._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/_0u29mdd._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +1 -1
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +6 -0
- package/app/apps/web/.next/static/chunks/08983lz4~hffx.js +6 -0
- package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +1 -0
- package/app/apps/web/.next/static/chunks/0p1lpoh02zh8h.js +10 -0
- package/app/apps/web/.next/static/chunks/{03fw7f2obcvq~.js → 14q3xsfpkvuef.js} +2 -2
- package/app/apps/web/app/layout.tsx +2 -0
- package/app/apps/web/app/projects/[slug]/page.tsx +2 -0
- package/app/apps/web/app/sessions/[id]/page.tsx +27 -1
- package/app/apps/web/app/sessions/[id]/session-view.tsx +372 -793
- package/app/apps/web/app/sessions/[id]/team-tab/adapter.ts +633 -0
- package/app/apps/web/app/sessions/[id]/team-tab/format.ts +20 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +682 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-tab-client.tsx +78 -0
- package/app/apps/web/app/sessions/[id]/team-tab/team-table.tsx +796 -0
- package/app/apps/web/app/sessions/[id]/team-tab/turn-drawer.tsx +805 -0
- package/app/apps/web/app/sessions/[id]/tool-cards.tsx +461 -0
- package/app/apps/web/app/sessions/[id]/turn-steps.tsx +306 -0
- package/app/apps/web/app/sessions/sessions-grid.tsx +36 -5
- package/app/apps/web/components/live-sessions-widget.tsx +20 -6
- package/app/apps/web/components/team-badge.tsx +90 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +74 -21
- package/dist/index.js +93 -23
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b..h_8._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0cdd~8n._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~a~v00._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/_0zv~jv-._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/_12dtryh._.js +0 -12
- package/app/apps/web/.next/static/chunks/07oyxajlp3dvb.js +0 -6
- package/app/apps/web/.next/static/chunks/0a24elrqoqqep.js +0 -10
- package/app/apps/web/.next/static/chunks/0i4zodv86~~~i.js +0 -1
- package/app/apps/web/.next/static/chunks/0rbm60-qyshdm.js +0 -6
- /package/app/apps/web/.next/static/{CH5VTNGEbso0WpO5B8BTA → 0fPDRTdMQ-Y62h6fHNJXL}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{CH5VTNGEbso0WpO5B8BTA → 0fPDRTdMQ-Y62h6fHNJXL}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{CH5VTNGEbso0WpO5B8BTA → 0fPDRTdMQ-Y62h6fHNJXL}/_ssgManifest.js +0 -0
|
@@ -39,6 +39,25 @@ import { formatGap, formatOffset, formatRelative, formatTokens, shortId } from "
|
|
|
39
39
|
import { LiveBadge } from "@/components/live-badge";
|
|
40
40
|
import { AskClaudeButton, AskClaudeDrawer } from "@/components/ask-claude";
|
|
41
41
|
import { TailMode } from "@/components/tail-mode";
|
|
42
|
+
import type { TimelineData } from "./team-tab/adapter";
|
|
43
|
+
import { TeamTabClient } from "./team-tab/team-tab-client";
|
|
44
|
+
import { TeamMinimap } from "./team-tab/team-minimap";
|
|
45
|
+
import {
|
|
46
|
+
ROLE_THEMES,
|
|
47
|
+
MAX_INLINE_STEPS,
|
|
48
|
+
rowPreview,
|
|
49
|
+
formatToolSummary,
|
|
50
|
+
shortenToolName,
|
|
51
|
+
TurnStepsList,
|
|
52
|
+
} from "./turn-steps";
|
|
53
|
+
import {
|
|
54
|
+
ToolUseCard,
|
|
55
|
+
ToolCardShell,
|
|
56
|
+
CodeBlock,
|
|
57
|
+
DiffView,
|
|
58
|
+
PathLabel,
|
|
59
|
+
type ToolUseInput,
|
|
60
|
+
} from "./tool-cards";
|
|
42
61
|
|
|
43
62
|
/* ------------------------------------------------------------------ */
|
|
44
63
|
/* Constants + theming */
|
|
@@ -52,63 +71,6 @@ const IDLE_THRESHOLD_MS = 2000;
|
|
|
52
71
|
* them below the sticky area instead of hidden underneath. */
|
|
53
72
|
const STICKY_HEADER_HEIGHT = 310;
|
|
54
73
|
|
|
55
|
-
type RoleTheme = {
|
|
56
|
-
label: string;
|
|
57
|
-
bg: string;
|
|
58
|
-
fg: string;
|
|
59
|
-
mini: string;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Palette modeled on Claude Managed Agents' Sessions view — muted,
|
|
64
|
-
* desaturated tones that read cleanly against the cream background.
|
|
65
|
-
* `bg`/`fg` drive the in-row pill; `mini` drives the timeline block fill.
|
|
66
|
-
*/
|
|
67
|
-
const ROLE_THEMES: Record<PresentationRowKind, RoleTheme> = {
|
|
68
|
-
user: {
|
|
69
|
-
label: "User",
|
|
70
|
-
bg: "rgba(201, 112, 112, 0.18)",
|
|
71
|
-
fg: "#8B3A3A",
|
|
72
|
-
mini: "#C97070",
|
|
73
|
-
},
|
|
74
|
-
agent: {
|
|
75
|
-
label: "Agent",
|
|
76
|
-
bg: "rgba(92, 132, 195, 0.18)",
|
|
77
|
-
fg: "#2E4A7A",
|
|
78
|
-
mini: "#5C84C3",
|
|
79
|
-
},
|
|
80
|
-
"tool-group": {
|
|
81
|
-
label: "Tool",
|
|
82
|
-
bg: "rgba(138, 133, 128, 0.16)",
|
|
83
|
-
fg: "#44403C",
|
|
84
|
-
mini: "#8A8580",
|
|
85
|
-
},
|
|
86
|
-
interrupt: {
|
|
87
|
-
label: "Interrupt",
|
|
88
|
-
bg: "rgba(217, 119, 6, 0.14)",
|
|
89
|
-
fg: "#78350F",
|
|
90
|
-
mini: "#D97706",
|
|
91
|
-
},
|
|
92
|
-
model: {
|
|
93
|
-
label: "Model",
|
|
94
|
-
bg: "rgba(168, 85, 247, 0.12)",
|
|
95
|
-
fg: "#581C87",
|
|
96
|
-
mini: "#A855F7",
|
|
97
|
-
},
|
|
98
|
-
error: {
|
|
99
|
-
label: "Error",
|
|
100
|
-
bg: "rgba(197, 48, 48, 0.18)",
|
|
101
|
-
fg: "#8B1818",
|
|
102
|
-
mini: "#C53030",
|
|
103
|
-
},
|
|
104
|
-
"task-notification": {
|
|
105
|
-
label: "Task",
|
|
106
|
-
bg: "rgba(100, 116, 139, 0.12)",
|
|
107
|
-
fg: "#475569",
|
|
108
|
-
mini: "#64748B",
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
74
|
type FilterMode = "turns" | "meaningful" | "all" | PresentationRowKind;
|
|
113
75
|
|
|
114
76
|
const FILTER_MODES: { value: FilterMode; label: string }[] = [
|
|
@@ -128,8 +90,59 @@ const FILTER_MODES: { value: FilterMode; label: string }[] = [
|
|
|
128
90
|
/** Drawer width in px — reserved on the transcript's right edge when open. */
|
|
129
91
|
const DRAWER_WIDTH = 460;
|
|
130
92
|
|
|
131
|
-
|
|
132
|
-
|
|
93
|
+
type TabId = "transcript" | "team" | "debug";
|
|
94
|
+
const VALID_TABS: TabId[] = ["transcript", "team", "debug"];
|
|
95
|
+
|
|
96
|
+
export function SessionView({
|
|
97
|
+
session,
|
|
98
|
+
team,
|
|
99
|
+
teamLead,
|
|
100
|
+
}: {
|
|
101
|
+
session: SessionDetail;
|
|
102
|
+
team?: (TimelineData & { teamName: string }) | null;
|
|
103
|
+
teamLead?: { leadSessionId: string; teamName: string; agentName: string } | null;
|
|
104
|
+
}) {
|
|
105
|
+
const readHash = (): TabId => {
|
|
106
|
+
if (typeof window === "undefined") return "transcript";
|
|
107
|
+
const h = window.location.hash.replace("#", "");
|
|
108
|
+
if (VALID_TABS.includes(h as TabId)) return h as TabId;
|
|
109
|
+
return "transcript";
|
|
110
|
+
};
|
|
111
|
+
const [tab, setTabRaw] = useState<TabId>(readHash);
|
|
112
|
+
const setTab = (t: TabId) => {
|
|
113
|
+
setTabRaw(t);
|
|
114
|
+
window.history.replaceState(null, "", `#${t}`);
|
|
115
|
+
};
|
|
116
|
+
// Re-read hash on client mount and whenever the session changes (Next.js
|
|
117
|
+
// reuses this component across /sessions/[id] navigations, so useState
|
|
118
|
+
// doesn't reinitialize — the old tab would stick without this).
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setTabRaw(readHash());
|
|
121
|
+
}, [session.sessionId]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const onHash = () => {
|
|
124
|
+
const h = window.location.hash.replace("#", "");
|
|
125
|
+
if (VALID_TABS.includes(h as TabId)) setTabRaw(h as TabId);
|
|
126
|
+
};
|
|
127
|
+
window.addEventListener("hashchange", onHash);
|
|
128
|
+
return () => window.removeEventListener("hashchange", onHash);
|
|
129
|
+
}, []);
|
|
130
|
+
// Team tab — playhead (set by TeamTable as the user scrolls) and seek
|
|
131
|
+
// target (set by TeamMinimap clicks). Hoisted to session-view so the
|
|
132
|
+
// sticky-header TeamMinimap and the body's TeamTable share the same
|
|
133
|
+
// state without duplicating the minimap.
|
|
134
|
+
const [teamPlayheadMs, setTeamPlayheadMs] = useState<number | null>(null);
|
|
135
|
+
const [teamSeekTarget, setTeamSeekTarget] = useState<{
|
|
136
|
+
tsMs: number;
|
|
137
|
+
trackId?: string;
|
|
138
|
+
} | null>(null);
|
|
139
|
+
// Member track ids currently in the table's horizontal viewport, published
|
|
140
|
+
// by TeamTable on scroll. The sticky TeamMinimap uses this to mirror the
|
|
141
|
+
// table's current agents in its default (collapsed) lane set.
|
|
142
|
+
const [teamVisibleTrackIds, setTeamVisibleTrackIds] = useState<string[]>([]);
|
|
143
|
+
// When the user explicitly clicks "Show all" in the minimap we display
|
|
144
|
+
// every lane regardless of table scroll position.
|
|
145
|
+
const [teamExpanded, setTeamExpanded] = useState(false);
|
|
133
146
|
const [filter, setFilter] = useState<FilterMode>("turns");
|
|
134
147
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
135
148
|
const [selectedSubagentId, setSelectedSubagentId] = useState<string | null>(null);
|
|
@@ -164,11 +177,28 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
164
177
|
* screen real-estate once you're deep in reading. Hysteresis (60
|
|
165
178
|
* vs 80) prevents flicker at the boundary. */
|
|
166
179
|
const [collapsed, setCollapsed] = useState(false);
|
|
180
|
+
// When the user explicitly clicks the toggle, suppress auto-collapse
|
|
181
|
+
// for a short period so the scroll listener doesn't immediately undo it.
|
|
182
|
+
const manualPinRef = useRef(0);
|
|
183
|
+
const toggleCollapsed = () => {
|
|
184
|
+
setCollapsed((v) => !v);
|
|
185
|
+
manualPinRef.current = Date.now();
|
|
186
|
+
};
|
|
167
187
|
useEffect(() => {
|
|
168
188
|
const el = headerRef.current;
|
|
169
189
|
if (!el) return;
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
190
|
+
const mainEl = el.closest("main") as HTMLElement | null;
|
|
191
|
+
if (!mainEl) return;
|
|
192
|
+
// Team tab has its own fixed-height scroll container, so listening
|
|
193
|
+
// to `main` alone misses all vertical scroll when the inner table
|
|
194
|
+
// is what's moving. Pick the team table's scroll element as the
|
|
195
|
+
// scroll target when on team tab (it has data-team-scroll). Falls
|
|
196
|
+
// back to the main element for every other tab.
|
|
197
|
+
const teamScroll =
|
|
198
|
+
tab === "team"
|
|
199
|
+
? (document.querySelector("[data-team-scroll]") as HTMLElement | null)
|
|
200
|
+
: null;
|
|
201
|
+
const main = teamScroll ?? mainEl;
|
|
172
202
|
// Track scroll direction to avoid jitter. Collapse only when
|
|
173
203
|
// scrolling DOWN past a threshold; expand only at scrollTop===0.
|
|
174
204
|
// This eliminates the loop where collapse changes height → scroll
|
|
@@ -179,12 +209,15 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
179
209
|
const update = () => {
|
|
180
210
|
raf = 0;
|
|
181
211
|
if (locked) return;
|
|
212
|
+
// Respect manual pin — user clicked the toggle, don't override
|
|
213
|
+
// their choice for 2 seconds so the next scroll doesn't fight it.
|
|
214
|
+
if (Date.now() - manualPinRef.current < 2000) return;
|
|
182
215
|
const y = main.scrollTop;
|
|
183
216
|
const dir = y - lastY; // positive = scrolling down
|
|
184
217
|
lastY = y;
|
|
185
218
|
setCollapsed((prev) => {
|
|
186
|
-
// Expand:
|
|
187
|
-
if (prev &&
|
|
219
|
+
// Expand: when user scrolls UP (iOS-style reveal).
|
|
220
|
+
if (prev && dir < 0) {
|
|
188
221
|
locked = true;
|
|
189
222
|
setTimeout(() => { locked = false; }, 300);
|
|
190
223
|
return false;
|
|
@@ -207,7 +240,7 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
207
240
|
main.removeEventListener("scroll", onScroll);
|
|
208
241
|
if (raf) cancelAnimationFrame(raf);
|
|
209
242
|
};
|
|
210
|
-
}, []);
|
|
243
|
+
}, [tab]);
|
|
211
244
|
|
|
212
245
|
// Run after every render that changed scrollIntent — the DOM is now
|
|
213
246
|
// guaranteed up-to-date (including any drawer grid reflow).
|
|
@@ -227,6 +260,22 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
227
260
|
const { events, durationMs, totalUsage, model, eventCount, projectName } = session;
|
|
228
261
|
const airTimeMs = session.airTimeMs ?? durationMs;
|
|
229
262
|
|
|
263
|
+
/** Inbound `<teammate-message>` events are cross-session team traffic
|
|
264
|
+
* wrapped in a synthetic user event. On the LEAD's transcript these are
|
|
265
|
+
* protocol noise (idle notifications, task assignments) — hide them and
|
|
266
|
+
* point the user to the Team tab. On a MEMBER's transcript the teammate
|
|
267
|
+
* messages ARE the task instructions from the lead, so we keep them
|
|
268
|
+
* visible — hiding them would strip all context from the member's work. */
|
|
269
|
+
const isLead = session.isTeamLead;
|
|
270
|
+
const teammateCount = useMemo(
|
|
271
|
+
() => (isLead ? events.filter((e) => e.teammateMessage).length : 0),
|
|
272
|
+
[events, isLead],
|
|
273
|
+
);
|
|
274
|
+
const visibleEvents = useMemo(
|
|
275
|
+
() => (teammateCount === 0 ? events : events.filter((e) => !e.teammateMessage)),
|
|
276
|
+
[events, teammateCount],
|
|
277
|
+
);
|
|
278
|
+
|
|
230
279
|
// A session is "live" if its last event was within 45 seconds.
|
|
231
280
|
const isSessionLive = (() => {
|
|
232
281
|
if (!session.lastTimestamp) return false;
|
|
@@ -238,7 +287,7 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
238
287
|
const prMarkers = useMemo(() => detectPrMarkers(session), [session]);
|
|
239
288
|
|
|
240
289
|
/** Build the full presentation stream once. */
|
|
241
|
-
const allRows = useMemo(() => buildPresentation(
|
|
290
|
+
const allRows = useMemo(() => buildPresentation(visibleEvents), [visibleEvents]);
|
|
242
291
|
|
|
243
292
|
/** Collapse the presentation stream into conversational turns (user →
|
|
244
293
|
* agent loop → next user). Used by the "turns" filter mode. */
|
|
@@ -269,7 +318,7 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
269
318
|
return flattenMegaRows(megaRows, expandedTurns);
|
|
270
319
|
}
|
|
271
320
|
if (filter === "all")
|
|
272
|
-
return allRowsAsRawRows(
|
|
321
|
+
return allRowsAsRawRows(visibleEvents).map((r) => ({
|
|
273
322
|
kind: "presentation",
|
|
274
323
|
row: r,
|
|
275
324
|
}));
|
|
@@ -281,10 +330,12 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
281
330
|
return r.kind === filter;
|
|
282
331
|
})
|
|
283
332
|
.map((r) => ({ kind: "presentation", row: r }));
|
|
284
|
-
}, [filter, megaRows, expandedTurns, allRows,
|
|
333
|
+
}, [filter, megaRows, expandedTurns, allRows, visibleEvents]);
|
|
285
334
|
|
|
286
335
|
const selectedEvent =
|
|
287
|
-
selectedIndex !== null
|
|
336
|
+
selectedIndex !== null
|
|
337
|
+
? (visibleEvents.find((e) => e.index === selectedIndex) ?? null)
|
|
338
|
+
: null;
|
|
288
339
|
const selectedRow =
|
|
289
340
|
selectedIndex !== null
|
|
290
341
|
? (allRows.find((r) => rowPrimaryIndex(r) === selectedIndex) ?? null)
|
|
@@ -331,9 +382,104 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
331
382
|
padding: "18px 40px 0",
|
|
332
383
|
}}
|
|
333
384
|
>
|
|
334
|
-
{/*
|
|
335
|
-
|
|
336
|
-
|
|
385
|
+
{/* Always-visible compact bar: breadcrumb + tabs + toggle — stays
|
|
386
|
+
accessible even when the header body is collapsed on scroll. */}
|
|
387
|
+
<div
|
|
388
|
+
style={{
|
|
389
|
+
display: "flex",
|
|
390
|
+
alignItems: "center",
|
|
391
|
+
gap: 12,
|
|
392
|
+
marginBottom: collapsed ? 6 : 6,
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
<div
|
|
396
|
+
style={{
|
|
397
|
+
fontSize: 12,
|
|
398
|
+
color: "var(--af-text-tertiary)",
|
|
399
|
+
whiteSpace: "nowrap",
|
|
400
|
+
}}
|
|
401
|
+
>
|
|
402
|
+
<Link
|
|
403
|
+
href="/sessions"
|
|
404
|
+
style={{
|
|
405
|
+
color: "var(--af-text-tertiary)",
|
|
406
|
+
textDecoration: "none",
|
|
407
|
+
display: "inline-flex",
|
|
408
|
+
alignItems: "center",
|
|
409
|
+
gap: 4,
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
<ArrowLeft size={12} /> Sessions
|
|
413
|
+
</Link>
|
|
414
|
+
<span style={{ margin: "0 8px" }}>/</span>
|
|
415
|
+
<span style={{ fontFamily: "var(--font-mono)" }}>sesn_{shortId(session.id)}</span>
|
|
416
|
+
{teamLead && (
|
|
417
|
+
<>
|
|
418
|
+
<span style={{ margin: "0 8px" }}>·</span>
|
|
419
|
+
<Link
|
|
420
|
+
href={`/sessions/${teamLead.leadSessionId}#team`}
|
|
421
|
+
style={{
|
|
422
|
+
color: "var(--af-accent)",
|
|
423
|
+
textDecoration: "none",
|
|
424
|
+
display: "inline-flex",
|
|
425
|
+
alignItems: "center",
|
|
426
|
+
gap: 4,
|
|
427
|
+
fontWeight: 500,
|
|
428
|
+
}}
|
|
429
|
+
title={`View team lead for ${teamLead.teamName}`}
|
|
430
|
+
>
|
|
431
|
+
{teamLead.agentName} in {teamLead.teamName} ↗
|
|
432
|
+
</Link>
|
|
433
|
+
</>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
<div className="af-tabs" style={{ flexShrink: 0, marginLeft: "auto" }}>
|
|
437
|
+
<button
|
|
438
|
+
className={`af-tab-btn ${tab === "transcript" ? "active" : ""}`}
|
|
439
|
+
onClick={() => setTab("transcript")}
|
|
440
|
+
>
|
|
441
|
+
Transcript
|
|
442
|
+
</button>
|
|
443
|
+
{team && (
|
|
444
|
+
<button
|
|
445
|
+
className={`af-tab-btn ${tab === "team" ? "active" : ""}`}
|
|
446
|
+
onClick={() => setTab("team")}
|
|
447
|
+
>
|
|
448
|
+
Team
|
|
449
|
+
</button>
|
|
450
|
+
)}
|
|
451
|
+
<button
|
|
452
|
+
className={`af-tab-btn ${tab === "debug" ? "active" : ""}`}
|
|
453
|
+
onClick={() => setTab("debug")}
|
|
454
|
+
>
|
|
455
|
+
Debug
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
<button
|
|
459
|
+
onClick={toggleCollapsed}
|
|
460
|
+
title={collapsed ? "Show session details" : "Hide session details"}
|
|
461
|
+
style={{
|
|
462
|
+
background: "var(--af-surface-hover)",
|
|
463
|
+
border: "1px solid var(--af-border-subtle)",
|
|
464
|
+
borderRadius: 4,
|
|
465
|
+
width: 24,
|
|
466
|
+
height: 24,
|
|
467
|
+
display: "flex",
|
|
468
|
+
alignItems: "center",
|
|
469
|
+
justifyContent: "center",
|
|
470
|
+
cursor: "pointer",
|
|
471
|
+
color: "var(--af-text-secondary)",
|
|
472
|
+
flexShrink: 0,
|
|
473
|
+
transition: "transform 0.2s ease",
|
|
474
|
+
transform: collapsed ? "rotate(180deg)" : "rotate(0deg)",
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
<ChevronUp size={14} />
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
{/* Collapsible block — title + meta-stats + filter toolbar.
|
|
482
|
+
Hidden on scroll-down so the minimap gets more space. */}
|
|
337
483
|
<div
|
|
338
484
|
style={{
|
|
339
485
|
maxHeight: collapsed ? 0 : 500,
|
|
@@ -345,29 +491,6 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
345
491
|
pointerEvents: collapsed ? "none" : "auto",
|
|
346
492
|
}}
|
|
347
493
|
>
|
|
348
|
-
{/* Breadcrumb */}
|
|
349
|
-
<div
|
|
350
|
-
style={{
|
|
351
|
-
fontSize: 12,
|
|
352
|
-
color: "var(--af-text-tertiary)",
|
|
353
|
-
marginBottom: 6,
|
|
354
|
-
}}
|
|
355
|
-
>
|
|
356
|
-
<Link
|
|
357
|
-
href="/sessions"
|
|
358
|
-
style={{
|
|
359
|
-
color: "var(--af-text-tertiary)",
|
|
360
|
-
textDecoration: "none",
|
|
361
|
-
display: "inline-flex",
|
|
362
|
-
alignItems: "center",
|
|
363
|
-
gap: 4,
|
|
364
|
-
}}
|
|
365
|
-
>
|
|
366
|
-
<ArrowLeft size={12} /> Sessions
|
|
367
|
-
</Link>
|
|
368
|
-
<span style={{ margin: "0 8px" }}>/</span>
|
|
369
|
-
<span style={{ fontFamily: "var(--font-mono)" }}>sesn_{shortId(session.id)}</span>
|
|
370
|
-
</div>
|
|
371
494
|
|
|
372
495
|
{/* Single-line compact header — title + inline dot-separated stats */}
|
|
373
496
|
<div
|
|
@@ -453,7 +576,7 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
453
576
|
</span>
|
|
454
577
|
</div>
|
|
455
578
|
|
|
456
|
-
{/*
|
|
579
|
+
{/* Toolbar (filter, copy, ask-claude) */}
|
|
457
580
|
<div
|
|
458
581
|
className="flex items-center"
|
|
459
582
|
style={{
|
|
@@ -461,29 +584,6 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
461
584
|
paddingBottom: 8,
|
|
462
585
|
}}
|
|
463
586
|
>
|
|
464
|
-
<div className="af-tabs">
|
|
465
|
-
<button
|
|
466
|
-
className={`af-tab-btn ${tab === "transcript" ? "active" : ""}`}
|
|
467
|
-
onClick={() => setTab("transcript")}
|
|
468
|
-
>
|
|
469
|
-
Transcript
|
|
470
|
-
</button>
|
|
471
|
-
<button
|
|
472
|
-
className={`af-tab-btn ${tab === "debug" ? "active" : ""}`}
|
|
473
|
-
onClick={() => setTab("debug")}
|
|
474
|
-
>
|
|
475
|
-
Debug
|
|
476
|
-
</button>
|
|
477
|
-
</div>
|
|
478
|
-
|
|
479
|
-
<div
|
|
480
|
-
style={{
|
|
481
|
-
width: 1,
|
|
482
|
-
height: 20,
|
|
483
|
-
background: "var(--af-border-subtle)",
|
|
484
|
-
}}
|
|
485
|
-
/>
|
|
486
|
-
|
|
487
587
|
<select
|
|
488
588
|
value={filter}
|
|
489
589
|
onChange={(e) =>
|
|
@@ -508,7 +608,7 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
508
608
|
{filter === "turns"
|
|
509
609
|
? `${megaRows.filter((m) => m.kind === "turn").length} turns · ${allRows.length} actions`
|
|
510
610
|
: filter === "meaningful"
|
|
511
|
-
? `${allRows.length} rows (of ${
|
|
611
|
+
? `${allRows.length} rows (of ${visibleEvents.length} raw events)`
|
|
512
612
|
: `${displayRows.length} rows`}
|
|
513
613
|
</span>
|
|
514
614
|
|
|
@@ -553,23 +653,37 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
553
653
|
transition: "padding-bottom 0.2s ease",
|
|
554
654
|
}}
|
|
555
655
|
>
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
656
|
+
{tab === "team" && team ? (
|
|
657
|
+
<TeamMinimap
|
|
658
|
+
data={team}
|
|
659
|
+
playheadMs={teamPlayheadMs}
|
|
660
|
+
onSeek={(tsMs, trackId) => {
|
|
661
|
+
setTeamSeekTarget({ tsMs, trackId });
|
|
662
|
+
if (teamExpanded) setTeamExpanded(false);
|
|
663
|
+
}}
|
|
664
|
+
expanded={teamExpanded}
|
|
665
|
+
onToggleExpanded={() => setTeamExpanded((v) => !v)}
|
|
666
|
+
visibleTrackIds={teamVisibleTrackIds}
|
|
667
|
+
/>
|
|
668
|
+
) : (
|
|
669
|
+
<Minimap
|
|
670
|
+
displayRows={displayRows}
|
|
671
|
+
durationMs={durationMs ?? 0}
|
|
672
|
+
selectedIndex={selectedIndex}
|
|
673
|
+
onSelect={scrollToIndex}
|
|
674
|
+
headerOffset={headerH}
|
|
675
|
+
subagents={session.subagents}
|
|
676
|
+
collapsed={collapsed}
|
|
677
|
+
prMarkers={prMarkers}
|
|
678
|
+
selectedSubagentId={selectedSubagentId}
|
|
679
|
+
onSelectSubagent={(id) => {
|
|
680
|
+
setSelectedSubagentId(id);
|
|
681
|
+
// Close the event drawer so we don't have two drawers fighting
|
|
682
|
+
// for the same right-side real estate.
|
|
683
|
+
if (id) setSelectedIndex(null);
|
|
684
|
+
}}
|
|
685
|
+
/>
|
|
686
|
+
)}
|
|
573
687
|
</div>
|
|
574
688
|
</div>
|
|
575
689
|
|
|
@@ -584,16 +698,59 @@ export function SessionView({ session }: { session: SessionDetail }) {
|
|
|
584
698
|
transition: "padding-right 0.15s ease",
|
|
585
699
|
}}
|
|
586
700
|
>
|
|
587
|
-
{tab === "
|
|
588
|
-
<
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
isSessionLive={isSessionLive}
|
|
701
|
+
{tab === "team" && team ? (
|
|
702
|
+
<TeamTabClient
|
|
703
|
+
initial={team}
|
|
704
|
+
teamName={team.teamName}
|
|
705
|
+
playheadMs={teamPlayheadMs}
|
|
706
|
+
onPlayheadChange={setTeamPlayheadMs}
|
|
707
|
+
onVisibleTrackIdsChange={setTeamVisibleTrackIds}
|
|
708
|
+
seekTarget={teamSeekTarget}
|
|
596
709
|
/>
|
|
710
|
+
) : tab === "transcript" ? (
|
|
711
|
+
<>
|
|
712
|
+
{teammateCount > 0 && (
|
|
713
|
+
<div
|
|
714
|
+
style={{
|
|
715
|
+
fontSize: 11,
|
|
716
|
+
color: "var(--af-text-tertiary, #888)",
|
|
717
|
+
background: "var(--af-surface-subtle, rgba(255,255,255,0.04))",
|
|
718
|
+
border: "1px solid var(--af-border-subtle, rgba(255,255,255,0.08))",
|
|
719
|
+
borderRadius: 6,
|
|
720
|
+
padding: "6px 10px",
|
|
721
|
+
marginBottom: 8,
|
|
722
|
+
}}
|
|
723
|
+
>
|
|
724
|
+
{teammateCount} inbound team message
|
|
725
|
+
{teammateCount === 1 ? "" : "s"} hidden —{" "}
|
|
726
|
+
<button
|
|
727
|
+
onClick={() => setTab("team")}
|
|
728
|
+
style={{
|
|
729
|
+
background: "none",
|
|
730
|
+
border: "none",
|
|
731
|
+
color: "var(--af-accent)",
|
|
732
|
+
cursor: "pointer",
|
|
733
|
+
padding: 0,
|
|
734
|
+
font: "inherit",
|
|
735
|
+
textDecoration: "underline",
|
|
736
|
+
}}
|
|
737
|
+
>
|
|
738
|
+
open the Team tab
|
|
739
|
+
</button>{" "}
|
|
740
|
+
to see them.
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
<TranscriptList
|
|
744
|
+
displayRows={displayRows}
|
|
745
|
+
rowRefs={rowRefs}
|
|
746
|
+
selectedIndex={selectedIndex}
|
|
747
|
+
onSelect={setSelectedIndex}
|
|
748
|
+
onToggleTurn={toggleTurn}
|
|
749
|
+
stickyOffset={headerH + 16}
|
|
750
|
+
isSessionLive={isSessionLive}
|
|
751
|
+
team={team}
|
|
752
|
+
/>
|
|
753
|
+
</>
|
|
597
754
|
) : (
|
|
598
755
|
<DebugList events={events} />
|
|
599
756
|
)}
|
|
@@ -742,53 +899,6 @@ function flattenMegaRows(megaRows: MegaRow[], expanded: Set<number>): DisplayRow
|
|
|
742
899
|
return out;
|
|
743
900
|
}
|
|
744
901
|
|
|
745
|
-
function rowPreview(r: PresentationRow): string {
|
|
746
|
-
switch (r.kind) {
|
|
747
|
-
case "user":
|
|
748
|
-
return r.displayPreview ?? r.event.preview;
|
|
749
|
-
case "agent":
|
|
750
|
-
return r.event.preview;
|
|
751
|
-
case "tool-group":
|
|
752
|
-
return formatToolSummary(r.toolNames);
|
|
753
|
-
case "interrupt":
|
|
754
|
-
return "Interrupted";
|
|
755
|
-
case "model":
|
|
756
|
-
return tokenSummaryLine(r.event.usage);
|
|
757
|
-
case "error":
|
|
758
|
-
return r.message;
|
|
759
|
-
case "task-notification": {
|
|
760
|
-
const icon =
|
|
761
|
-
r.status === "success"
|
|
762
|
-
? "✓"
|
|
763
|
-
: r.status === "failed"
|
|
764
|
-
? "✗"
|
|
765
|
-
: r.status === "running"
|
|
766
|
-
? "…"
|
|
767
|
-
: "•";
|
|
768
|
-
return `${icon} ${r.summary}`;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/** Render a compact "Bash ×3 · Read ×2 · Grep · Edit" style summary.
|
|
774
|
-
* Truncates after 4 unique tools with "+N more". */
|
|
775
|
-
function formatToolSummary(toolNames: { name: string; count: number }[]): string {
|
|
776
|
-
const MAX = 4;
|
|
777
|
-
const shown = toolNames.slice(0, MAX);
|
|
778
|
-
const overflow = toolNames.length - MAX;
|
|
779
|
-
const parts = shown.map((t) => {
|
|
780
|
-
const display = shortenToolName(t.name);
|
|
781
|
-
return t.count > 1 ? `${display} ×${t.count}` : display;
|
|
782
|
-
});
|
|
783
|
-
if (overflow > 0) parts.push(`+${overflow} more`);
|
|
784
|
-
return parts.join(" · ");
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function tokenSummaryLine(u: SessionEvent["usage"]): string {
|
|
788
|
-
if (!u) return "0 input · 0 output · 0 cache read · 0 cache write";
|
|
789
|
-
return `${u.input} input · ${u.output} output · ${u.cacheRead} cache read · ${u.cacheWrite} cache write`;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
902
|
/** Fallback for "All events" filter mode — wraps each raw event in a
|
|
793
903
|
* synthetic PresentationRow so the transcript component can render
|
|
794
904
|
* everything (attachments, meta, thinking, tool_result) without
|
|
@@ -2203,6 +2313,7 @@ function TranscriptList({
|
|
|
2203
2313
|
onToggleTurn,
|
|
2204
2314
|
stickyOffset,
|
|
2205
2315
|
isSessionLive,
|
|
2316
|
+
team,
|
|
2206
2317
|
}: {
|
|
2207
2318
|
displayRows: DisplayRow[];
|
|
2208
2319
|
rowRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
|
|
@@ -2211,6 +2322,7 @@ function TranscriptList({
|
|
|
2211
2322
|
onToggleTurn: (firstPrimaryIndex: number) => void;
|
|
2212
2323
|
stickyOffset: number;
|
|
2213
2324
|
isSessionLive?: boolean;
|
|
2325
|
+
team?: (TimelineData & { teamName: string }) | null;
|
|
2214
2326
|
}) {
|
|
2215
2327
|
// Find the last collapsed turn index so we can mark it as in-progress
|
|
2216
2328
|
// when the session is live.
|
|
@@ -2238,6 +2350,7 @@ function TranscriptList({
|
|
|
2238
2350
|
stickyOffset={stickyOffset}
|
|
2239
2351
|
onClick={() => onToggleTurn(idx)}
|
|
2240
2352
|
inProgress={i === lastCollapsedTurnIdx}
|
|
2353
|
+
team={team}
|
|
2241
2354
|
refCb={(el) => {
|
|
2242
2355
|
rowRefs.current[idx] = el;
|
|
2243
2356
|
}}
|
|
@@ -2326,17 +2439,13 @@ function IdleDivider({ gapMs }: { gapMs: number }) {
|
|
|
2326
2439
|
/* 3. Final agent preview (answer / conclusion) */
|
|
2327
2440
|
/* ------------------------------------------------------------------ */
|
|
2328
2441
|
|
|
2329
|
-
/** Max number of steps shown inline in a collapsed turn. Larger turns are
|
|
2330
|
-
* truncated with a "+N more" line; the user can click to expand the turn
|
|
2331
|
-
* to see everything. */
|
|
2332
|
-
const MAX_INLINE_STEPS = 12;
|
|
2333
|
-
|
|
2334
2442
|
function CollapsedTurnRow({
|
|
2335
2443
|
turn,
|
|
2336
2444
|
onClick,
|
|
2337
2445
|
refCb,
|
|
2338
2446
|
stickyOffset,
|
|
2339
2447
|
inProgress,
|
|
2448
|
+
team,
|
|
2340
2449
|
}: {
|
|
2341
2450
|
turn: TurnMegaRow;
|
|
2342
2451
|
/** Fires when the user wants to fully expand this turn into the
|
|
@@ -2346,6 +2455,7 @@ function CollapsedTurnRow({
|
|
|
2346
2455
|
refCb: (el: HTMLDivElement | null) => void;
|
|
2347
2456
|
stickyOffset: number;
|
|
2348
2457
|
inProgress?: boolean;
|
|
2458
|
+
team?: (TimelineData & { teamName: string }) | null;
|
|
2349
2459
|
}) {
|
|
2350
2460
|
const theme = ROLE_THEMES.agent;
|
|
2351
2461
|
const s = turn.summary;
|
|
@@ -2387,7 +2497,7 @@ function CollapsedTurnRow({
|
|
|
2387
2497
|
data-sl-toffset={turn.tOffsetMs ?? 0}
|
|
2388
2498
|
style={{
|
|
2389
2499
|
display: "grid",
|
|
2390
|
-
gridTemplateColumns: "20px
|
|
2500
|
+
gridTemplateColumns: "20px 84px 1fr auto auto",
|
|
2391
2501
|
columnGap: 12,
|
|
2392
2502
|
alignItems: "start",
|
|
2393
2503
|
padding: "14px 12px 0 12px",
|
|
@@ -2441,7 +2551,7 @@ function CollapsedTurnRow({
|
|
|
2441
2551
|
|
|
2442
2552
|
{/* Stats line */}
|
|
2443
2553
|
<div onClick={stop}>
|
|
2444
|
-
<TurnStatsLine summary={s} durationMs={turn.durationMs} rows={turn.rows} />
|
|
2554
|
+
<TurnStatsLine summary={s} durationMs={turn.durationMs} rows={turn.rows} team={team} />
|
|
2445
2555
|
</div>
|
|
2446
2556
|
|
|
2447
2557
|
{/* 2. Steps list — each middle row as a compact bullet */}
|
|
@@ -2684,10 +2794,12 @@ function TurnStatsLine({
|
|
|
2684
2794
|
summary,
|
|
2685
2795
|
durationMs,
|
|
2686
2796
|
rows,
|
|
2797
|
+
team,
|
|
2687
2798
|
}: {
|
|
2688
2799
|
summary: TurnSummary;
|
|
2689
2800
|
durationMs?: number;
|
|
2690
2801
|
rows: PresentationRow[];
|
|
2802
|
+
team?: (TimelineData & { teamName: string }) | null;
|
|
2691
2803
|
}) {
|
|
2692
2804
|
// Aggregate tool calls into activity categories.
|
|
2693
2805
|
let editCount = 0;
|
|
@@ -2699,6 +2811,7 @@ function TurnStatsLine({
|
|
|
2699
2811
|
let agentCount = 0;
|
|
2700
2812
|
let otherToolCount = 0;
|
|
2701
2813
|
let lastFilePath: string | undefined;
|
|
2814
|
+
const dispatchedNames: string[] = [];
|
|
2702
2815
|
|
|
2703
2816
|
for (const r of rows) {
|
|
2704
2817
|
if (r.kind !== "tool-group") continue;
|
|
@@ -2734,9 +2847,12 @@ function TurnStatsLine({
|
|
|
2734
2847
|
case "Glob":
|
|
2735
2848
|
globCount++;
|
|
2736
2849
|
break;
|
|
2737
|
-
case "Agent":
|
|
2850
|
+
case "Agent": {
|
|
2738
2851
|
agentCount++;
|
|
2852
|
+
const agentName = typeof input?.name === "string" ? input.name : undefined;
|
|
2853
|
+
if (agentName) dispatchedNames.push(agentName);
|
|
2739
2854
|
break;
|
|
2855
|
+
}
|
|
2740
2856
|
default:
|
|
2741
2857
|
otherToolCount++;
|
|
2742
2858
|
break;
|
|
@@ -2778,11 +2894,58 @@ function TurnStatsLine({
|
|
|
2778
2894
|
);
|
|
2779
2895
|
}
|
|
2780
2896
|
if (agentCount > 0) {
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2897
|
+
// When team data is available and we extracted agent names from the
|
|
2898
|
+
// tool inputs, render clickable member chips instead of a plain count.
|
|
2899
|
+
const nameToSession = team
|
|
2900
|
+
? new Map(team.tracks.filter((t) => !t.isLead).map((t) => [t.label, t.id]))
|
|
2901
|
+
: null;
|
|
2902
|
+
if (dispatchedNames.length > 0 && nameToSession && nameToSession.size > 0) {
|
|
2903
|
+
phrases.push(
|
|
2904
|
+
<span key="agent" style={{ display: "inline-flex", alignItems: "center", gap: 4, flexWrap: "wrap" }}>
|
|
2905
|
+
dispatched{" "}
|
|
2906
|
+
{dispatchedNames.map((n, i) => {
|
|
2907
|
+
const sid = nameToSession.get(n);
|
|
2908
|
+
return sid ? (
|
|
2909
|
+
<Link
|
|
2910
|
+
key={i}
|
|
2911
|
+
href={`/sessions/${sid}`}
|
|
2912
|
+
style={{
|
|
2913
|
+
fontSize: 10,
|
|
2914
|
+
padding: "1px 7px",
|
|
2915
|
+
background: "var(--af-surface-hover)",
|
|
2916
|
+
border: "1px solid var(--af-border-subtle)",
|
|
2917
|
+
borderRadius: 10,
|
|
2918
|
+
color: "var(--af-text-secondary)",
|
|
2919
|
+
textDecoration: "none",
|
|
2920
|
+
fontWeight: 600,
|
|
2921
|
+
}}
|
|
2922
|
+
>
|
|
2923
|
+
{n}
|
|
2924
|
+
</Link>
|
|
2925
|
+
) : (
|
|
2926
|
+
<span
|
|
2927
|
+
key={i}
|
|
2928
|
+
style={{
|
|
2929
|
+
fontSize: 10,
|
|
2930
|
+
padding: "1px 7px",
|
|
2931
|
+
background: "var(--af-surface-hover)",
|
|
2932
|
+
borderRadius: 10,
|
|
2933
|
+
color: "var(--af-text-tertiary)",
|
|
2934
|
+
}}
|
|
2935
|
+
>
|
|
2936
|
+
{n}
|
|
2937
|
+
</span>
|
|
2938
|
+
);
|
|
2939
|
+
})}
|
|
2940
|
+
</span>,
|
|
2941
|
+
);
|
|
2942
|
+
} else {
|
|
2943
|
+
phrases.push(
|
|
2944
|
+
<span key="agent">
|
|
2945
|
+
dispatched <b style={{ fontWeight: 600 }}>{agentCount}</b> sub-agent{agentCount === 1 ? "" : "s"}
|
|
2946
|
+
</span>,
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2786
2949
|
}
|
|
2787
2950
|
if (otherToolCount > 0 && phrases.length === 0) {
|
|
2788
2951
|
phrases.push(
|
|
@@ -2864,125 +3027,6 @@ function TurnStatsLine({
|
|
|
2864
3027
|
);
|
|
2865
3028
|
}
|
|
2866
3029
|
|
|
2867
|
-
/** Bulleted list of everything that happened in the middle of a turn.
|
|
2868
|
-
* Each row is a one-line compact entry with role + preview. Capped at
|
|
2869
|
-
* MAX_INLINE_STEPS items with an overflow indicator. */
|
|
2870
|
-
function TurnStepsList({ rows }: { rows: PresentationRow[] }) {
|
|
2871
|
-
const overflow = Math.max(0, rows.length - MAX_INLINE_STEPS);
|
|
2872
|
-
const shown = overflow > 0 ? rows.slice(-MAX_INLINE_STEPS) : rows;
|
|
2873
|
-
|
|
2874
|
-
return (
|
|
2875
|
-
<div
|
|
2876
|
-
style={{
|
|
2877
|
-
display: "flex",
|
|
2878
|
-
flexDirection: "column",
|
|
2879
|
-
gap: 2,
|
|
2880
|
-
paddingLeft: 2,
|
|
2881
|
-
borderLeft: "1px solid var(--af-border-subtle)",
|
|
2882
|
-
paddingBlock: 2,
|
|
2883
|
-
}}
|
|
2884
|
-
>
|
|
2885
|
-
{overflow > 0 && (
|
|
2886
|
-
<div
|
|
2887
|
-
style={{
|
|
2888
|
-
fontSize: 11,
|
|
2889
|
-
color: "var(--af-text-tertiary)",
|
|
2890
|
-
fontStyle: "italic",
|
|
2891
|
-
paddingLeft: 12,
|
|
2892
|
-
paddingBottom: 2,
|
|
2893
|
-
}}
|
|
2894
|
-
>
|
|
2895
|
-
{overflow} earlier step{overflow === 1 ? "" : "s"} …
|
|
2896
|
-
</div>
|
|
2897
|
-
)}
|
|
2898
|
-
{shown.map((r, i) => (
|
|
2899
|
-
<TurnStepLine key={i} row={r} />
|
|
2900
|
-
))}
|
|
2901
|
-
</div>
|
|
2902
|
-
);
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
/** One step in the middle list. Renders a small role marker + the preview
|
|
2906
|
-
* for that row, truncated to one line. */
|
|
2907
|
-
function TurnStepLine({ row }: { row: PresentationRow }) {
|
|
2908
|
-
const theme = ROLE_THEMES[row.kind];
|
|
2909
|
-
const label = theme.label;
|
|
2910
|
-
const preview = rowPreview(row);
|
|
2911
|
-
return (
|
|
2912
|
-
<div
|
|
2913
|
-
style={{
|
|
2914
|
-
display: "grid",
|
|
2915
|
-
gridTemplateColumns: "46px 1fr",
|
|
2916
|
-
columnGap: 8,
|
|
2917
|
-
fontSize: 12,
|
|
2918
|
-
lineHeight: 1.45,
|
|
2919
|
-
color: row.kind === "error" ? "var(--af-danger)" : "var(--af-text-secondary)",
|
|
2920
|
-
}}
|
|
2921
|
-
>
|
|
2922
|
-
<span
|
|
2923
|
-
style={{
|
|
2924
|
-
fontSize: 10,
|
|
2925
|
-
fontWeight: 600,
|
|
2926
|
-
background: theme.bg,
|
|
2927
|
-
color: theme.fg,
|
|
2928
|
-
padding: "1px 6px",
|
|
2929
|
-
borderRadius: 3,
|
|
2930
|
-
textAlign: "center",
|
|
2931
|
-
justifySelf: "start",
|
|
2932
|
-
maxWidth: "100%",
|
|
2933
|
-
}}
|
|
2934
|
-
>
|
|
2935
|
-
{label}
|
|
2936
|
-
</span>
|
|
2937
|
-
<span
|
|
2938
|
-
style={{
|
|
2939
|
-
overflow: "hidden",
|
|
2940
|
-
textOverflow: "ellipsis",
|
|
2941
|
-
whiteSpace: "nowrap",
|
|
2942
|
-
minWidth: 0,
|
|
2943
|
-
}}
|
|
2944
|
-
>
|
|
2945
|
-
{row.kind === "tool-group" ? (
|
|
2946
|
-
<span>
|
|
2947
|
-
{row.toolNames.slice(0, 4).map((t, i) => (
|
|
2948
|
-
<span key={t.name}>
|
|
2949
|
-
{i > 0 && (
|
|
2950
|
-
<span style={{ color: "var(--af-text-tertiary)", margin: "0 5px" }}>·</span>
|
|
2951
|
-
)}
|
|
2952
|
-
<b style={{ fontWeight: 600 }}>{shortenToolName(t.name)}</b>
|
|
2953
|
-
{t.count > 1 && (
|
|
2954
|
-
<span
|
|
2955
|
-
style={{
|
|
2956
|
-
color: "var(--af-text-tertiary)",
|
|
2957
|
-
fontFamily: "var(--font-mono)",
|
|
2958
|
-
marginLeft: 3,
|
|
2959
|
-
}}
|
|
2960
|
-
>
|
|
2961
|
-
×{t.count}
|
|
2962
|
-
</span>
|
|
2963
|
-
)}
|
|
2964
|
-
</span>
|
|
2965
|
-
))}
|
|
2966
|
-
{row.toolNames.length > 4 && (
|
|
2967
|
-
<span
|
|
2968
|
-
style={{
|
|
2969
|
-
color: "var(--af-text-tertiary)",
|
|
2970
|
-
marginLeft: 6,
|
|
2971
|
-
fontSize: 10,
|
|
2972
|
-
}}
|
|
2973
|
-
>
|
|
2974
|
-
+{row.toolNames.length - 4}
|
|
2975
|
-
</span>
|
|
2976
|
-
)}
|
|
2977
|
-
</span>
|
|
2978
|
-
) : (
|
|
2979
|
-
preview
|
|
2980
|
-
)}
|
|
2981
|
-
</span>
|
|
2982
|
-
</div>
|
|
2983
|
-
);
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
3030
|
function ExpandedTurnHeader({ turn, onClick }: { turn: TurnMegaRow; onClick: () => void }) {
|
|
2987
3031
|
const s = turn.summary;
|
|
2988
3032
|
return (
|
|
@@ -3082,6 +3126,8 @@ function TranscriptRow({
|
|
|
3082
3126
|
const event = row.kind === "tool-group" ? row.events[0] : row.event;
|
|
3083
3127
|
const usage = event.usage;
|
|
3084
3128
|
const hasUsage = row.kind === "agent" && usage && (usage.input > 0 || usage.output > 0);
|
|
3129
|
+
const isTeammateMsg = row.kind === "user" && !!row.event.teammateMessage;
|
|
3130
|
+
const roleLabel = isTeammateMsg ? "Team Lead" : theme.label;
|
|
3085
3131
|
|
|
3086
3132
|
const preview = rowPreview(row);
|
|
3087
3133
|
|
|
@@ -3096,7 +3142,7 @@ function TranscriptRow({
|
|
|
3096
3142
|
// Empty 20px prefix column keeps the role pill at the same x-offset
|
|
3097
3143
|
// as collapsed turn rows (which have a chevron there). Ensures the
|
|
3098
3144
|
// User/Agent/Tool tags align vertically in the transcript.
|
|
3099
|
-
gridTemplateColumns: "20px
|
|
3145
|
+
gridTemplateColumns: "20px 84px 1fr auto auto",
|
|
3100
3146
|
gap: 14,
|
|
3101
3147
|
alignItems: "center",
|
|
3102
3148
|
padding: "11px 12px",
|
|
@@ -3134,7 +3180,7 @@ function TranscriptRow({
|
|
|
3134
3180
|
color: theme.fg,
|
|
3135
3181
|
}}
|
|
3136
3182
|
>
|
|
3137
|
-
{
|
|
3183
|
+
{roleLabel}
|
|
3138
3184
|
</span>
|
|
3139
3185
|
|
|
3140
3186
|
<span
|
|
@@ -4085,473 +4131,6 @@ function DrawerContent({ event, row }: { event: SessionEvent; row: PresentationR
|
|
|
4085
4131
|
</div>
|
|
4086
4132
|
);
|
|
4087
4133
|
}
|
|
4088
|
-
|
|
4089
|
-
/* ------------------------------------------------------------------ */
|
|
4090
|
-
/* ToolUseCard — pretty rendering of tool_use blocks */
|
|
4091
|
-
/* */
|
|
4092
|
-
/* Claude Code emits tool calls with varying input schemas. Raw JSON */
|
|
4093
|
-
/* dumps are unreadable; instead we dispatch on tool name and render */
|
|
4094
|
-
/* a human-friendly card tailored to each common tool. */
|
|
4095
|
-
/* ------------------------------------------------------------------ */
|
|
4096
|
-
|
|
4097
|
-
type ToolUseInput = Record<string, unknown> | unknown;
|
|
4098
|
-
|
|
4099
|
-
function shortenToolName(name: string): string {
|
|
4100
|
-
// mcp__plugin_linear_linear__get_issue → linear.get_issue
|
|
4101
|
-
const m = name.match(/^mcp__(?:plugin_)?([^_]+)_(?:\1_)?(.+)$/);
|
|
4102
|
-
if (m) return `${m[1]}.${m[2]}`;
|
|
4103
|
-
// mcp__claude_ai_Gmail__search_threads → gmail.search_threads
|
|
4104
|
-
const m2 = name.match(/^mcp__claude_ai_([^_]+)__(.+)$/);
|
|
4105
|
-
if (m2) return `${m2[1].toLowerCase()}.${m2[2]}`;
|
|
4106
|
-
return name;
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
|
-
/** Split an absolute path into (dir, filename) with an abbreviated dir
|
|
4110
|
-
* (last 3 segments max) for compact display. */
|
|
4111
|
-
function splitPath(p: string): { dir: string; file: string } {
|
|
4112
|
-
const parts = p.split("/");
|
|
4113
|
-
const file = parts[parts.length - 1] ?? p;
|
|
4114
|
-
const dirParts = parts.slice(0, -1).filter(Boolean);
|
|
4115
|
-
let dir = dirParts.join("/");
|
|
4116
|
-
if (dirParts.length > 4) {
|
|
4117
|
-
dir = "…/" + dirParts.slice(-3).join("/");
|
|
4118
|
-
}
|
|
4119
|
-
return { dir, file };
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
function PathLabel({ path }: { path: string }) {
|
|
4123
|
-
const { dir, file } = splitPath(path);
|
|
4124
|
-
return (
|
|
4125
|
-
<code
|
|
4126
|
-
style={{
|
|
4127
|
-
fontSize: 12,
|
|
4128
|
-
background: "var(--af-border-subtle)",
|
|
4129
|
-
padding: "2px 8px",
|
|
4130
|
-
borderRadius: 4,
|
|
4131
|
-
fontFamily: "var(--font-mono)",
|
|
4132
|
-
color: "var(--af-text)",
|
|
4133
|
-
}}
|
|
4134
|
-
>
|
|
4135
|
-
{dir && <span style={{ color: "var(--af-text-tertiary)" }}>{dir}/</span>}
|
|
4136
|
-
<b style={{ fontWeight: 600 }}>{file}</b>
|
|
4137
|
-
</code>
|
|
4138
|
-
);
|
|
4139
|
-
}
|
|
4140
|
-
|
|
4141
|
-
function ToolCardShell({
|
|
4142
|
-
icon,
|
|
4143
|
-
label,
|
|
4144
|
-
children,
|
|
4145
|
-
}: {
|
|
4146
|
-
icon: React.ReactNode;
|
|
4147
|
-
label: React.ReactNode;
|
|
4148
|
-
children?: React.ReactNode;
|
|
4149
|
-
}) {
|
|
4150
|
-
return (
|
|
4151
|
-
<div
|
|
4152
|
-
style={{
|
|
4153
|
-
border: "1px solid var(--af-border-subtle)",
|
|
4154
|
-
borderRadius: 8,
|
|
4155
|
-
background: "var(--background)",
|
|
4156
|
-
overflow: "hidden",
|
|
4157
|
-
}}
|
|
4158
|
-
>
|
|
4159
|
-
<div
|
|
4160
|
-
style={{
|
|
4161
|
-
padding: "8px 12px",
|
|
4162
|
-
display: "flex",
|
|
4163
|
-
alignItems: "center",
|
|
4164
|
-
gap: 8,
|
|
4165
|
-
borderBottom: children ? "1px solid var(--af-border-subtle)" : "none",
|
|
4166
|
-
fontSize: 12,
|
|
4167
|
-
color: "var(--af-text-secondary)",
|
|
4168
|
-
}}
|
|
4169
|
-
>
|
|
4170
|
-
<span style={{ fontSize: 13 }}>{icon}</span>
|
|
4171
|
-
<span>{label}</span>
|
|
4172
|
-
</div>
|
|
4173
|
-
{children}
|
|
4174
|
-
</div>
|
|
4175
|
-
);
|
|
4176
|
-
}
|
|
4177
|
-
|
|
4178
|
-
function CodeBlock({ text, maxHeight = 280 }: { text: string; maxHeight?: number }) {
|
|
4179
|
-
return (
|
|
4180
|
-
<pre
|
|
4181
|
-
style={{
|
|
4182
|
-
margin: 0,
|
|
4183
|
-
padding: "10px 12px",
|
|
4184
|
-
fontFamily: "var(--font-mono)",
|
|
4185
|
-
fontSize: 11.5,
|
|
4186
|
-
lineHeight: 1.55,
|
|
4187
|
-
color: "var(--af-text)",
|
|
4188
|
-
whiteSpace: "pre",
|
|
4189
|
-
overflow: "auto",
|
|
4190
|
-
maxHeight,
|
|
4191
|
-
}}
|
|
4192
|
-
>
|
|
4193
|
-
{text}
|
|
4194
|
-
</pre>
|
|
4195
|
-
);
|
|
4196
|
-
}
|
|
4197
|
-
|
|
4198
|
-
function DiffView({ oldText, newText }: { oldText: string; newText: string }) {
|
|
4199
|
-
const oldLines = oldText.split("\n");
|
|
4200
|
-
const newLines = newText.split("\n");
|
|
4201
|
-
return (
|
|
4202
|
-
<pre
|
|
4203
|
-
style={{
|
|
4204
|
-
margin: 0,
|
|
4205
|
-
padding: "10px 12px",
|
|
4206
|
-
fontFamily: "var(--font-mono)",
|
|
4207
|
-
fontSize: 11.5,
|
|
4208
|
-
lineHeight: 1.55,
|
|
4209
|
-
whiteSpace: "pre",
|
|
4210
|
-
overflow: "auto",
|
|
4211
|
-
maxHeight: 320,
|
|
4212
|
-
}}
|
|
4213
|
-
>
|
|
4214
|
-
{oldLines.map((line, i) => (
|
|
4215
|
-
<div
|
|
4216
|
-
key={`o${i}`}
|
|
4217
|
-
style={{
|
|
4218
|
-
background: "rgba(220, 38, 38, 0.08)",
|
|
4219
|
-
color: "#991B1B",
|
|
4220
|
-
padding: "0 4px",
|
|
4221
|
-
borderLeft: "3px solid #DC2626",
|
|
4222
|
-
}}
|
|
4223
|
-
>
|
|
4224
|
-
<span style={{ opacity: 0.6, userSelect: "none" }}>− </span>
|
|
4225
|
-
{line || "\u00A0"}
|
|
4226
|
-
</div>
|
|
4227
|
-
))}
|
|
4228
|
-
{newLines.map((line, i) => (
|
|
4229
|
-
<div
|
|
4230
|
-
key={`n${i}`}
|
|
4231
|
-
style={{
|
|
4232
|
-
background: "rgba(5, 150, 105, 0.08)",
|
|
4233
|
-
color: "#065F46",
|
|
4234
|
-
padding: "0 4px",
|
|
4235
|
-
borderLeft: "3px solid #059669",
|
|
4236
|
-
}}
|
|
4237
|
-
>
|
|
4238
|
-
<span style={{ opacity: 0.6, userSelect: "none" }}>+ </span>
|
|
4239
|
-
{line || "\u00A0"}
|
|
4240
|
-
</div>
|
|
4241
|
-
))}
|
|
4242
|
-
</pre>
|
|
4243
|
-
);
|
|
4244
|
-
}
|
|
4245
|
-
|
|
4246
|
-
function ToolUseCard({ name, input }: { name: string; input: ToolUseInput }) {
|
|
4247
|
-
const i = (input ?? {}) as Record<string, unknown>;
|
|
4248
|
-
const str = (k: string): string | undefined =>
|
|
4249
|
-
typeof i[k] === "string" ? (i[k] as string) : undefined;
|
|
4250
|
-
const num = (k: string): number | undefined =>
|
|
4251
|
-
typeof i[k] === "number" ? (i[k] as number) : undefined;
|
|
4252
|
-
|
|
4253
|
-
// --- Write -------------------------------------------------------
|
|
4254
|
-
if (name === "Write") {
|
|
4255
|
-
const filePath = str("file_path") ?? "";
|
|
4256
|
-
const content = str("content") ?? "";
|
|
4257
|
-
return (
|
|
4258
|
-
<ToolCardShell
|
|
4259
|
-
icon="📝"
|
|
4260
|
-
label={
|
|
4261
|
-
<>
|
|
4262
|
-
<b>Write</b> <PathLabel path={filePath} />
|
|
4263
|
-
</>
|
|
4264
|
-
}
|
|
4265
|
-
>
|
|
4266
|
-
<CodeBlock text={content} />
|
|
4267
|
-
</ToolCardShell>
|
|
4268
|
-
);
|
|
4269
|
-
}
|
|
4270
|
-
|
|
4271
|
-
// --- Edit --------------------------------------------------------
|
|
4272
|
-
if (name === "Edit") {
|
|
4273
|
-
const filePath = str("file_path") ?? "";
|
|
4274
|
-
const oldStr = str("old_string") ?? "";
|
|
4275
|
-
const newStr = str("new_string") ?? "";
|
|
4276
|
-
const replaceAll = i.replace_all === true;
|
|
4277
|
-
return (
|
|
4278
|
-
<ToolCardShell
|
|
4279
|
-
icon="✏️"
|
|
4280
|
-
label={
|
|
4281
|
-
<>
|
|
4282
|
-
<b>Edit</b> <PathLabel path={filePath} />
|
|
4283
|
-
{replaceAll && (
|
|
4284
|
-
<span
|
|
4285
|
-
style={{
|
|
4286
|
-
fontSize: 10,
|
|
4287
|
-
color: "var(--af-warning)",
|
|
4288
|
-
background: "var(--af-warning-subtle)",
|
|
4289
|
-
padding: "1px 6px",
|
|
4290
|
-
borderRadius: 10,
|
|
4291
|
-
}}
|
|
4292
|
-
>
|
|
4293
|
-
replace all
|
|
4294
|
-
</span>
|
|
4295
|
-
)}
|
|
4296
|
-
</>
|
|
4297
|
-
}
|
|
4298
|
-
>
|
|
4299
|
-
<DiffView oldText={oldStr} newText={newStr} />
|
|
4300
|
-
</ToolCardShell>
|
|
4301
|
-
);
|
|
4302
|
-
}
|
|
4303
|
-
|
|
4304
|
-
// --- Read --------------------------------------------------------
|
|
4305
|
-
if (name === "Read") {
|
|
4306
|
-
const filePath = str("file_path") ?? "";
|
|
4307
|
-
const offset = num("offset");
|
|
4308
|
-
const limit = num("limit");
|
|
4309
|
-
const range =
|
|
4310
|
-
offset !== undefined || limit !== undefined
|
|
4311
|
-
? ` · lines ${offset ?? 1}${limit !== undefined ? `–${(offset ?? 0) + limit}` : "…"}`
|
|
4312
|
-
: "";
|
|
4313
|
-
return (
|
|
4314
|
-
<ToolCardShell
|
|
4315
|
-
icon="📖"
|
|
4316
|
-
label={
|
|
4317
|
-
<>
|
|
4318
|
-
<b>Read</b> <PathLabel path={filePath} />
|
|
4319
|
-
<span style={{ color: "var(--af-text-tertiary)" }}>{range}</span>
|
|
4320
|
-
</>
|
|
4321
|
-
}
|
|
4322
|
-
/>
|
|
4323
|
-
);
|
|
4324
|
-
}
|
|
4325
|
-
|
|
4326
|
-
// --- Bash --------------------------------------------------------
|
|
4327
|
-
if (name === "Bash") {
|
|
4328
|
-
const command = str("command") ?? "";
|
|
4329
|
-
const description = str("description");
|
|
4330
|
-
const runInBg = i.run_in_background === true;
|
|
4331
|
-
return (
|
|
4332
|
-
<ToolCardShell
|
|
4333
|
-
icon="⚡"
|
|
4334
|
-
label={
|
|
4335
|
-
<>
|
|
4336
|
-
<b>Bash</b>
|
|
4337
|
-
{description && (
|
|
4338
|
-
<span
|
|
4339
|
-
style={{
|
|
4340
|
-
color: "var(--af-text)",
|
|
4341
|
-
fontStyle: "italic",
|
|
4342
|
-
}}
|
|
4343
|
-
>
|
|
4344
|
-
{description}
|
|
4345
|
-
</span>
|
|
4346
|
-
)}
|
|
4347
|
-
{runInBg && (
|
|
4348
|
-
<span
|
|
4349
|
-
style={{
|
|
4350
|
-
fontSize: 10,
|
|
4351
|
-
color: "var(--af-info)",
|
|
4352
|
-
background: "var(--af-info-subtle)",
|
|
4353
|
-
padding: "1px 6px",
|
|
4354
|
-
borderRadius: 10,
|
|
4355
|
-
}}
|
|
4356
|
-
>
|
|
4357
|
-
background
|
|
4358
|
-
</span>
|
|
4359
|
-
)}
|
|
4360
|
-
</>
|
|
4361
|
-
}
|
|
4362
|
-
>
|
|
4363
|
-
<CodeBlock text={command} maxHeight={220} />
|
|
4364
|
-
</ToolCardShell>
|
|
4365
|
-
);
|
|
4366
|
-
}
|
|
4367
|
-
|
|
4368
|
-
// --- Grep --------------------------------------------------------
|
|
4369
|
-
if (name === "Grep") {
|
|
4370
|
-
const pattern = str("pattern") ?? "";
|
|
4371
|
-
const path = str("path");
|
|
4372
|
-
const glob = str("glob");
|
|
4373
|
-
const type = str("type");
|
|
4374
|
-
const outputMode = str("output_mode");
|
|
4375
|
-
return (
|
|
4376
|
-
<ToolCardShell
|
|
4377
|
-
icon="🔍"
|
|
4378
|
-
label={
|
|
4379
|
-
<>
|
|
4380
|
-
<b>Grep</b>{" "}
|
|
4381
|
-
<code
|
|
4382
|
-
style={{
|
|
4383
|
-
background: "var(--af-border-subtle)",
|
|
4384
|
-
padding: "1px 6px",
|
|
4385
|
-
borderRadius: 4,
|
|
4386
|
-
fontFamily: "var(--font-mono)",
|
|
4387
|
-
fontSize: 11,
|
|
4388
|
-
}}
|
|
4389
|
-
>
|
|
4390
|
-
{pattern}
|
|
4391
|
-
</code>
|
|
4392
|
-
{path && (
|
|
4393
|
-
<>
|
|
4394
|
-
{" in "}
|
|
4395
|
-
<PathLabel path={path} />
|
|
4396
|
-
</>
|
|
4397
|
-
)}
|
|
4398
|
-
{(glob || type || outputMode) && (
|
|
4399
|
-
<span
|
|
4400
|
-
style={{
|
|
4401
|
-
color: "var(--af-text-tertiary)",
|
|
4402
|
-
fontSize: 11,
|
|
4403
|
-
marginLeft: 6,
|
|
4404
|
-
}}
|
|
4405
|
-
>
|
|
4406
|
-
{[
|
|
4407
|
-
glob && `glob=${glob}`,
|
|
4408
|
-
type && `type=${type}`,
|
|
4409
|
-
outputMode && `mode=${outputMode}`,
|
|
4410
|
-
]
|
|
4411
|
-
.filter(Boolean)
|
|
4412
|
-
.join(" · ")}
|
|
4413
|
-
</span>
|
|
4414
|
-
)}
|
|
4415
|
-
</>
|
|
4416
|
-
}
|
|
4417
|
-
/>
|
|
4418
|
-
);
|
|
4419
|
-
}
|
|
4420
|
-
|
|
4421
|
-
// --- Glob --------------------------------------------------------
|
|
4422
|
-
if (name === "Glob") {
|
|
4423
|
-
const pattern = str("pattern") ?? "";
|
|
4424
|
-
const path = str("path");
|
|
4425
|
-
return (
|
|
4426
|
-
<ToolCardShell
|
|
4427
|
-
icon="📁"
|
|
4428
|
-
label={
|
|
4429
|
-
<>
|
|
4430
|
-
<b>Glob</b>{" "}
|
|
4431
|
-
<code
|
|
4432
|
-
style={{
|
|
4433
|
-
background: "var(--af-border-subtle)",
|
|
4434
|
-
padding: "1px 6px",
|
|
4435
|
-
borderRadius: 4,
|
|
4436
|
-
fontFamily: "var(--font-mono)",
|
|
4437
|
-
fontSize: 11,
|
|
4438
|
-
}}
|
|
4439
|
-
>
|
|
4440
|
-
{pattern}
|
|
4441
|
-
</code>
|
|
4442
|
-
{path && (
|
|
4443
|
-
<>
|
|
4444
|
-
{" in "}
|
|
4445
|
-
<PathLabel path={path} />
|
|
4446
|
-
</>
|
|
4447
|
-
)}
|
|
4448
|
-
</>
|
|
4449
|
-
}
|
|
4450
|
-
/>
|
|
4451
|
-
);
|
|
4452
|
-
}
|
|
4453
|
-
|
|
4454
|
-
// --- Skill -------------------------------------------------------
|
|
4455
|
-
if (name === "Skill") {
|
|
4456
|
-
const skill = str("skill") ?? "";
|
|
4457
|
-
const args = str("args");
|
|
4458
|
-
return (
|
|
4459
|
-
<ToolCardShell
|
|
4460
|
-
icon="🧩"
|
|
4461
|
-
label={
|
|
4462
|
-
<>
|
|
4463
|
-
<b>/{skill}</b>
|
|
4464
|
-
{args && <span style={{ color: "var(--af-text-secondary)" }}>{args}</span>}
|
|
4465
|
-
</>
|
|
4466
|
-
}
|
|
4467
|
-
/>
|
|
4468
|
-
);
|
|
4469
|
-
}
|
|
4470
|
-
|
|
4471
|
-
// --- ToolSearch --------------------------------------------------
|
|
4472
|
-
if (name === "ToolSearch") {
|
|
4473
|
-
const query = str("query") ?? "";
|
|
4474
|
-
const max = num("max_results");
|
|
4475
|
-
return (
|
|
4476
|
-
<ToolCardShell
|
|
4477
|
-
icon="🔎"
|
|
4478
|
-
label={
|
|
4479
|
-
<>
|
|
4480
|
-
<b>ToolSearch</b>{" "}
|
|
4481
|
-
<code
|
|
4482
|
-
style={{
|
|
4483
|
-
background: "var(--af-border-subtle)",
|
|
4484
|
-
padding: "1px 6px",
|
|
4485
|
-
borderRadius: 4,
|
|
4486
|
-
fontFamily: "var(--font-mono)",
|
|
4487
|
-
fontSize: 11,
|
|
4488
|
-
}}
|
|
4489
|
-
>
|
|
4490
|
-
{query}
|
|
4491
|
-
</code>
|
|
4492
|
-
{max !== undefined && (
|
|
4493
|
-
<span style={{ color: "var(--af-text-tertiary)", fontSize: 11 }}>max={max}</span>
|
|
4494
|
-
)}
|
|
4495
|
-
</>
|
|
4496
|
-
}
|
|
4497
|
-
/>
|
|
4498
|
-
);
|
|
4499
|
-
}
|
|
4500
|
-
|
|
4501
|
-
// --- TodoWrite ---------------------------------------------------
|
|
4502
|
-
if (name === "TodoWrite") {
|
|
4503
|
-
const todos = Array.isArray(i.todos) ? (i.todos as Array<Record<string, unknown>>) : [];
|
|
4504
|
-
return (
|
|
4505
|
-
<ToolCardShell icon="✅" label={<b>TodoWrite · {todos.length} items</b>}>
|
|
4506
|
-
<div style={{ padding: "10px 12px", fontSize: 12 }}>
|
|
4507
|
-
{todos.map((t, ti) => {
|
|
4508
|
-
const status = String(t.status ?? "pending");
|
|
4509
|
-
const content =
|
|
4510
|
-
typeof t.content === "string"
|
|
4511
|
-
? t.content
|
|
4512
|
-
: typeof t.activeForm === "string"
|
|
4513
|
-
? t.activeForm
|
|
4514
|
-
: "";
|
|
4515
|
-
const icon = status === "completed" ? "✔" : status === "in_progress" ? "◐" : "○";
|
|
4516
|
-
return (
|
|
4517
|
-
<div
|
|
4518
|
-
key={ti}
|
|
4519
|
-
style={{
|
|
4520
|
-
display: "flex",
|
|
4521
|
-
gap: 8,
|
|
4522
|
-
padding: "2px 0",
|
|
4523
|
-
opacity: status === "completed" ? 0.6 : 1,
|
|
4524
|
-
textDecoration: status === "completed" ? "line-through" : "none",
|
|
4525
|
-
}}
|
|
4526
|
-
>
|
|
4527
|
-
<span style={{ color: "var(--af-text-tertiary)" }}>{icon}</span>
|
|
4528
|
-
<span>{content}</span>
|
|
4529
|
-
</div>
|
|
4530
|
-
);
|
|
4531
|
-
})}
|
|
4532
|
-
</div>
|
|
4533
|
-
</ToolCardShell>
|
|
4534
|
-
);
|
|
4535
|
-
}
|
|
4536
|
-
|
|
4537
|
-
// --- MCP tools (linear, slack, etc.) -----------------------------
|
|
4538
|
-
if (name.startsWith("mcp__")) {
|
|
4539
|
-
const short = shortenToolName(name);
|
|
4540
|
-
return (
|
|
4541
|
-
<ToolCardShell icon="🔌" label={<b>{short}</b>}>
|
|
4542
|
-
<CodeBlock text={JSON.stringify(input, null, 2)} maxHeight={200} />
|
|
4543
|
-
</ToolCardShell>
|
|
4544
|
-
);
|
|
4545
|
-
}
|
|
4546
|
-
|
|
4547
|
-
// --- Fallback ----------------------------------------------------
|
|
4548
|
-
return (
|
|
4549
|
-
<ToolCardShell icon="🔧" label={<b>{name}</b>}>
|
|
4550
|
-
<CodeBlock text={JSON.stringify(input, null, 2)} maxHeight={220} />
|
|
4551
|
-
</ToolCardShell>
|
|
4552
|
-
);
|
|
4553
|
-
}
|
|
4554
|
-
|
|
4555
4134
|
function BlockView({ block }: { block: ContentBlock }) {
|
|
4556
4135
|
if (block.type === "text") {
|
|
4557
4136
|
return (
|