fleetlens 0.6.4 → 0.7.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 +2 -2
- 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/api/ask/route.js +2 -2
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js +2 -2
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/route.js +3 -3
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js +3 -3
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js +2 -2
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js +2 -2
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js +2 -2
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -2
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page.js +2 -2
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/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 +2 -2
- 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 +3 -3
- 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/runs/page.js +2 -2
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +3 -3
- 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 +3 -3
- 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/settings/page.js +2 -2
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js +2 -2
- 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/{[externals]__0o43ejh._.js → [externals]__0fpbr8-._.js} +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0-vgrh1._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.z48ue._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0dctov1._.js +5 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e8n_x4._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +2 -2
- package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +1 -1
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_09hmrqx._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_118y1lp._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +26 -17
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_122oe0-.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0e.gxk3._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0p1x6v2._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0sp6.jt._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0v.6dx0._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wwa6gh._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv-a8z._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10~b4bf._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0woap-3._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_settings_ai-features-form_tsx_01p699p._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_usage-data_ts_0xtv99n._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{node_modules_0i2xw~e._.js → node_modules_0fx6-.x._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_10_oegc._.js +3 -0
- 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/0.3mqbj24kwtc.js +6 -0
- package/app/apps/web/.next/static/chunks/07-fq4im3pmjp.js +4 -0
- package/app/apps/web/.next/static/chunks/09gdxttgu~.aj.js +14 -0
- package/app/apps/web/.next/static/chunks/0n3-prt61k7pr.js +1 -0
- package/app/apps/web/.next/static/chunks/{0_7303jr0sf0w.js → 0q2vd~aj.1hpz.js} +3 -3
- package/app/apps/web/.next/static/chunks/0qmh5da6gfm56.js +1 -0
- package/app/apps/web/.next/static/chunks/{0k9pksi_i.odf.js → 111-bgx08j2nq.js} +1 -1
- package/app/apps/web/app/api/ask/route.ts +1 -1
- package/app/apps/web/app/api/digest/week/[startDate]/route.ts +2 -1
- package/app/apps/web/app/api/settings/route.ts +1 -0
- package/app/apps/web/app/insights/[key]/page.tsx +8 -2
- package/app/apps/web/app/layout.tsx +10 -7
- package/app/apps/web/app/page.tsx +19 -15
- package/app/apps/web/app/parallelism/gantt-chart.tsx +7 -18
- package/app/apps/web/app/projects/projects-view.tsx +3 -0
- package/app/apps/web/app/sessions/[id]/session-view.tsx +168 -41
- package/app/apps/web/app/sessions/page.tsx +22 -12
- package/app/apps/web/app/sessions/sessions-grid.tsx +82 -9
- package/app/apps/web/app/settings/ai-features-form.tsx +20 -1
- package/app/apps/web/app/settings/page.tsx +1 -0
- package/app/apps/web/app/usage/page.tsx +161 -52
- package/app/apps/web/components/agent-badge.tsx +43 -0
- package/app/apps/web/components/agent-icon.tsx +38 -0
- package/app/apps/web/components/agent-mix-chip.tsx +42 -0
- package/app/apps/web/components/{ask-claude.tsx → ask.tsx} +13 -12
- package/app/apps/web/components/day-digest.tsx +1 -1
- package/app/apps/web/components/live-sessions-widget.tsx +5 -1
- package/app/apps/web/components/outcome-pill.tsx +7 -2
- package/app/apps/web/components/sidebar.tsx +1 -1
- package/app/apps/web/components/week-digest.tsx +2 -0
- package/app/apps/web/lib/ai/session-summary.ts +1 -1
- package/app/apps/web/lib/data.ts +7 -8
- package/app/apps/web/lib/date-range.ts +9 -2
- package/app/apps/web/lib/usage-data.ts +24 -1
- package/app/apps/web/lib/validate-settings.ts +1 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +3152 -2355
- package/dist/index.js +4418 -3752
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/[externals]__0_xztrf._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +0 -5
- package/app/apps/web/.next/server/chunks/packages_0e7q9u_._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_0n6p2ot._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-94ji~._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_6.inc._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0jdyefm._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ln9jsw._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u5d7de._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~_8vff._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11v.eqw._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +0 -3
- package/app/apps/web/.next/static/chunks/04m3slg_5ph02.js +0 -1
- package/app/apps/web/.next/static/chunks/0coa1800xqgfj.js +0 -1
- package/app/apps/web/.next/static/chunks/0k1psab.bybh9.js +0 -14
- package/app/apps/web/.next/static/chunks/0xfu09fn_358k.js +0 -4
- package/app/apps/web/.next/static/chunks/17u2ww8ya5dv1.js +0 -6
- /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → qYW0u_NOmV0v7lf7xZc6x}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → qYW0u_NOmV0v7lf7xZc6x}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → qYW0u_NOmV0v7lf7xZc6x}/_ssgManifest.js +0 -0
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
} from "@claude-lens/parser";
|
|
40
40
|
import { estimateCost, formatCost, formatGap, formatOffset, formatRelative, formatTokens, shortId } from "@/lib/format";
|
|
41
41
|
import { LiveBadge } from "@/components/live-badge";
|
|
42
|
-
import {
|
|
42
|
+
import { AskButton, AskDrawer } from "@/components/ask";
|
|
43
43
|
import { TailMode } from "@/components/tail-mode";
|
|
44
44
|
import type { TimelineData } from "./team-tab/adapter";
|
|
45
45
|
import { TeamTabClient } from "./team-tab/team-tab-client";
|
|
@@ -65,7 +65,9 @@ import {
|
|
|
65
65
|
/* Constants + theming */
|
|
66
66
|
/* ------------------------------------------------------------------ */
|
|
67
67
|
|
|
68
|
-
/** Gap > this (ms) before a user row is shown as a "Session idle" divider
|
|
68
|
+
/** Gap > this (ms) before a user row is shown as a "Session idle" divider
|
|
69
|
+
* in the transcript body (separate from the minimap, which now derives
|
|
70
|
+
* idle from raw event timestamps via rawIdleBands). */
|
|
69
71
|
const IDLE_THRESHOLD_MS = 2000;
|
|
70
72
|
|
|
71
73
|
/** Sticky header height (session header + tabs + mini-map).
|
|
@@ -150,7 +152,7 @@ export function SessionView({
|
|
|
150
152
|
const [filter, setFilter] = useState<FilterMode>("turns");
|
|
151
153
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
152
154
|
const [selectedSubagentId, setSelectedSubagentId] = useState<string | null>(null);
|
|
153
|
-
const [
|
|
155
|
+
const [askOpen, setAskOpen] = useState(false);
|
|
154
156
|
const [expandedTurns, setExpandedTurns] = useState<Set<number>>(new Set());
|
|
155
157
|
/** When a timeline click sets the index we also want to scroll. Track that
|
|
156
158
|
* intent separately so selection via row-click doesn't auto-scroll. */
|
|
@@ -290,6 +292,76 @@ export function SessionView({
|
|
|
290
292
|
/** Detect PR creations in this session. */
|
|
291
293
|
const prMarkers = useMemo(() => detectPrMarkers(session), [session]);
|
|
292
294
|
|
|
295
|
+
/** Idle bands derived from raw event timestamps, classified per gap.
|
|
296
|
+
*
|
|
297
|
+
* Two flavors:
|
|
298
|
+
* - In-turn idle (gap between agent activities within the same user
|
|
299
|
+
* turn) — multiple of these inside one turn collapse into ONE merged
|
|
300
|
+
* band whose `start..end` spans the whole period and `durationMs`
|
|
301
|
+
* sums the actual idle.
|
|
302
|
+
* - Between-turn idle (gap whose terminating anchor IS a user message)
|
|
303
|
+
* — emitted standalone. This is "user paused, walked away, came
|
|
304
|
+
* back" time and is conceptually distinct from agent thinking.
|
|
305
|
+
*
|
|
306
|
+
* Anchors only count user / agent / tool-call / tool-result events —
|
|
307
|
+
* meta + thinking events are skipped so sub-second event-to-event gaps
|
|
308
|
+
* don't crowd the strip.
|
|
309
|
+
*
|
|
310
|
+
* Side effect: the leading warm-up gap before the first agent response
|
|
311
|
+
* drops out unless it crosses MIN_GAP_MS — which the first 6s in a
|
|
312
|
+
* typical Codex session does not. */
|
|
313
|
+
const rawIdleBands = useMemo(() => {
|
|
314
|
+
const isAnchor = (role: string) =>
|
|
315
|
+
role === "user" ||
|
|
316
|
+
role === "agent" ||
|
|
317
|
+
role === "tool-call" ||
|
|
318
|
+
role === "tool-result";
|
|
319
|
+
const MIN_GAP_MS = 10_000;
|
|
320
|
+
const anchors: { ms: number; role: string }[] = [];
|
|
321
|
+
for (const e of events) {
|
|
322
|
+
if (!isAnchor(e.role)) continue;
|
|
323
|
+
if (!e.timestamp) continue;
|
|
324
|
+
const ms = Date.parse(e.timestamp);
|
|
325
|
+
if (!Number.isFinite(ms)) continue;
|
|
326
|
+
anchors.push({ ms, role: e.role });
|
|
327
|
+
}
|
|
328
|
+
if (anchors.length < 2) return [];
|
|
329
|
+
anchors.sort((a, b) => a.ms - b.ms);
|
|
330
|
+
const sessionStartMs = anchors[0]!.ms - (events.find((e) => e.timestamp)?.tOffsetMs ?? 0);
|
|
331
|
+
|
|
332
|
+
type Band = { start: number; end: number; durationMs: number };
|
|
333
|
+
const bands: Band[] = [];
|
|
334
|
+
let inTurn: Band | null = null;
|
|
335
|
+
|
|
336
|
+
const flushInTurn = () => {
|
|
337
|
+
if (inTurn && inTurn.durationMs > MIN_GAP_MS) bands.push(inTurn);
|
|
338
|
+
inTurn = null;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
for (let i = 1; i < anchors.length; i++) {
|
|
342
|
+
const gap = anchors[i]!.ms - anchors[i - 1]!.ms;
|
|
343
|
+
if (gap <= MIN_GAP_MS) continue;
|
|
344
|
+
const gStart = anchors[i - 1]!.ms - sessionStartMs;
|
|
345
|
+
const gEnd = anchors[i]!.ms - sessionStartMs;
|
|
346
|
+
|
|
347
|
+
if (anchors[i]!.role === "user") {
|
|
348
|
+
// Between-turn idle — close any in-progress in-turn merge first,
|
|
349
|
+
// then push the between-turn band on its own.
|
|
350
|
+
flushInTurn();
|
|
351
|
+
bands.push({ start: gStart, end: gEnd, durationMs: gap });
|
|
352
|
+
} else {
|
|
353
|
+
// In-turn idle — accumulate into the running merged band.
|
|
354
|
+
if (!inTurn) inTurn = { start: gStart, end: gEnd, durationMs: gap };
|
|
355
|
+
else {
|
|
356
|
+
inTurn.end = gEnd;
|
|
357
|
+
inTurn.durationMs += gap;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
flushInTurn();
|
|
362
|
+
return bands;
|
|
363
|
+
}, [events]);
|
|
364
|
+
|
|
293
365
|
const coldResumeMarkers = useMemo(() => {
|
|
294
366
|
const seen = new Set<string>();
|
|
295
367
|
const out: {
|
|
@@ -607,7 +679,7 @@ export function SessionView({
|
|
|
607
679
|
</span>
|
|
608
680
|
</div>
|
|
609
681
|
|
|
610
|
-
{/* Toolbar (filter, copy, ask
|
|
682
|
+
{/* Toolbar (filter, copy, ask) */}
|
|
611
683
|
<div
|
|
612
684
|
className="flex items-center"
|
|
613
685
|
style={{
|
|
@@ -662,9 +734,9 @@ export function SessionView({
|
|
|
662
734
|
>
|
|
663
735
|
<Copy size={12} /> Copy all
|
|
664
736
|
</button>
|
|
665
|
-
<
|
|
737
|
+
<AskButton
|
|
666
738
|
onClick={() => {
|
|
667
|
-
|
|
739
|
+
setAskOpen((p) => !p);
|
|
668
740
|
setSelectedIndex(null);
|
|
669
741
|
setSelectedSubagentId(null);
|
|
670
742
|
}}
|
|
@@ -707,6 +779,7 @@ export function SessionView({
|
|
|
707
779
|
collapsed={collapsed}
|
|
708
780
|
prMarkers={prMarkers}
|
|
709
781
|
coldResumeMarkers={coldResumeMarkers}
|
|
782
|
+
rawIdleBands={rawIdleBands}
|
|
710
783
|
model={model}
|
|
711
784
|
selectedSubagentId={selectedSubagentId}
|
|
712
785
|
onSelectSubagent={(id) => {
|
|
@@ -788,6 +861,7 @@ export function SessionView({
|
|
|
788
861
|
isSessionLive={isSessionLive}
|
|
789
862
|
team={team}
|
|
790
863
|
model={model}
|
|
864
|
+
rawIdleBands={rawIdleBands}
|
|
791
865
|
/>
|
|
792
866
|
</>
|
|
793
867
|
) : (
|
|
@@ -868,8 +942,8 @@ export function SessionView({
|
|
|
868
942
|
);
|
|
869
943
|
})()}
|
|
870
944
|
|
|
871
|
-
{/* Ask
|
|
872
|
-
{
|
|
945
|
+
{/* Ask drawer */}
|
|
946
|
+
{askOpen && (
|
|
873
947
|
<aside
|
|
874
948
|
style={{
|
|
875
949
|
position: "fixed",
|
|
@@ -885,9 +959,9 @@ export function SessionView({
|
|
|
885
959
|
flexDirection: "column",
|
|
886
960
|
}}
|
|
887
961
|
>
|
|
888
|
-
<
|
|
962
|
+
<AskDrawer
|
|
889
963
|
sessionId={session.id}
|
|
890
|
-
onClose={() =>
|
|
964
|
+
onClose={() => setAskOpen(false)}
|
|
891
965
|
/>
|
|
892
966
|
</aside>
|
|
893
967
|
)}
|
|
@@ -1285,6 +1359,7 @@ function Minimap({
|
|
|
1285
1359
|
onSelectSubagent,
|
|
1286
1360
|
prMarkers,
|
|
1287
1361
|
coldResumeMarkers,
|
|
1362
|
+
rawIdleBands,
|
|
1288
1363
|
model,
|
|
1289
1364
|
}: {
|
|
1290
1365
|
displayRows: DisplayRow[];
|
|
@@ -1301,6 +1376,11 @@ function Minimap({
|
|
|
1301
1376
|
tOffsetMs: number;
|
|
1302
1377
|
info: NonNullable<SessionEvent["coldResume"]>;
|
|
1303
1378
|
}[];
|
|
1379
|
+
/** Idle bands derived directly from raw event timestamps. Used in place
|
|
1380
|
+
* of the legacy "gap before user row" heuristic so Codex sessions
|
|
1381
|
+
* (which spend most of their time in agent reasoning, not user input)
|
|
1382
|
+
* surface their dead-air on the minimap. */
|
|
1383
|
+
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
1304
1384
|
model?: string;
|
|
1305
1385
|
}) {
|
|
1306
1386
|
const WIDTH = 1400;
|
|
@@ -1456,52 +1536,66 @@ function Minimap({
|
|
|
1456
1536
|
}
|
|
1457
1537
|
}
|
|
1458
1538
|
|
|
1539
|
+
// Build a sorted copy of idle bands so we can clip row/turn ends to
|
|
1540
|
+
// the start of the next idle band (or the next row, whichever comes
|
|
1541
|
+
// first). This stops a row's rectangle from spanning across an idle
|
|
1542
|
+
// gap and visually swallowing it.
|
|
1543
|
+
const idleBands = (rawIdleBands ?? []).slice().sort((a, b) => a.start - b.start);
|
|
1544
|
+
const nextIdleStartFrom = (t: number): number | undefined => {
|
|
1545
|
+
for (const b of idleBands) {
|
|
1546
|
+
if (b.start >= t) return b.start;
|
|
1547
|
+
}
|
|
1548
|
+
return undefined;
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1459
1551
|
for (let i = 0; i < withTime.length; i++) {
|
|
1460
1552
|
const item = withTime[i];
|
|
1461
1553
|
const next = withTime[i + 1];
|
|
1462
1554
|
const start = item.start;
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
out.push({
|
|
1473
|
-
kind: "idle",
|
|
1474
|
-
start: Math.max(0, start - item.gapMs),
|
|
1475
|
-
end: start,
|
|
1476
|
-
durationMs: item.gapMs,
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1555
|
+
const nextRowStart = next?.start;
|
|
1556
|
+
const nextIdleStart = nextIdleStartFrom(start + 1);
|
|
1557
|
+
// Cap end to whichever comes first: next visible row, next idle band.
|
|
1558
|
+
const cap = (raw: number) =>
|
|
1559
|
+
Math.min(
|
|
1560
|
+
raw,
|
|
1561
|
+
nextRowStart ?? Number.POSITIVE_INFINITY,
|
|
1562
|
+
nextIdleStart ?? Number.POSITIVE_INFINITY,
|
|
1563
|
+
);
|
|
1479
1564
|
|
|
1480
1565
|
if (item.kind === "turn") {
|
|
1481
|
-
// Turn's end = start + duration, clamped against the next
|
|
1482
|
-
// segment's start so adjacent turns don't overlap.
|
|
1483
1566
|
const turnEnd = start + (item.turn.durationMs ?? 0);
|
|
1484
|
-
const nextStart = next?.start;
|
|
1485
|
-
const end = nextStart !== undefined ? Math.min(turnEnd, nextStart) : turnEnd;
|
|
1486
1567
|
out.push({
|
|
1487
1568
|
kind: "turn",
|
|
1488
1569
|
turn: item.turn,
|
|
1489
1570
|
primaryIndex: item.turn.firstPrimaryIndex,
|
|
1490
1571
|
start,
|
|
1491
|
-
end: Math.max(
|
|
1572
|
+
end: Math.max(cap(turnEnd), start + 1),
|
|
1492
1573
|
});
|
|
1493
1574
|
} else {
|
|
1494
|
-
const end =
|
|
1575
|
+
const end = cap(nextRowStart ?? start + 800);
|
|
1495
1576
|
out.push({
|
|
1496
1577
|
kind: "row",
|
|
1497
1578
|
row: item.row,
|
|
1498
1579
|
primaryIndex: rowPrimaryIndex(item.row),
|
|
1499
1580
|
start,
|
|
1500
|
-
end,
|
|
1581
|
+
end: Math.max(end, start + 1),
|
|
1501
1582
|
});
|
|
1502
1583
|
}
|
|
1503
1584
|
}
|
|
1504
1585
|
|
|
1586
|
+
// Idle bands are computed from raw event timestamps (5s threshold) up
|
|
1587
|
+
// in the parent — append them and let the sort below interleave by
|
|
1588
|
+
// start time so they slot between rows correctly.
|
|
1589
|
+
for (const band of idleBands) {
|
|
1590
|
+
out.push({
|
|
1591
|
+
kind: "idle",
|
|
1592
|
+
start: band.start,
|
|
1593
|
+
end: band.end,
|
|
1594
|
+
durationMs: band.durationMs,
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
out.sort((a, b) => a.start - b.start);
|
|
1598
|
+
|
|
1505
1599
|
// Cap the number of rendered segments. Each <rect> carries two
|
|
1506
1600
|
// event handlers (onMouseEnter + onClick), so 2000 raw-events in
|
|
1507
1601
|
// "All events" mode becomes 4000 DOM event listeners — enough to
|
|
@@ -1546,7 +1640,7 @@ function Minimap({
|
|
|
1546
1640
|
}
|
|
1547
1641
|
}
|
|
1548
1642
|
return bucketed;
|
|
1549
|
-
}, [displayRows, safeDur]);
|
|
1643
|
+
}, [displayRows, safeDur, rawIdleBands]);
|
|
1550
1644
|
|
|
1551
1645
|
/* Sequential layout with a minimum displayed width per segment.
|
|
1552
1646
|
- Raw proportional width = (seg.end - seg.start) / safeDur * WIDTH
|
|
@@ -2514,6 +2608,7 @@ function TranscriptList({
|
|
|
2514
2608
|
isSessionLive,
|
|
2515
2609
|
team,
|
|
2516
2610
|
model,
|
|
2611
|
+
rawIdleBands,
|
|
2517
2612
|
}: {
|
|
2518
2613
|
displayRows: DisplayRow[];
|
|
2519
2614
|
rowRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
|
|
@@ -2524,6 +2619,10 @@ function TranscriptList({
|
|
|
2524
2619
|
isSessionLive?: boolean;
|
|
2525
2620
|
team?: (TimelineData & { teamName: string }) | null;
|
|
2526
2621
|
model?: string;
|
|
2622
|
+
/** Anchor-to-anchor idle bands. The body emits IdleDivider before any
|
|
2623
|
+
* row whose start matches a band's end, so Codex sessions get the same
|
|
2624
|
+
* "Session idle X minutes" markers Claude has always had. */
|
|
2625
|
+
rawIdleBands?: { start: number; end: number; durationMs: number }[];
|
|
2527
2626
|
}) {
|
|
2528
2627
|
// Find the last collapsed turn index so we can mark it as in-progress
|
|
2529
2628
|
// when the session is live.
|
|
@@ -2537,9 +2636,38 @@ function TranscriptList({
|
|
|
2537
2636
|
}
|
|
2538
2637
|
}
|
|
2539
2638
|
|
|
2639
|
+
const idleBandList = (rawIdleBands ?? []).slice().sort((a, b) => a.start - b.start);
|
|
2640
|
+
/** Returns the durationMs of the idle band that ends at (or just before)
|
|
2641
|
+
* the given row's tOffset, or null if no band matches. Tolerance is 1s
|
|
2642
|
+
* to absorb Date.parse rounding differences between anchor.ms and the
|
|
2643
|
+
* presentation row's tOffsetMs. */
|
|
2644
|
+
const idleBeforeOffset = (tOff: number | undefined): number | null => {
|
|
2645
|
+
if (tOff === undefined) return null;
|
|
2646
|
+
for (const b of idleBandList) {
|
|
2647
|
+
if (Math.abs(b.end - tOff) < 1000) return b.durationMs;
|
|
2648
|
+
}
|
|
2649
|
+
return null;
|
|
2650
|
+
};
|
|
2651
|
+
/** Pull a t-offset off whichever display-row variant we have. */
|
|
2652
|
+
const rowTOffset = (d: DisplayRow): number | undefined => {
|
|
2653
|
+
if (d.kind === "turn-collapsed" || d.kind === "turn-expanded-header") return d.turn.tOffsetMs;
|
|
2654
|
+
if (d.kind === "turn-expanded-footer") return undefined;
|
|
2655
|
+
return d.row.tOffsetMs;
|
|
2656
|
+
};
|
|
2657
|
+
|
|
2540
2658
|
const out: React.ReactNode[] = [];
|
|
2541
2659
|
for (let i = 0; i < displayRows.length; i++) {
|
|
2542
2660
|
const d = displayRows[i];
|
|
2661
|
+
// Emit a "Session idle" divider before any row that starts right after
|
|
2662
|
+
// an anchor-to-anchor idle band. Skips indented (child) rows so an
|
|
2663
|
+
// expanded turn doesn't render dividers for its inner steps.
|
|
2664
|
+
const indented = d.kind === "presentation" ? d.indented : false;
|
|
2665
|
+
if (!indented) {
|
|
2666
|
+
const idleMs = idleBeforeOffset(rowTOffset(d));
|
|
2667
|
+
if (idleMs !== null && idleMs > IDLE_THRESHOLD_MS) {
|
|
2668
|
+
out.push(<IdleDivider key={`idle-before-${i}`} gapMs={idleMs} />);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2543
2671
|
|
|
2544
2672
|
// Collapsed turn summary row
|
|
2545
2673
|
if (d.kind === "turn-collapsed") {
|
|
@@ -2592,12 +2720,10 @@ function TranscriptList({
|
|
|
2592
2720
|
continue;
|
|
2593
2721
|
}
|
|
2594
2722
|
|
|
2595
|
-
// Normal presentation row (possibly indented as a child of an expanded
|
|
2723
|
+
// Normal presentation row (possibly indented as a child of an expanded
|
|
2724
|
+
// turn). Idle dividers are now emitted at the top of the loop from the
|
|
2725
|
+
// shared rawIdleBands list — no separate gap-before-user check needed.
|
|
2596
2726
|
const r = d.row;
|
|
2597
|
-
const gap = r.gapMs ?? 0;
|
|
2598
|
-
if (r.kind === "user" && gap > IDLE_THRESHOLD_MS && !d.indented) {
|
|
2599
|
-
out.push(<IdleDivider key={`idle-${rowPrimaryIndex(r)}`} gapMs={gap} />);
|
|
2600
|
-
}
|
|
2601
2727
|
const idx = rowPrimaryIndex(r);
|
|
2602
2728
|
out.push(
|
|
2603
2729
|
<TranscriptRow
|
|
@@ -4699,7 +4825,7 @@ function EntryDayStrip({ entry }: { entry: Entry }) {
|
|
|
4699
4825
|
📅 {fmtDate}
|
|
4700
4826
|
</span>
|
|
4701
4827
|
{isTrivial ? (
|
|
4702
|
-
<OutcomePill outcome="trivial" size="md" />
|
|
4828
|
+
<OutcomePill outcome="trivial" size="md" agent={entry.agent} />
|
|
4703
4829
|
) : isPending ? (
|
|
4704
4830
|
<OutcomePill
|
|
4705
4831
|
outcome={null}
|
|
@@ -4707,9 +4833,10 @@ function EntryDayStrip({ entry }: { entry: Entry }) {
|
|
|
4707
4833
|
sessionId={entry.session_id}
|
|
4708
4834
|
localDay={entry.local_day}
|
|
4709
4835
|
size="md"
|
|
4836
|
+
agent={entry.agent}
|
|
4710
4837
|
/>
|
|
4711
4838
|
) : enr.outcome ? (
|
|
4712
|
-
<OutcomePill outcome={enr.outcome} size="md" />
|
|
4839
|
+
<OutcomePill outcome={enr.outcome} size="md" agent={entry.agent} />
|
|
4713
4840
|
) : null}
|
|
4714
4841
|
<span
|
|
4715
4842
|
style={{
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
1
2
|
import { listSessions } from "@/lib/data";
|
|
2
3
|
import { buildEntriesIndex } from "@/lib/entries-index";
|
|
4
|
+
import { agentSources } from "@claude-lens/parser/fs";
|
|
3
5
|
import { SessionsGrid, type SessionRow } from "./sessions-grid";
|
|
4
6
|
|
|
5
7
|
export const dynamic = "force-dynamic";
|
|
6
8
|
|
|
9
|
+
const HOME = homedir();
|
|
10
|
+
const codeStyle = {
|
|
11
|
+
fontFamily: "var(--font-mono)",
|
|
12
|
+
fontSize: 11,
|
|
13
|
+
background: "var(--af-border-subtle)",
|
|
14
|
+
padding: "1px 6px",
|
|
15
|
+
borderRadius: 4,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
7
18
|
export default async function SessionsPage() {
|
|
8
19
|
const [sessions, index] = await Promise.all([listSessions(), buildEntriesIndex()]);
|
|
9
20
|
|
|
@@ -29,18 +40,17 @@ export default async function SessionsPage() {
|
|
|
29
40
|
All sessions
|
|
30
41
|
</h1>
|
|
31
42
|
<p style={{ fontSize: 13, color: "var(--af-text-secondary)", marginTop: 4 }}>
|
|
32
|
-
{sessions.length} session{sessions.length === 1 ? "" : "s"}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</code>
|
|
43
|
+
{sessions.length} session{sessions.length === 1 ? "" : "s"} across{" "}
|
|
44
|
+
{agentSources.map((source, i) => (
|
|
45
|
+
<span key={source.kind}>
|
|
46
|
+
{i > 0 && " + "}
|
|
47
|
+
<code style={codeStyle}>
|
|
48
|
+
{source.defaultRoot.startsWith(HOME)
|
|
49
|
+
? "~" + source.defaultRoot.slice(HOME.length)
|
|
50
|
+
: source.defaultRoot}
|
|
51
|
+
</code>
|
|
52
|
+
</span>
|
|
53
|
+
))}
|
|
44
54
|
</p>
|
|
45
55
|
</header>
|
|
46
56
|
<SessionsGrid rows={rows} />
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import {
|
|
5
|
-
import { useRouter } from "next/navigation";
|
|
6
|
-
import type { SessionMeta } from "@claude-lens/parser";
|
|
4
|
+
import { useCallback, useMemo } from "react";
|
|
5
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
6
|
+
import type { AgentKind, SessionMeta } from "@claude-lens/parser";
|
|
7
|
+
import { getAgentMetadata, isAgentKind } from "@claude-lens/parser";
|
|
7
8
|
import type { DayOutcome, EntryEnrichmentStatus } from "@claude-lens/entries";
|
|
8
9
|
import {
|
|
9
10
|
formatDuration,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
import { Search, Wrench, MessagesSquare, Clock } from "lucide-react";
|
|
16
17
|
import { LiveBadge } from "@/components/live-badge";
|
|
17
18
|
import { TeamBadge } from "@/components/team-badge";
|
|
19
|
+
import { AgentBadge } from "@/components/agent-badge";
|
|
18
20
|
import { DataTable, type Column } from "@/components/data-table";
|
|
19
21
|
import { useViewToggle } from "@/components/view-toggle";
|
|
20
22
|
import { OutcomePill, outcomePriority } from "@/components/outcome-pill";
|
|
@@ -27,21 +29,66 @@ export type SessionRow = {
|
|
|
27
29
|
latestLocalDay: string | null;
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
type SortBy = "newest" | "longest" | "most-tokens" | "outcome";
|
|
33
|
+
|
|
34
|
+
const SORT_VALUES: readonly SortBy[] = ["newest", "longest", "most-tokens", "outcome"] as const;
|
|
35
|
+
|
|
36
|
+
function isSortBy(s: string | null | undefined): s is SortBy {
|
|
37
|
+
return s !== null && s !== undefined && (SORT_VALUES as readonly string[]).includes(s);
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
31
41
|
const router = useRouter();
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
42
|
+
const pathname = usePathname();
|
|
43
|
+
const searchParams = useSearchParams();
|
|
44
|
+
|
|
45
|
+
// Filter state lives in the URL — back/forward replays the same view, and
|
|
46
|
+
// returning from a session detail page restores what the user had picked.
|
|
47
|
+
const query = searchParams.get("q") ?? "";
|
|
48
|
+
const project = searchParams.get("project") ?? "all";
|
|
49
|
+
const agentParam = searchParams.get("agent");
|
|
50
|
+
const agent: AgentKind | "all" = isAgentKind(agentParam) ? agentParam : "all";
|
|
51
|
+
const sortBy: SortBy = isSortBy(searchParams.get("sort"))
|
|
52
|
+
? (searchParams.get("sort") as SortBy)
|
|
53
|
+
: "newest";
|
|
35
54
|
const { mode: viewMode, toggle: viewToggle } = useViewToggle("cclens:sessions:view");
|
|
36
55
|
|
|
56
|
+
const updateParam = useCallback(
|
|
57
|
+
(key: string, value: string, fallback: string) => {
|
|
58
|
+
const next = new URLSearchParams(searchParams.toString());
|
|
59
|
+
if (!value || value === fallback) next.delete(key);
|
|
60
|
+
else next.set(key, value);
|
|
61
|
+
const qs = next.toString();
|
|
62
|
+
// replace, not push — we don't want a history entry per keystroke.
|
|
63
|
+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
64
|
+
},
|
|
65
|
+
[pathname, router, searchParams],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const setQuery = (v: string) => updateParam("q", v, "");
|
|
69
|
+
const setProject = (v: string) => updateParam("project", v, "all");
|
|
70
|
+
const setAgent = (v: AgentKind | "all") => updateParam("agent", v, "all");
|
|
71
|
+
const setSortBy = (v: SortBy) => updateParam("sort", v, "newest");
|
|
72
|
+
|
|
37
73
|
const projects = useMemo(() => {
|
|
38
74
|
const s = new Set(rows.map((r) => r.session.projectName));
|
|
39
75
|
return ["all", ...Array.from(s).sort()];
|
|
40
76
|
}, [rows]);
|
|
41
77
|
|
|
78
|
+
const agentOptions = useMemo(() => {
|
|
79
|
+
const counts = new Map<AgentKind, number>();
|
|
80
|
+
for (const r of rows) {
|
|
81
|
+
const k = (r.session.agent ?? "claude-code") as AgentKind;
|
|
82
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
83
|
+
}
|
|
84
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);
|
|
85
|
+
}, [rows]);
|
|
86
|
+
|
|
42
87
|
const filtered = useMemo(() => {
|
|
43
88
|
let items = rows.slice();
|
|
44
89
|
if (project !== "all") items = items.filter((r) => r.session.projectName === project);
|
|
90
|
+
if (agent !== "all")
|
|
91
|
+
items = items.filter((r) => (r.session.agent ?? "claude-code") === agent);
|
|
45
92
|
if (query) {
|
|
46
93
|
const q = query.toLowerCase();
|
|
47
94
|
items = items.filter(
|
|
@@ -78,7 +125,7 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
78
125
|
else if (sortBy === "outcome")
|
|
79
126
|
items.sort((a, b) => outcomePriority(b.outcome) - outcomePriority(a.outcome));
|
|
80
127
|
return items;
|
|
81
|
-
}, [rows, project, query, sortBy]);
|
|
128
|
+
}, [rows, project, agent, query, sortBy]);
|
|
82
129
|
|
|
83
130
|
return (
|
|
84
131
|
<div>
|
|
@@ -116,6 +163,20 @@ export function SessionsGrid({ rows }: { rows: SessionRow[] }) {
|
|
|
116
163
|
</option>
|
|
117
164
|
))}
|
|
118
165
|
</select>
|
|
166
|
+
{agentOptions.length > 1 && (
|
|
167
|
+
<select
|
|
168
|
+
value={agent}
|
|
169
|
+
onChange={(e) => setAgent(e.target.value as typeof agent)}
|
|
170
|
+
title="Filter by source agent"
|
|
171
|
+
>
|
|
172
|
+
<option value="all">All agents</option>
|
|
173
|
+
{agentOptions.map(([k, count]) => (
|
|
174
|
+
<option key={k} value={k}>
|
|
175
|
+
{agentLabel(k)} ({count})
|
|
176
|
+
</option>
|
|
177
|
+
))}
|
|
178
|
+
</select>
|
|
179
|
+
)}
|
|
119
180
|
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as typeof sortBy)}>
|
|
120
181
|
<option value="newest">Newest</option>
|
|
121
182
|
<option value="longest">Longest</option>
|
|
@@ -196,6 +257,7 @@ const sessionTableColumns: Column<SessionRow>[] = [
|
|
|
196
257
|
{prettyProjectName(r.session.projectName)}
|
|
197
258
|
</span>
|
|
198
259
|
<TeamBadge session={r.session} />
|
|
260
|
+
<AgentBadge agent={r.session.agent} />
|
|
199
261
|
</div>
|
|
200
262
|
<div
|
|
201
263
|
style={{
|
|
@@ -319,7 +381,9 @@ const sessionTableColumns: Column<SessionRow>[] = [
|
|
|
319
381
|
];
|
|
320
382
|
|
|
321
383
|
function OutcomeCell({ row }: { row: SessionRow }) {
|
|
322
|
-
if (row.outcome)
|
|
384
|
+
if (row.outcome) {
|
|
385
|
+
return <OutcomePill outcome={row.outcome} size="sm" label="text" agent={row.session.agent} />;
|
|
386
|
+
}
|
|
323
387
|
if (row.enrichmentStatus && row.latestLocalDay && row.enrichmentStatus !== "skipped_trivial") {
|
|
324
388
|
return (
|
|
325
389
|
<OutcomePill
|
|
@@ -328,6 +392,7 @@ function OutcomeCell({ row }: { row: SessionRow }) {
|
|
|
328
392
|
sessionId={row.session.id}
|
|
329
393
|
localDay={row.latestLocalDay}
|
|
330
394
|
size="sm"
|
|
395
|
+
agent={row.session.agent}
|
|
331
396
|
/>
|
|
332
397
|
);
|
|
333
398
|
}
|
|
@@ -383,6 +448,7 @@ function SessionCard({ row }: { row: SessionRow }) {
|
|
|
383
448
|
{prettyProjectName(s.projectName)}
|
|
384
449
|
</span>
|
|
385
450
|
<TeamBadge session={s} linkable={false} />
|
|
451
|
+
<AgentBadge agent={s.agent} />
|
|
386
452
|
</div>
|
|
387
453
|
<div
|
|
388
454
|
style={{
|
|
@@ -400,7 +466,7 @@ function SessionCard({ row }: { row: SessionRow }) {
|
|
|
400
466
|
{(outcome || showPending) && (
|
|
401
467
|
<div onClick={(e) => e.stopPropagation()}>
|
|
402
468
|
{outcome ? (
|
|
403
|
-
<OutcomePill outcome={outcome} size="md" />
|
|
469
|
+
<OutcomePill outcome={outcome} size="md" agent={s.agent} />
|
|
404
470
|
) : showPending ? (
|
|
405
471
|
<OutcomePill
|
|
406
472
|
outcome={null}
|
|
@@ -408,6 +474,7 @@ function SessionCard({ row }: { row: SessionRow }) {
|
|
|
408
474
|
sessionId={s.id}
|
|
409
475
|
localDay={latestLocalDay!}
|
|
410
476
|
size="md"
|
|
477
|
+
agent={s.agent}
|
|
411
478
|
/>
|
|
412
479
|
) : null}
|
|
413
480
|
</div>
|
|
@@ -481,3 +548,9 @@ function Stat({ icon, label }: { icon: React.ReactNode; label: string }) {
|
|
|
481
548
|
</span>
|
|
482
549
|
);
|
|
483
550
|
}
|
|
551
|
+
|
|
552
|
+
// Registry-driven label — reads displayName off the agent metadata so
|
|
553
|
+
// new agents auto-show up with their declared name.
|
|
554
|
+
function agentLabel(kind: AgentKind): string {
|
|
555
|
+
return getAgentMetadata(kind)?.displayName ?? kind;
|
|
556
|
+
}
|
|
@@ -4,11 +4,13 @@ import { useState } from "react";
|
|
|
4
4
|
type Initial = {
|
|
5
5
|
enabled: boolean;
|
|
6
6
|
autoBackfillLastWeek: boolean;
|
|
7
|
+
autoBackfillYesterday: boolean;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
export function AiFeaturesForm({ initial }: { initial: Initial }) {
|
|
10
11
|
const [enabled, setEnabled] = useState(initial.enabled);
|
|
11
12
|
const [autoBackfillLastWeek, setAutoBackfillLastWeek] = useState(initial.autoBackfillLastWeek);
|
|
13
|
+
const [autoBackfillYesterday, setAutoBackfillYesterday] = useState(initial.autoBackfillYesterday);
|
|
12
14
|
const [saving, setSaving] = useState(false);
|
|
13
15
|
const [savedMsg, setSavedMsg] = useState<string | null>(null);
|
|
14
16
|
|
|
@@ -20,7 +22,7 @@ export function AiFeaturesForm({ initial }: { initial: Initial }) {
|
|
|
20
22
|
method: "PUT",
|
|
21
23
|
headers: { "content-type": "application/json" },
|
|
22
24
|
body: JSON.stringify({
|
|
23
|
-
ai_features: { enabled, autoBackfillLastWeek },
|
|
25
|
+
ai_features: { enabled, autoBackfillLastWeek, autoBackfillYesterday },
|
|
24
26
|
}),
|
|
25
27
|
});
|
|
26
28
|
setSaving(false);
|
|
@@ -34,6 +36,23 @@ export function AiFeaturesForm({ initial }: { initial: Initial }) {
|
|
|
34
36
|
<span>Enable AI digests and enrichment</span>
|
|
35
37
|
</label>
|
|
36
38
|
|
|
39
|
+
<label className={`flex items-start gap-2 ${enabled ? "" : "opacity-50"}`}>
|
|
40
|
+
<input
|
|
41
|
+
type="checkbox"
|
|
42
|
+
checked={autoBackfillYesterday}
|
|
43
|
+
disabled={!enabled}
|
|
44
|
+
onChange={e => setAutoBackfillYesterday(e.target.checked)}
|
|
45
|
+
/>
|
|
46
|
+
<span>
|
|
47
|
+
Auto-backfill yesterday's digest on daemon start
|
|
48
|
+
<span className="block text-xs text-gray-500 mt-0.5">
|
|
49
|
+
Generates yesterday's enriched entries and day digest. Runs once
|
|
50
|
+
per daemon boot. Without this, yesterday's digest only appears
|
|
51
|
+
after you load the homepage.
|
|
52
|
+
</span>
|
|
53
|
+
</span>
|
|
54
|
+
</label>
|
|
55
|
+
|
|
37
56
|
<label className={`flex items-start gap-2 ${enabled ? "" : "opacity-50"}`}>
|
|
38
57
|
<input
|
|
39
58
|
type="checkbox"
|
|
@@ -16,6 +16,7 @@ export default function SettingsPage() {
|
|
|
16
16
|
<AiFeaturesForm initial={{
|
|
17
17
|
enabled: s.ai_features.enabled,
|
|
18
18
|
autoBackfillLastWeek: s.ai_features.autoBackfillLastWeek,
|
|
19
|
+
autoBackfillYesterday: s.ai_features.autoBackfillYesterday,
|
|
19
20
|
}} />
|
|
20
21
|
</section>
|
|
21
22
|
</main>
|