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
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 }}>
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
+
}
|