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,306 @@
1
+ import type {
2
+ PresentationRow,
3
+ PresentationRowKind,
4
+ SessionEvent,
5
+ } from "@claude-lens/parser";
6
+
7
+ export type RoleTheme = {
8
+ label: string;
9
+ bg: string;
10
+ fg: string;
11
+ mini: string;
12
+ };
13
+
14
+ /**
15
+ * Palette modeled on Claude Managed Agents' Sessions view — muted,
16
+ * desaturated tones that read cleanly against the cream background.
17
+ * `bg`/`fg` drive the in-row pill; `mini` drives the timeline block fill.
18
+ */
19
+ export const ROLE_THEMES: Record<PresentationRowKind, RoleTheme> = {
20
+ user: {
21
+ label: "User",
22
+ bg: "rgba(201, 112, 112, 0.18)",
23
+ fg: "#8B3A3A",
24
+ mini: "#C97070",
25
+ },
26
+ agent: {
27
+ label: "Agent",
28
+ bg: "rgba(92, 132, 195, 0.18)",
29
+ fg: "#2E4A7A",
30
+ mini: "#5C84C3",
31
+ },
32
+ "tool-group": {
33
+ label: "Tool",
34
+ bg: "rgba(138, 133, 128, 0.16)",
35
+ fg: "#44403C",
36
+ mini: "#8A8580",
37
+ },
38
+ interrupt: {
39
+ label: "Interrupt",
40
+ bg: "rgba(217, 119, 6, 0.14)",
41
+ fg: "#78350F",
42
+ mini: "#D97706",
43
+ },
44
+ model: {
45
+ label: "Model",
46
+ bg: "rgba(168, 85, 247, 0.12)",
47
+ fg: "#581C87",
48
+ mini: "#A855F7",
49
+ },
50
+ error: {
51
+ label: "Error",
52
+ bg: "rgba(197, 48, 48, 0.18)",
53
+ fg: "#8B1818",
54
+ mini: "#C53030",
55
+ },
56
+ "task-notification": {
57
+ label: "Task",
58
+ bg: "rgba(100, 116, 139, 0.12)",
59
+ fg: "#475569",
60
+ mini: "#64748B",
61
+ },
62
+ };
63
+
64
+ /** Max number of steps shown inline in a collapsed turn. Larger turns are
65
+ * truncated with a "+N more" line; the user can click to expand the turn
66
+ * to see everything. */
67
+ export const MAX_INLINE_STEPS = 12;
68
+
69
+ export function rowPreview(r: PresentationRow): string {
70
+ switch (r.kind) {
71
+ case "user":
72
+ return r.displayPreview ?? r.event.preview;
73
+ case "agent":
74
+ return r.event.preview;
75
+ case "tool-group":
76
+ return formatToolSummary(r.toolNames);
77
+ case "interrupt":
78
+ return "Interrupted";
79
+ case "model":
80
+ return tokenSummaryLine(r.event.usage);
81
+ case "error":
82
+ return r.message;
83
+ case "task-notification": {
84
+ const icon =
85
+ r.status === "success"
86
+ ? "✓"
87
+ : r.status === "failed"
88
+ ? "✗"
89
+ : r.status === "running"
90
+ ? "…"
91
+ : "•";
92
+ return `${icon} ${r.summary}`;
93
+ }
94
+ }
95
+ }
96
+
97
+ /** Render a compact "Bash ×3 · Read ×2 · Grep · Edit" style summary.
98
+ * Truncates after 4 unique tools with "+N more". */
99
+ export function formatToolSummary(
100
+ toolNames: { name: string; count: number }[],
101
+ ): string {
102
+ const MAX = 4;
103
+ const shown = toolNames.slice(0, MAX);
104
+ const overflow = toolNames.length - MAX;
105
+ const parts = shown.map((t) => {
106
+ const display = shortenToolName(t.name);
107
+ return t.count > 1 ? `${display} ×${t.count}` : display;
108
+ });
109
+ if (overflow > 0) parts.push(`+${overflow} more`);
110
+ return parts.join(" · ");
111
+ }
112
+
113
+ export function tokenSummaryLine(u: SessionEvent["usage"]): string {
114
+ if (!u) return "0 input · 0 output · 0 cache read · 0 cache write";
115
+ return `${u.input} input · ${u.output} output · ${u.cacheRead} cache read · ${u.cacheWrite} cache write`;
116
+ }
117
+
118
+ export function shortenToolName(name: string): string {
119
+ // mcp__plugin_linear_linear__get_issue → linear.get_issue
120
+ const m = name.match(/^mcp__(?:plugin_)?([^_]+)_(?:\1_)?(.+)$/);
121
+ if (m) return `${m[1]}.${m[2]}`;
122
+ // mcp__claude_ai_Gmail__search_threads → gmail.search_threads
123
+ const m2 = name.match(/^mcp__claude_ai_([^_]+)__(.+)$/);
124
+ if (m2) return `${m2[1].toLowerCase()}.${m2[2]}`;
125
+ return name;
126
+ }
127
+
128
+ /** Bulleted list of everything that happened in the middle of a turn.
129
+ * Each row is a one-line compact entry with role + preview. Capped at
130
+ * MAX_INLINE_STEPS items with an overflow indicator. */
131
+ export function TurnStepsList({
132
+ rows,
133
+ onStepClick,
134
+ showAll,
135
+ }: {
136
+ rows: PresentationRow[];
137
+ /** Optional: when set, each step line becomes a button. */
138
+ onStepClick?: (row: PresentationRow, index: number) => void;
139
+ /** Bypass the MAX_INLINE_STEPS cap and show every row. */
140
+ showAll?: boolean;
141
+ }) {
142
+ const cap = showAll ? rows.length : MAX_INLINE_STEPS;
143
+ const overflow = Math.max(0, rows.length - cap);
144
+ const shown = overflow > 0 ? rows.slice(-cap) : rows;
145
+ const baseIndex = overflow > 0 ? rows.length - shown.length : 0;
146
+
147
+ return (
148
+ <div
149
+ style={{
150
+ display: "flex",
151
+ flexDirection: "column",
152
+ gap: 2,
153
+ paddingLeft: 2,
154
+ borderLeft: "1px solid var(--af-border-subtle)",
155
+ paddingBlock: 2,
156
+ }}
157
+ >
158
+ {overflow > 0 && (
159
+ <div
160
+ style={{
161
+ fontSize: 11,
162
+ color: "var(--af-text-tertiary)",
163
+ fontStyle: "italic",
164
+ paddingLeft: 12,
165
+ paddingBottom: 2,
166
+ }}
167
+ >
168
+ {overflow} earlier step{overflow === 1 ? "" : "s"} …
169
+ </div>
170
+ )}
171
+ {shown.map((r, i) => {
172
+ const absoluteIndex = baseIndex + i;
173
+ return (
174
+ <TurnStepLine
175
+ key={i}
176
+ row={r}
177
+ onClick={
178
+ onStepClick ? () => onStepClick(r, absoluteIndex) : undefined
179
+ }
180
+ />
181
+ );
182
+ })}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ /** One step in the middle list. Renders a small role marker + the preview
188
+ * for that row, truncated to one line. */
189
+ function TurnStepLine({
190
+ row,
191
+ onClick,
192
+ }: {
193
+ row: PresentationRow;
194
+ onClick?: () => void;
195
+ }) {
196
+ const theme = ROLE_THEMES[row.kind];
197
+ const isTeammateMsg = row.kind === "user" && !!row.event.teammateMessage;
198
+ const label = isTeammateMsg ? "Team Lead" : theme.label;
199
+ const preview = rowPreview(row);
200
+ const interactive = !!onClick;
201
+ return (
202
+ <div
203
+ role={interactive ? "button" : undefined}
204
+ tabIndex={interactive ? 0 : undefined}
205
+ onClick={onClick}
206
+ onKeyDown={
207
+ interactive
208
+ ? (e) => {
209
+ if (e.key === "Enter" || e.key === " ") {
210
+ e.preventDefault();
211
+ onClick?.();
212
+ }
213
+ }
214
+ : undefined
215
+ }
216
+ style={{
217
+ display: "grid",
218
+ gridTemplateColumns: "46px 1fr",
219
+ columnGap: 8,
220
+ fontSize: 12,
221
+ lineHeight: 1.45,
222
+ color: row.kind === "error" ? "var(--af-danger)" : "var(--af-text-secondary)",
223
+ cursor: interactive ? "pointer" : undefined,
224
+ borderRadius: interactive ? 3 : undefined,
225
+ padding: interactive ? "2px 4px" : undefined,
226
+ transition: interactive ? "background 0.1s ease" : undefined,
227
+ }}
228
+ onMouseEnter={
229
+ interactive
230
+ ? (e) => {
231
+ (e.currentTarget as HTMLDivElement).style.background =
232
+ "var(--af-surface-hover)";
233
+ }
234
+ : undefined
235
+ }
236
+ onMouseLeave={
237
+ interactive
238
+ ? (e) => {
239
+ (e.currentTarget as HTMLDivElement).style.background = "";
240
+ }
241
+ : undefined
242
+ }
243
+ >
244
+ <span
245
+ style={{
246
+ fontSize: 10,
247
+ fontWeight: 600,
248
+ background: theme.bg,
249
+ color: theme.fg,
250
+ padding: "1px 6px",
251
+ borderRadius: 3,
252
+ textAlign: "center",
253
+ justifySelf: "start",
254
+ maxWidth: "100%",
255
+ }}
256
+ >
257
+ {label}
258
+ </span>
259
+ <span
260
+ style={{
261
+ overflow: "hidden",
262
+ textOverflow: "ellipsis",
263
+ whiteSpace: "nowrap",
264
+ minWidth: 0,
265
+ }}
266
+ >
267
+ {row.kind === "tool-group" ? (
268
+ <span>
269
+ {row.toolNames.slice(0, 4).map((t, i) => (
270
+ <span key={t.name}>
271
+ {i > 0 && (
272
+ <span style={{ color: "var(--af-text-tertiary)", margin: "0 5px" }}>·</span>
273
+ )}
274
+ <b style={{ fontWeight: 600 }}>{shortenToolName(t.name)}</b>
275
+ {t.count > 1 && (
276
+ <span
277
+ style={{
278
+ color: "var(--af-text-tertiary)",
279
+ fontFamily: "var(--font-mono)",
280
+ marginLeft: 3,
281
+ }}
282
+ >
283
+ ×{t.count}
284
+ </span>
285
+ )}
286
+ </span>
287
+ ))}
288
+ {row.toolNames.length > 4 && (
289
+ <span
290
+ style={{
291
+ color: "var(--af-text-tertiary)",
292
+ marginLeft: 6,
293
+ fontSize: 10,
294
+ }}
295
+ >
296
+ +{row.toolNames.length - 4}
297
+ </span>
298
+ )}
299
+ </span>
300
+ ) : (
301
+ preview
302
+ )}
303
+ </span>
304
+ </div>
305
+ );
306
+ }
@@ -13,6 +13,7 @@ import {
13
13
  } from "@/lib/format";
14
14
  import { Search, Wrench, MessagesSquare, Clock } from "lucide-react";
15
15
  import { LiveBadge } from "@/components/live-badge";
16
+ import { TeamBadge } from "@/components/team-badge";
16
17
  import { DataTable, type Column } from "@/components/data-table";
17
18
  import { useViewToggle } from "@/components/view-toggle";
18
19
 
@@ -41,10 +42,13 @@ export function SessionsGrid({ sessions }: { sessions: SessionMeta[] }) {
41
42
  s.projectName.toLowerCase().includes(q),
42
43
  );
43
44
  }
44
- // "Longest" uses active time (airTimeMs), not wall-clock duration.
45
- // Otherwise a session that sat idle for 14 days with one message
46
- // would out-rank a session that was actively coding for 4 hours.
47
- if (sortBy === "longest")
45
+ if (sortBy === "newest")
46
+ rows.sort(
47
+ (a, b) =>
48
+ (b.firstTimestamp ? Date.parse(b.firstTimestamp) : 0) -
49
+ (a.firstTimestamp ? Date.parse(a.firstTimestamp) : 0),
50
+ );
51
+ else if (sortBy === "longest")
48
52
  rows.sort(
49
53
  (a, b) =>
50
54
  (b.airTimeMs ?? b.durationMs ?? 0) - (a.airTimeMs ?? a.durationMs ?? 0),
@@ -169,7 +173,19 @@ const sessionTableColumns: Column<SessionMeta>[] = [
169
173
  }}
170
174
  title={s.projectName}
171
175
  >
172
- <div style={{ fontWeight: 500 }}>{prettyProjectName(s.projectName)}</div>
176
+ <div style={{ fontWeight: 500, display: "flex", alignItems: "center", gap: 6 }}>
177
+ <span
178
+ style={{
179
+ overflow: "hidden",
180
+ textOverflow: "ellipsis",
181
+ whiteSpace: "nowrap",
182
+ minWidth: 0,
183
+ }}
184
+ >
185
+ {prettyProjectName(s.projectName)}
186
+ </span>
187
+ <TeamBadge session={s} />
188
+ </div>
173
189
  <div
174
190
  style={{
175
191
  fontSize: 10,
@@ -201,6 +217,20 @@ const sessionTableColumns: Column<SessionMeta>[] = [
201
217
  </div>
202
218
  ),
203
219
  },
220
+ {
221
+ key: "created",
222
+ header: "Created",
223
+ sortValue: (s) => (s.firstTimestamp ? Date.parse(s.firstTimestamp) : 0),
224
+ align: "right",
225
+ render: (s) => (
226
+ <span
227
+ suppressHydrationWarning
228
+ style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}
229
+ >
230
+ {s.firstTimestamp ? formatRelative(s.firstTimestamp) : "—"}
231
+ </span>
232
+ ),
233
+ },
204
234
  {
205
235
  key: "lastActive",
206
236
  header: "Last active",
@@ -325,6 +355,7 @@ function SessionCard({ session: s }: { session: SessionMeta }) {
325
355
  >
326
356
  {prettyProjectName(s.projectName)}
327
357
  </span>
358
+ <TeamBadge session={s} linkable={false} />
328
359
  </div>
329
360
  <div
330
361
  style={{
@@ -16,6 +16,7 @@ import { useEffect, useState } from "react";
16
16
  import { usePathname } from "next/navigation";
17
17
  import Link from "next/link";
18
18
  import { prettyProjectName } from "@/lib/format";
19
+ import { TeamBadge } from "@/components/team-badge";
19
20
 
20
21
  const LIVE_WINDOW_MS = 45_000;
21
22
  const MAX_VISIBLE = 5;
@@ -29,6 +30,8 @@ export type LiveSessionPick = {
29
30
  lastAgentPreview?: string;
30
31
  firstTimestamp?: string;
31
32
  lastTimestamp?: string;
33
+ teamName?: string;
34
+ agentName?: string;
32
35
  };
33
36
 
34
37
  export function LiveSessionsWidget({ sessions }: { sessions: LiveSessionPick[] }) {
@@ -190,16 +193,27 @@ export function LiveSessionsWidget({ sessions }: { sessions: LiveSessionPick[] }
190
193
  {/* Title: most recent user message — "what am I working on now". */}
191
194
  <div
192
195
  style={{
193
- overflow: "hidden",
194
- textOverflow: "ellipsis",
195
- whiteSpace: "nowrap",
196
+ display: "flex",
197
+ alignItems: "center",
198
+ gap: 6,
196
199
  fontWeight: 500,
197
200
  color: "var(--af-text)",
201
+ minWidth: 0,
198
202
  }}
199
203
  >
200
- {s.lastUserPreview || s.firstUserPreview || (
201
- <em style={{ color: "var(--af-text-tertiary)" }}>(no user message)</em>
202
- )}
204
+ <span
205
+ style={{
206
+ overflow: "hidden",
207
+ textOverflow: "ellipsis",
208
+ whiteSpace: "nowrap",
209
+ minWidth: 0,
210
+ }}
211
+ >
212
+ {s.lastUserPreview || s.firstUserPreview || (
213
+ <em style={{ color: "var(--af-text-tertiary)" }}>(no user message)</em>
214
+ )}
215
+ </span>
216
+ <TeamBadge session={s} linkable={false} />
203
217
  </div>
204
218
  {/* Subtitle: what the agent is saying in response. */}
205
219
  <div
@@ -0,0 +1,90 @@
1
+ import Link from "next/link";
2
+
3
+ type BadgeSession = {
4
+ id: string;
5
+ teamName?: string;
6
+ agentName?: string;
7
+ isTeamLead?: boolean;
8
+ };
9
+
10
+ const BASE_STYLE = {
11
+ display: "inline-flex",
12
+ alignItems: "center",
13
+ borderRadius: 4,
14
+ padding: "2px 6px",
15
+ fontSize: 10,
16
+ letterSpacing: "0.04em",
17
+ textTransform: "uppercase",
18
+ whiteSpace: "nowrap",
19
+ } as const;
20
+
21
+ export function TeamBadge({
22
+ session,
23
+ linkable = true,
24
+ }: {
25
+ session: BadgeSession;
26
+ /** When false, the lead variant renders as a span instead of a Link — use
27
+ * inside parent `<Link>` contexts (e.g. a card wrapper) to avoid nested
28
+ * anchors. */
29
+ linkable?: boolean;
30
+ }) {
31
+ if (!session.teamName) return null;
32
+ // Lead = explicit orchestration evidence. A bare teamName + missing
33
+ // agentName isn't enough — see SessionMeta.isTeamLead. Sessions that
34
+ // sit in that grey zone (tagged with a team but doing nothing teamy)
35
+ // get no badge.
36
+ const isLead = session.isTeamLead === true;
37
+ const isMember = session.agentName !== undefined;
38
+ if (!isLead && !isMember) return null;
39
+
40
+ if (isLead && linkable) {
41
+ return (
42
+ <Link
43
+ href={`/sessions/${session.id}`}
44
+ style={{
45
+ ...BASE_STYLE,
46
+ fontWeight: 600,
47
+ background: "var(--af-warning-subtle)",
48
+ color: "var(--af-warning)",
49
+ border: "1px solid var(--af-warning-subtle)",
50
+ textDecoration: "none",
51
+ }}
52
+ title={`Team lead — ${session.teamName}`}
53
+ >
54
+ Team Lead
55
+ </Link>
56
+ );
57
+ }
58
+
59
+ if (isLead) {
60
+ return (
61
+ <span
62
+ style={{
63
+ ...BASE_STYLE,
64
+ fontWeight: 600,
65
+ background: "var(--af-warning-subtle)",
66
+ color: "var(--af-warning)",
67
+ border: "1px solid var(--af-warning-subtle)",
68
+ }}
69
+ title={`Team lead — ${session.teamName}`}
70
+ >
71
+ Team Lead
72
+ </span>
73
+ );
74
+ }
75
+
76
+ return (
77
+ <span
78
+ style={{
79
+ ...BASE_STYLE,
80
+ fontWeight: 500,
81
+ background: "var(--af-surface-hover)",
82
+ color: "var(--af-text-tertiary)",
83
+ border: "1px solid var(--af-border-subtle)",
84
+ }}
85
+ title={`Team member — ${session.teamName} · ${session.agentName}`}
86
+ >
87
+ Team Member
88
+ </span>
89
+ );
90
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-lens/web",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "description": "Local-only dashboard for Claude Code sessions. Reads ~/.claude/projects/*.jsonl and visualizes agent activity.",
6
6
  "license": "MIT",