claude-discover 1.9.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/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/claude/hooks.mjs +102 -0
- package/bin/claude/statusline.mjs +130 -0
- package/bin/claude-discover.mjs +16 -0
- package/bin/proxy.config.js +19 -0
- package/bin/proxy.mjs +268 -0
- package/out/main/main.js +1542 -0
- package/out/preload/findPreload.cjs +16 -0
- package/out/preload/preload.cjs +28 -0
- package/out/renderer/assets/claude-icon-BqihzN4C.svg +3 -0
- package/out/renderer/assets/find-CxcUmpSm.css +88 -0
- package/out/renderer/assets/find-DSEw-KLs.js +59 -0
- package/out/renderer/assets/index-0bBLuYQv.css +2331 -0
- package/out/renderer/assets/index-BLwpO6Wp.js +2629 -0
- package/out/renderer/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/out/renderer/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/out/renderer/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/out/renderer/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/out/renderer/assets/jetbrains-mono-latin-200-normal-CHn02WOn.woff +0 -0
- package/out/renderer/assets/jetbrains-mono-latin-200-normal-DvzYDkvL.woff2 +0 -0
- package/out/renderer/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/out/renderer/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/out/renderer/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/out/renderer/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/out/renderer/assets/vendors-B4XMnGdr.css +153 -0
- package/out/renderer/assets/vendors-D6iOvAG3.js +49248 -0
- package/out/renderer/find/find.html +26 -0
- package/out/renderer/index.html +17 -0
- package/package.json +75 -0
|
@@ -0,0 +1,2629 @@
|
|
|
1
|
+
import { r as reactExports, j as jsxRuntimeExports, f as format, s as startOfWeek, a as startOfMonth, b as startOfDay, e as endOfWeek, c as endOfMonth, d as endOfDay, g as addWeeks, h as addMonths, i as addDays, k as isSameWeek, l as isSameMonth, m as isSameDay, n as intervalToDuration, o as getDaysInMonth, p as addHours, H as Hourglass, G as Gauge, M as MessageSquare, C as Clock, Z as Zap, D as DollarSign, R as React, q as Markdown$1, t as rehypeHighlight, u as remarkGfm, T as Terminal, J as JsonView, v as vscodeTheme, w as tippy, x as reactDomExports, y as sn, X as Xt, z as Zt, A as tn, B as delegate, E as clientExports } from "./vendors-D6iOvAG3.js";
|
|
2
|
+
const SOURCE_COLORS = {
|
|
3
|
+
scheduled: "#14b8a6",
|
|
4
|
+
cli: "#3b82f6",
|
|
5
|
+
"claude-desktop": "#22c55e",
|
|
6
|
+
"sdk-cli": "#c084fc",
|
|
7
|
+
"sdk-py": "#6f3eca",
|
|
8
|
+
"sdk-ts": "#a851db",
|
|
9
|
+
other: "#94a3b8"
|
|
10
|
+
};
|
|
11
|
+
const SOURCE_ORDER = ["cli", "claude-desktop", "sdk-cli", "sdk-ts", "sdk-py", "scheduled", "other"];
|
|
12
|
+
const SOURCE_LABELS = {
|
|
13
|
+
scheduled: "Scheduled",
|
|
14
|
+
cli: "CLI",
|
|
15
|
+
"claude-desktop": "Desktop",
|
|
16
|
+
"sdk-cli": "SDK (CLI)",
|
|
17
|
+
"sdk-ts": "SDK (TS)",
|
|
18
|
+
"sdk-py": "SDK (Py)",
|
|
19
|
+
other: "Other"
|
|
20
|
+
};
|
|
21
|
+
const VERSION_KEY = "version";
|
|
22
|
+
function useLocalStorage(key, initial) {
|
|
23
|
+
const [value, setValue] = reactExports.useState(() => {
|
|
24
|
+
const raw = localStorage.getItem(key);
|
|
25
|
+
if (raw === null) {
|
|
26
|
+
return typeof initial === "function" ? initial() : initial;
|
|
27
|
+
}
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
});
|
|
30
|
+
reactExports.useEffect(() => {
|
|
31
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
32
|
+
}, [key, value]);
|
|
33
|
+
return [value, setValue];
|
|
34
|
+
}
|
|
35
|
+
function clearOutdatedLocalStorage() {
|
|
36
|
+
const version = localStorage.getItem(VERSION_KEY);
|
|
37
|
+
if (version !== "1.9.0") {
|
|
38
|
+
const [MAJOR, MINOR, PATCH] = "1.9.0".split(".");
|
|
39
|
+
const [major, minor, patch] = (version ?? "").split(".");
|
|
40
|
+
if (MAJOR !== major || MINOR !== minor) {
|
|
41
|
+
localStorage.clear();
|
|
42
|
+
}
|
|
43
|
+
localStorage.setItem(VERSION_KEY, "1.9.0");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function useMouseFontScale(storageKey) {
|
|
47
|
+
const [scale, setScale] = useLocalStorage(storageKey, 1);
|
|
48
|
+
const ref = reactExports.useRef(null);
|
|
49
|
+
useMouse(ref, {
|
|
50
|
+
onZoom: (e) => {
|
|
51
|
+
const dir = (e.deltaY || e.deltaX) < 0 ? 1 : -1;
|
|
52
|
+
setScale((s) => Math.min(2, Math.max(0.7, Math.round((s + dir * 0.1) * 10) / 10)));
|
|
53
|
+
},
|
|
54
|
+
onReset: () => setScale(1)
|
|
55
|
+
});
|
|
56
|
+
return [scale, ref];
|
|
57
|
+
}
|
|
58
|
+
function useMouse(ref, handlers) {
|
|
59
|
+
const fns = reactExports.useRef(handlers);
|
|
60
|
+
fns.current = handlers;
|
|
61
|
+
reactExports.useEffect(() => {
|
|
62
|
+
const el = ref.current;
|
|
63
|
+
const wheel = (e) => {
|
|
64
|
+
const { onZoom, onPan } = fns.current;
|
|
65
|
+
if (e.shiftKey || e.ctrlKey) {
|
|
66
|
+
if (!onZoom) return;
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
onZoom(e);
|
|
69
|
+
} else if (onPan && e.deltaX !== 0) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
onPan(e.deltaX);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const dragToPan = (e) => {
|
|
75
|
+
let startX = e.clientX;
|
|
76
|
+
const move = (me) => {
|
|
77
|
+
fns.current.onPan(startX - me.clientX);
|
|
78
|
+
startX = me.clientX;
|
|
79
|
+
};
|
|
80
|
+
const up = () => {
|
|
81
|
+
document.removeEventListener("mousemove", move);
|
|
82
|
+
document.removeEventListener("mouseup", up);
|
|
83
|
+
};
|
|
84
|
+
document.addEventListener("mousemove", move);
|
|
85
|
+
document.addEventListener("mouseup", up);
|
|
86
|
+
};
|
|
87
|
+
const mousedown = (e) => {
|
|
88
|
+
const { onReset, onPan } = fns.current;
|
|
89
|
+
if (e.shiftKey || e.ctrlKey) {
|
|
90
|
+
if (e.button !== 1 || !onReset) return;
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
onReset(e);
|
|
93
|
+
} else if (onPan && !e.target.closest?.("[data-no-pan]")) {
|
|
94
|
+
dragToPan(e);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
el.addEventListener("wheel", wheel, { passive: false });
|
|
98
|
+
el.addEventListener("mousedown", mousedown);
|
|
99
|
+
return () => {
|
|
100
|
+
el.removeEventListener("wheel", wheel);
|
|
101
|
+
el.removeEventListener("mousedown", mousedown);
|
|
102
|
+
};
|
|
103
|
+
}, [ref]);
|
|
104
|
+
}
|
|
105
|
+
const isJournal = (s) => s.sessionId.endsWith("-journal");
|
|
106
|
+
function subagentsByParent(sessions) {
|
|
107
|
+
const ids = new Set(sessions.map((s) => s.sessionId));
|
|
108
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const s of sessions) {
|
|
110
|
+
if (!ids.has(s.parentSessionId)) continue;
|
|
111
|
+
let arr = byParent.get(s.parentSessionId);
|
|
112
|
+
if (!arr) byParent.set(s.parentSessionId, arr = []);
|
|
113
|
+
arr.push(s);
|
|
114
|
+
}
|
|
115
|
+
return byParent;
|
|
116
|
+
}
|
|
117
|
+
function subagentClusters(sessions, gap) {
|
|
118
|
+
return [...subagentsByParent(sessions).values()].flatMap((subs) => subs.sort((a, b) => a.startedAt - b.startedAt).reduce((runs, s, i, arr) => {
|
|
119
|
+
if (i && s.startedAt - arr[i - 1].lastActivityAt <= gap) runs.at(-1).push(s);
|
|
120
|
+
else runs.push([s]);
|
|
121
|
+
return runs;
|
|
122
|
+
}, []));
|
|
123
|
+
}
|
|
124
|
+
const BIN_HEIGHT_PX = 4;
|
|
125
|
+
const BIN_WIDTH_MINUTES = 5;
|
|
126
|
+
const BIN_WIDTH_MS = BIN_WIDTH_MINUTES * 6e4;
|
|
127
|
+
const STOPS = [
|
|
128
|
+
[2, "rgb(128 136 147 / 0.25)"],
|
|
129
|
+
// light
|
|
130
|
+
[5, "rgb(245 158 11 / 0.70)"],
|
|
131
|
+
// heavy
|
|
132
|
+
[10, "rgb(239 68 68 / 0.95)"],
|
|
133
|
+
// very expensive
|
|
134
|
+
[Infinity, "rgb(185 28 28 / 1.00)"]
|
|
135
|
+
// outlier
|
|
136
|
+
];
|
|
137
|
+
function HeatStrip({ sessions, viewStart, viewEnd, width, gutter }) {
|
|
138
|
+
const chartWidth = width - gutter;
|
|
139
|
+
const slotStart = Math.floor(viewStart / BIN_WIDTH_MS) * BIN_WIDTH_MS;
|
|
140
|
+
const binCount = Math.ceil((viewEnd - slotStart) / BIN_WIDTH_MS);
|
|
141
|
+
const binW = BIN_WIDTH_MS / (viewEnd - viewStart) * chartWidth;
|
|
142
|
+
const x0 = (slotStart - viewStart) / (viewEnd - viewStart) * chartWidth;
|
|
143
|
+
const bins = reactExports.useMemo(() => {
|
|
144
|
+
const arr = new Float64Array(binCount);
|
|
145
|
+
for (const s of sessions) {
|
|
146
|
+
for (const { start, end, cost } of s.costSamples ?? []) {
|
|
147
|
+
const turnEnd = Math.max(end, start + 1);
|
|
148
|
+
const ratePerMs = cost / (turnEnd - start);
|
|
149
|
+
const b0 = Math.max(0, Math.floor((start - slotStart) / BIN_WIDTH_MS));
|
|
150
|
+
const b1 = Math.min(binCount - 1, Math.floor((turnEnd - slotStart) / BIN_WIDTH_MS));
|
|
151
|
+
for (let b = b0; b <= b1; b++) {
|
|
152
|
+
const bs = slotStart + b * BIN_WIDTH_MS;
|
|
153
|
+
arr[b] += ratePerMs * (Math.min(turnEnd, bs + BIN_WIDTH_MS) - Math.max(start, bs));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return arr;
|
|
158
|
+
}, [sessions, slotStart, binCount]);
|
|
159
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width, height: BIN_HEIGHT_PX, className: "heat-strip", children: [
|
|
160
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: 0, y: 0, width: gutter, height: BIN_HEIGHT_PX, className: "gantt-gutter" }),
|
|
161
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("g", { transform: `translate(${gutter},0)`, children: Array.from(bins, (cost, i) => {
|
|
162
|
+
if (cost <= 0) return null;
|
|
163
|
+
const fill = STOPS.find(([max]) => cost < max)[1];
|
|
164
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: x0 + i * binW, y: 0, width: binW + 0.6, height: BIN_HEIGHT_PX, fill, children: /* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: `$${cost.toFixed(2)} in ${BIN_WIDTH_MINUTES}m` }) }, i);
|
|
165
|
+
}) })
|
|
166
|
+
] });
|
|
167
|
+
}
|
|
168
|
+
function minorIntervalFor(span) {
|
|
169
|
+
if (span < 15 * 6e4) return 6e4;
|
|
170
|
+
if (span < 60 * 6e4) return 5 * 6e4;
|
|
171
|
+
if (span < 3 * 36e5) return 10 * 6e4;
|
|
172
|
+
if (span < 12 * 36e5) return 30 * 6e4;
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function computeTicks({ viewStart, viewEnd, width, span }) {
|
|
176
|
+
if (span > 2 * 864e5) {
|
|
177
|
+
const ticks2 = [];
|
|
178
|
+
const d = new Date(viewStart);
|
|
179
|
+
d.setHours(0, 0, 0, 0);
|
|
180
|
+
if (d.getTime() < viewStart) d.setDate(d.getDate() + 1);
|
|
181
|
+
for (let t = d.getTime(); t <= viewEnd; d.setDate(d.getDate() + 1), t = d.getTime()) {
|
|
182
|
+
ticks2.push({ x: (t - viewStart) / span * width, label: format(t, "MMM d"), major: true });
|
|
183
|
+
}
|
|
184
|
+
return ticks2;
|
|
185
|
+
}
|
|
186
|
+
const step = minorIntervalFor(span) ?? 36e5;
|
|
187
|
+
const intervalMin = step / 6e4;
|
|
188
|
+
const t0 = new Date(viewStart);
|
|
189
|
+
t0.setSeconds(0, 0);
|
|
190
|
+
t0.setMinutes(t0.getMinutes() - t0.getMinutes() % intervalMin);
|
|
191
|
+
if (t0.getTime() < viewStart) t0.setMinutes(t0.getMinutes() + intervalMin);
|
|
192
|
+
const ticks = [];
|
|
193
|
+
for (let t = t0.getTime(); t <= viewEnd; t += step) {
|
|
194
|
+
const d = new Date(t);
|
|
195
|
+
const major = d.getMinutes() === 0;
|
|
196
|
+
const label = major ? String(d.getHours()).padStart(2, "0") : `:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
197
|
+
ticks.push({ x: (t - viewStart) / span * width, label, major });
|
|
198
|
+
}
|
|
199
|
+
return ticks;
|
|
200
|
+
}
|
|
201
|
+
function TimeAxis({ viewStart, viewEnd, width, span, headerHeight }) {
|
|
202
|
+
const ticks = computeTicks({ viewStart, viewEnd, width, span });
|
|
203
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "ticks", children: ticks.map((tk, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(${tk.x},0)`, children: [
|
|
204
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { y1: 0, y2: headerHeight, className: `tick-hline ${tk.major ? "tick-major" : "tick-minor"}` }),
|
|
205
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: 4, y: 14, className: `tick-text ${tk.major ? "tick-major" : "tick-minor"}`, children: tk.label })
|
|
206
|
+
] }, i)) });
|
|
207
|
+
}
|
|
208
|
+
const SNAP_MIN = 5;
|
|
209
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
210
|
+
const fmtHM = (m) => `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
|
|
211
|
+
const parseHM = (s) => {
|
|
212
|
+
const [h, m] = s.split(":").map(Number);
|
|
213
|
+
return h * 60 + m;
|
|
214
|
+
};
|
|
215
|
+
const weekendSpans = (viewStart, viewEnd) => {
|
|
216
|
+
const spans = [];
|
|
217
|
+
const d = new Date(viewStart);
|
|
218
|
+
d.setHours(0, 0, 0, 0);
|
|
219
|
+
while (d.getTime() < viewEnd) {
|
|
220
|
+
const day = d.getDay();
|
|
221
|
+
if (day === 0 || day === 6) {
|
|
222
|
+
const start = d.getTime();
|
|
223
|
+
const end = new Date(d).setDate(d.getDate() + 1);
|
|
224
|
+
spans.push([clamp(start, viewStart, viewEnd), clamp(end, viewStart, viewEnd)]);
|
|
225
|
+
}
|
|
226
|
+
d.setDate(d.getDate() + 1);
|
|
227
|
+
}
|
|
228
|
+
return spans;
|
|
229
|
+
};
|
|
230
|
+
function WorkTimeOverlay({
|
|
231
|
+
showWorkBand = true,
|
|
232
|
+
dayStart,
|
|
233
|
+
viewStart,
|
|
234
|
+
viewEnd,
|
|
235
|
+
chartLeft,
|
|
236
|
+
chartWidth,
|
|
237
|
+
totalHeight,
|
|
238
|
+
headerHeight,
|
|
239
|
+
containerRef
|
|
240
|
+
}) {
|
|
241
|
+
const [workTime, setWorkTime] = reactExports.useState(null);
|
|
242
|
+
const [dragging, setDragging] = reactExports.useState(null);
|
|
243
|
+
const [now, setNow] = reactExports.useState(() => Date.now());
|
|
244
|
+
reactExports.useEffect(() => {
|
|
245
|
+
window.api.getWorkHours().then(({ work_hours: w }) => setWorkTime({ startMin: parseHM(w.start), endMin: parseHM(w.end) }));
|
|
246
|
+
}, []);
|
|
247
|
+
reactExports.useEffect(() => {
|
|
248
|
+
const id = setInterval(() => setNow(Date.now()), 6e4);
|
|
249
|
+
return () => clearInterval(id);
|
|
250
|
+
}, []);
|
|
251
|
+
reactExports.useEffect(() => {
|
|
252
|
+
if (workTime) window.api.setWorkHours({ work_hours: { start: fmtHM(workTime.startMin), end: fmtHM(workTime.endMin) } });
|
|
253
|
+
}, [workTime]);
|
|
254
|
+
const chartRight = chartLeft + chartWidth;
|
|
255
|
+
const xForMin = (min) => chartLeft + (dayStart + min * 6e4 - viewStart) / (viewEnd - viewStart) * chartWidth;
|
|
256
|
+
const xStart = workTime ? xForMin(workTime.startMin) : 0;
|
|
257
|
+
const xEnd = workTime ? xForMin(workTime.endMin) : 0;
|
|
258
|
+
const startDrag = (which) => (e) => {
|
|
259
|
+
e.stopPropagation();
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
setDragging(which);
|
|
262
|
+
const onMove = (m) => {
|
|
263
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
264
|
+
const ratio = clamp((m.clientX - rect.left - chartLeft) / chartWidth, 0, 1);
|
|
265
|
+
const ts = viewStart + ratio * (viewEnd - viewStart);
|
|
266
|
+
const min = clamp(Math.round((ts - dayStart) / 6e4 / SNAP_MIN) * SNAP_MIN, 0, 24 * 60);
|
|
267
|
+
setWorkTime((wt) => which === "start" ? { ...wt, startMin: Math.min(min, wt.endMin - SNAP_MIN) } : { ...wt, endMin: Math.max(min, wt.startMin + SNAP_MIN) });
|
|
268
|
+
};
|
|
269
|
+
const onUp = () => {
|
|
270
|
+
setDragging(null);
|
|
271
|
+
document.removeEventListener("mousemove", onMove);
|
|
272
|
+
document.removeEventListener("mouseup", onUp);
|
|
273
|
+
};
|
|
274
|
+
document.addEventListener("mousemove", onMove);
|
|
275
|
+
document.addEventListener("mouseup", onUp);
|
|
276
|
+
};
|
|
277
|
+
const renderHandle = (which, rawX, min) => {
|
|
278
|
+
if (rawX < chartLeft - 1 || rawX > chartRight + 1) return null;
|
|
279
|
+
const x = clamp(rawX, chartLeft, chartRight);
|
|
280
|
+
const isDragging = dragging === which;
|
|
281
|
+
const labelOnLeft = which === "end";
|
|
282
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "work-handle", "data-no-pan": "", onMouseDown: startDrag(which), children: [
|
|
283
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { className: "work-handle-hit", x: x - 7, y: 0, width: 14, height: totalHeight }),
|
|
284
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { className: "work-handle-line", x1: x, x2: x, y1: 0, y2: totalHeight }),
|
|
285
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { className: "work-handle-grip", x: x - 4, y: 6, width: 8, height: 16, rx: 2 }),
|
|
286
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: `Work ${which}: ${fmtHM(min)}` }),
|
|
287
|
+
isDragging && /* @__PURE__ */ jsxRuntimeExports.jsx("foreignObject", { x: labelOnLeft ? x - 50 : x + 6, y: 2, width: 48, height: 20, pointerEvents: "none", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "work-handle-label", children: fmtHM(min) }) })
|
|
288
|
+
] });
|
|
289
|
+
};
|
|
290
|
+
const shadeL = clamp(xStart, chartLeft, chartRight);
|
|
291
|
+
const shadeR = clamp(xEnd, chartLeft, chartRight);
|
|
292
|
+
const xForTs = (ts) => chartLeft + (ts - viewStart) / (viewEnd - viewStart) * chartWidth;
|
|
293
|
+
const showNow = now >= viewStart && now <= viewEnd;
|
|
294
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
|
|
295
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("pattern", { id: "work-offhours-stripes", width: 8, height: 8, patternUnits: "userSpaceOnUse", patternTransform: "rotate(45)", children: /* @__PURE__ */ jsxRuntimeExports.jsx("line", { className: "work-offhours-stripe", x1: 0, y1: 0, x2: 0, y2: 8 }) }) }),
|
|
296
|
+
showWorkBand && workTime && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "work-overlay", children: [
|
|
297
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { className: "work-offhours-shade", x: chartLeft, y: 0, width: Math.max(0, shadeL - chartLeft), height: totalHeight }),
|
|
298
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { className: "work-offhours-shade", x: shadeR, y: 0, width: Math.max(0, chartRight - shadeR), height: totalHeight }),
|
|
299
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "work-handles", children: [
|
|
300
|
+
renderHandle("start", xStart, workTime.startMin),
|
|
301
|
+
renderHandle("end", xEnd, workTime.endMin)
|
|
302
|
+
] })
|
|
303
|
+
] }),
|
|
304
|
+
!showWorkBand && /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "work-overlay", children: weekendSpans(viewStart, viewEnd).map(([start, end], i) => {
|
|
305
|
+
const x = clamp(xForTs(start), chartLeft, chartRight);
|
|
306
|
+
const w = clamp(xForTs(end), chartLeft, chartRight) - x;
|
|
307
|
+
if (w <= 0) return null;
|
|
308
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { className: "work-offhours-shade", x, y: 0, width: w, height: totalHeight }, i);
|
|
309
|
+
}) }),
|
|
310
|
+
showNow && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "now-line", children: [
|
|
311
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: xForTs(now), x2: xForTs(now), y1: 0, y2: totalHeight }),
|
|
312
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: xForTs(now) + 4, y: headerHeight - 2, children: "now" })
|
|
313
|
+
] })
|
|
314
|
+
] });
|
|
315
|
+
}
|
|
316
|
+
const HEADER_HEIGHT = 28;
|
|
317
|
+
const PROJECTS_WIDTH = 220;
|
|
318
|
+
const BARS = {
|
|
319
|
+
day: { height: 22, min_width: 4, row_gap: 4, group_gap: 8, radius: 3 },
|
|
320
|
+
week: { height: 15, min_width: 3, row_gap: 3, group_gap: 4, radius: 2 },
|
|
321
|
+
month: { height: 10, min_width: 2, row_gap: 2, group_gap: 2, radius: 1 }
|
|
322
|
+
};
|
|
323
|
+
const SUBAGENT_SPLIT_GAP = 5 * 6e4;
|
|
324
|
+
const endOf = (s) => Math.max(s.lastActivityAt, s.startedAt + 6e4);
|
|
325
|
+
function packLanes(items) {
|
|
326
|
+
const lanes = [];
|
|
327
|
+
const placed = items.slice().sort((a, b) => a.start - b.start).map((item) => {
|
|
328
|
+
let lane = lanes.findIndex((endTs) => endTs <= item.start);
|
|
329
|
+
if (lane === -1) {
|
|
330
|
+
lane = lanes.length;
|
|
331
|
+
lanes.push(item.end);
|
|
332
|
+
} else {
|
|
333
|
+
lanes[lane] = item.end;
|
|
334
|
+
}
|
|
335
|
+
return { item, lane };
|
|
336
|
+
});
|
|
337
|
+
return { placed, laneCount: Math.max(1, lanes.length) };
|
|
338
|
+
}
|
|
339
|
+
function GanttChart({
|
|
340
|
+
dayRange,
|
|
341
|
+
sessions,
|
|
342
|
+
onSelect,
|
|
343
|
+
selectedId,
|
|
344
|
+
dayAnchor,
|
|
345
|
+
granularity = "day",
|
|
346
|
+
projectFilter,
|
|
347
|
+
onToggleProjectFilter
|
|
348
|
+
}) {
|
|
349
|
+
const bar = BARS[granularity] ?? BARS.day;
|
|
350
|
+
const containerRef = reactExports.useRef(null);
|
|
351
|
+
const [width, setWidth] = reactExports.useState(1200);
|
|
352
|
+
const [view, setView] = useLocalStorage("gantt-chart.view", { dayAnchor, granularity, start: dayRange.start, end: dayRange.end });
|
|
353
|
+
reactExports.useEffect(() => {
|
|
354
|
+
if (view.dayAnchor !== dayAnchor || view.granularity !== granularity) {
|
|
355
|
+
setView({ dayAnchor, granularity, start: dayRange.start, end: dayRange.end });
|
|
356
|
+
}
|
|
357
|
+
}, [dayAnchor, granularity]);
|
|
358
|
+
reactExports.useEffect(() => {
|
|
359
|
+
if (!containerRef.current) return;
|
|
360
|
+
const ro = new ResizeObserver((entries) => {
|
|
361
|
+
for (const e of entries) setWidth(Math.max(400, e.contentRect.width));
|
|
362
|
+
});
|
|
363
|
+
ro.observe(containerRef.current);
|
|
364
|
+
return () => ro.disconnect();
|
|
365
|
+
}, []);
|
|
366
|
+
const { groups, totalHeight } = reactExports.useMemo(() => {
|
|
367
|
+
const clusters = subagentClusters(sessions, SUBAGENT_SPLIT_GAP).filter((c) => c.length >= 2);
|
|
368
|
+
const collapsed = new Set(clusters.flat().map((s) => s.sessionId));
|
|
369
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
370
|
+
const addItem = (s, item, cost) => {
|
|
371
|
+
const key = s.project || "(no project)";
|
|
372
|
+
let group = byKey.get(key);
|
|
373
|
+
if (!group) byKey.set(key, group = { projectShort: s.projectShort, items: [], cost: 0 });
|
|
374
|
+
group.items.push(item);
|
|
375
|
+
group.cost += cost || 0;
|
|
376
|
+
};
|
|
377
|
+
for (const s of sessions) {
|
|
378
|
+
if (collapsed.has(s.sessionId)) continue;
|
|
379
|
+
addItem(s, {
|
|
380
|
+
id: s.sessionId,
|
|
381
|
+
label: s.summary || s.firstUserPrompt || s.firstUserCommand || s.sessionId,
|
|
382
|
+
start: s.startedAt,
|
|
383
|
+
end: endOf(s),
|
|
384
|
+
source: s.source,
|
|
385
|
+
activityPeriods: s.activityPeriods
|
|
386
|
+
}, s.cost);
|
|
387
|
+
}
|
|
388
|
+
for (const subs of clusters) {
|
|
389
|
+
const periods = subs.map((c) => ({ start: c.startedAt, end: endOf(c) }));
|
|
390
|
+
addItem(subs[0], {
|
|
391
|
+
id: `${subs[0].sessionId}::subagents`,
|
|
392
|
+
start: periods[0].start,
|
|
393
|
+
end: Math.max(...periods.map((p) => p.end)),
|
|
394
|
+
source: subs[0].source,
|
|
395
|
+
activityPeriods: periods,
|
|
396
|
+
subs,
|
|
397
|
+
subCount: subs.filter((c) => !isJournal(c)).length
|
|
398
|
+
// journals are workflow logs, not subagents
|
|
399
|
+
}, subs.reduce((sum, c) => sum + (c.cost || 0), 0));
|
|
400
|
+
}
|
|
401
|
+
const arr = [...byKey.entries()].map(([key, { projectShort, items, cost }]) => {
|
|
402
|
+
const { placed, laneCount } = packLanes(items);
|
|
403
|
+
return { key, projectShort, placed, laneCount, cost };
|
|
404
|
+
});
|
|
405
|
+
arr.sort((a, b) => b.cost - a.cost);
|
|
406
|
+
let y = HEADER_HEIGHT;
|
|
407
|
+
for (const g of arr) {
|
|
408
|
+
g.yOffset = y;
|
|
409
|
+
g.height = g.laneCount * (bar.height + bar.row_gap);
|
|
410
|
+
y += g.height + bar.group_gap;
|
|
411
|
+
}
|
|
412
|
+
return { groups: arr, totalHeight: Math.max(HEADER_HEIGHT + bar.height + 12, y + 4) };
|
|
413
|
+
}, [sessions, granularity]);
|
|
414
|
+
const chartWidth = Math.max(100, width - PROJECTS_WIDTH);
|
|
415
|
+
const span = view.end - view.start;
|
|
416
|
+
const xFor = (ts) => PROJECTS_WIDTH + (ts - view.start) / span * chartWidth;
|
|
417
|
+
const ticks = reactExports.useMemo(
|
|
418
|
+
() => computeTicks({ viewStart: view.start, viewEnd: view.end, width: chartWidth, span }),
|
|
419
|
+
[view.start, view.end, chartWidth, span]
|
|
420
|
+
);
|
|
421
|
+
useMouse(containerRef, {
|
|
422
|
+
onZoom: (e) => {
|
|
423
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
424
|
+
const xInCanvas = e.clientX - rect.left;
|
|
425
|
+
const ratio = Math.min(1, Math.max(0, (xInCanvas - PROJECTS_WIDTH) / chartWidth));
|
|
426
|
+
const newSpan = Math.max(6e4, Math.min(dayRange.end - dayRange.start, span * Math.exp(e.deltaY * 15e-4)));
|
|
427
|
+
const center = view.start + ratio * span;
|
|
428
|
+
const start = Math.max(dayRange.start, center - ratio * newSpan);
|
|
429
|
+
const end = Math.min(dayRange.end, center + (1 - ratio) * newSpan);
|
|
430
|
+
setView((v) => ({ ...v, start, end }));
|
|
431
|
+
},
|
|
432
|
+
onReset: () => setView((v) => ({ ...v, start: dayRange.start, end: dayRange.end })),
|
|
433
|
+
onPan: (dx) => {
|
|
434
|
+
const dt = dx / chartWidth * span;
|
|
435
|
+
setView((v) => {
|
|
436
|
+
const newStart = Math.max(dayRange.start, v.start + dt);
|
|
437
|
+
const newEnd = Math.min(dayRange.end, v.end + dt);
|
|
438
|
+
return newEnd - newStart < span ? v : { ...v, start: newStart, end: newEnd };
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-wrap", children: [
|
|
443
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
444
|
+
HeatStrip,
|
|
445
|
+
{
|
|
446
|
+
sessions,
|
|
447
|
+
viewStart: view.start,
|
|
448
|
+
viewEnd: view.end,
|
|
449
|
+
width,
|
|
450
|
+
gutter: PROJECTS_WIDTH
|
|
451
|
+
}
|
|
452
|
+
),
|
|
453
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "gantt-canvas", ref: containerRef, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width, height: totalHeight, children: [
|
|
454
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("pattern", { id: "gantt-subagent-hatch", patternUnits: "userSpaceOnUse", width: 5, height: 5, patternTransform: "rotate(45)", children: /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: 0, y1: 0, x2: 0, y2: 5, stroke: "#0b0e14", strokeWidth: 3.5 }) }) }),
|
|
455
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: 0, y: 0, width: PROJECTS_WIDTH, height: totalHeight, className: "gantt-gutter" }),
|
|
456
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: PROJECTS_WIDTH, x2: PROJECTS_WIDTH, y1: 0, y2: totalHeight, className: "gantt-divider" }),
|
|
457
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(${PROJECTS_WIDTH},0)`, children: [
|
|
458
|
+
ticks.map((tk, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
459
|
+
"line",
|
|
460
|
+
{
|
|
461
|
+
x1: tk.x,
|
|
462
|
+
x2: tk.x,
|
|
463
|
+
y1: HEADER_HEIGHT,
|
|
464
|
+
y2: totalHeight,
|
|
465
|
+
className: `tick-vline ${tk.major ? "tick-major" : "tick-minor"}`
|
|
466
|
+
},
|
|
467
|
+
i
|
|
468
|
+
)),
|
|
469
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(TimeAxis, { viewStart: view.start, viewEnd: view.end, width: chartWidth, span, headerHeight: HEADER_HEIGHT })
|
|
470
|
+
] }),
|
|
471
|
+
groups.map((g, gi) => /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { style: projectFilter && projectFilter !== g.key ? { opacity: 0.25 } : void 0, children: [
|
|
472
|
+
gi > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
473
|
+
"line",
|
|
474
|
+
{
|
|
475
|
+
x1: 0,
|
|
476
|
+
x2: width,
|
|
477
|
+
y1: g.yOffset - bar.group_gap / 2,
|
|
478
|
+
y2: g.yOffset - bar.group_gap / 2,
|
|
479
|
+
className: "gantt-group-sep"
|
|
480
|
+
}
|
|
481
|
+
),
|
|
482
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
483
|
+
"text",
|
|
484
|
+
{
|
|
485
|
+
x: 8,
|
|
486
|
+
y: g.yOffset + 14,
|
|
487
|
+
className: `gantt-project gantt-project-clickable${projectFilter === g.key ? " gantt-project-active" : ""}`,
|
|
488
|
+
onClick: (e) => {
|
|
489
|
+
e.stopPropagation();
|
|
490
|
+
onToggleProjectFilter?.(g.key);
|
|
491
|
+
},
|
|
492
|
+
children: [
|
|
493
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: g.key }),
|
|
494
|
+
g.projectShort
|
|
495
|
+
]
|
|
496
|
+
}
|
|
497
|
+
),
|
|
498
|
+
g.placed.map(({ item, lane }) => {
|
|
499
|
+
const y = g.yOffset + lane * (bar.height + bar.row_gap);
|
|
500
|
+
const color = SOURCE_COLORS[item.source] || SOURCE_COLORS.other;
|
|
501
|
+
const isSelected = item.subs ? item.subs.some((c) => c.sessionId === selectedId) : item.id === selectedId;
|
|
502
|
+
const periods = item.activityPeriods?.length ? item.activityPeriods : [{ start: item.start, end: item.end }];
|
|
503
|
+
const rawX1 = xFor(item.start);
|
|
504
|
+
const rawX2 = xFor(item.end);
|
|
505
|
+
if (rawX2 < PROJECTS_WIDTH || rawX1 > width) return null;
|
|
506
|
+
const x = Math.max(PROJECTS_WIDTH, rawX1);
|
|
507
|
+
const w = Math.max(bar.min_width, Math.min(width, rawX2) - x);
|
|
508
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
509
|
+
"g",
|
|
510
|
+
{
|
|
511
|
+
className: `bar ${isSelected ? "selected" : ""}`,
|
|
512
|
+
onClick: () => onSelect(item.subs ? item.subs.at(-1).sessionId : item.id),
|
|
513
|
+
children: [
|
|
514
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x, y, width: w, height: bar.height, rx: bar.radius, fill: color, className: "bar-fill" }),
|
|
515
|
+
periods.slice(0, -1).map((p, i) => {
|
|
516
|
+
const gx1 = Math.max(PROJECTS_WIDTH, xFor(p.end));
|
|
517
|
+
const gx2 = Math.min(width, xFor(periods[i + 1].start));
|
|
518
|
+
if (gx2 <= gx1) return null;
|
|
519
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: gx1, y, width: gx2 - gx1, height: bar.height, className: "gantt-idle" }, i);
|
|
520
|
+
}),
|
|
521
|
+
item.subs && /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x, y, width: w, height: bar.height, rx: bar.radius, fill: "url(#gantt-subagent-hatch)", opacity: 0.5, pointerEvents: "none" }),
|
|
522
|
+
item.subs && /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: x + w / 2, y: y + bar.height / 2, textAnchor: "middle", dominantBaseline: "central", className: "gantt-bar-count", children: item.subCount }),
|
|
523
|
+
isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x, y, width: w, height: bar.height, rx: bar.radius, className: "bar-outline" }),
|
|
524
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: `${item.subs ? `${item.subCount} subagents` : `${SOURCE_LABELS[item.source] || item.source} · ${item.label}`}
|
|
525
|
+
${g.key}
|
|
526
|
+
${new Date(item.start).toLocaleString()} → ${new Date(item.end).toLocaleString()}` })
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
item.id
|
|
530
|
+
);
|
|
531
|
+
})
|
|
532
|
+
] }, g.key)),
|
|
533
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
534
|
+
WorkTimeOverlay,
|
|
535
|
+
{
|
|
536
|
+
showWorkBand: granularity === "day",
|
|
537
|
+
dayStart: dayRange.start,
|
|
538
|
+
viewStart: view.start,
|
|
539
|
+
viewEnd: view.end,
|
|
540
|
+
chartLeft: PROJECTS_WIDTH,
|
|
541
|
+
chartWidth,
|
|
542
|
+
totalHeight,
|
|
543
|
+
headerHeight: HEADER_HEIGHT,
|
|
544
|
+
containerRef
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
] }) })
|
|
548
|
+
] });
|
|
549
|
+
}
|
|
550
|
+
const WEEK = { weekStartsOn: 1 };
|
|
551
|
+
function startOfPeriod(ts, granularity) {
|
|
552
|
+
if (granularity === "week") return +startOfWeek(ts, WEEK);
|
|
553
|
+
if (granularity === "month") return +startOfMonth(ts);
|
|
554
|
+
return +startOfDay(ts);
|
|
555
|
+
}
|
|
556
|
+
function endOfPeriod(ts, granularity) {
|
|
557
|
+
if (granularity === "week") return +endOfWeek(ts, WEEK);
|
|
558
|
+
if (granularity === "month") return +endOfMonth(ts);
|
|
559
|
+
return +endOfDay(ts);
|
|
560
|
+
}
|
|
561
|
+
function addPeriod(ts, granularity, delta) {
|
|
562
|
+
if (granularity === "week") return +addWeeks(ts, delta);
|
|
563
|
+
if (granularity === "month") return +addMonths(ts, delta);
|
|
564
|
+
return +addDays(ts, delta);
|
|
565
|
+
}
|
|
566
|
+
function isSamePeriod(a, b, granularity) {
|
|
567
|
+
if (granularity === "week") return isSameWeek(a, b, WEEK);
|
|
568
|
+
if (granularity === "month") return isSameMonth(a, b);
|
|
569
|
+
return isSameDay(a, b);
|
|
570
|
+
}
|
|
571
|
+
const GRANULARITIES = [
|
|
572
|
+
{ key: "day", label: "Daily" },
|
|
573
|
+
{ key: "week", label: "Weekly" },
|
|
574
|
+
{ key: "month", label: "Monthly" }
|
|
575
|
+
];
|
|
576
|
+
const RESET_LABELS = { day: "Today", week: "This week", month: "This month" };
|
|
577
|
+
function periodTitle(anchor, granularity) {
|
|
578
|
+
if (granularity === "week") return `${format(anchor, "MMM d")} – ${format(endOfPeriod(anchor, "week"), "MMM d")}`;
|
|
579
|
+
if (granularity === "month") return format(anchor, "MMMM yyyy");
|
|
580
|
+
return format(anchor, "EEE, MMM d");
|
|
581
|
+
}
|
|
582
|
+
function Toolbar({
|
|
583
|
+
granularity,
|
|
584
|
+
onSetGranularity,
|
|
585
|
+
dayAnchor,
|
|
586
|
+
onShiftDay,
|
|
587
|
+
onResetToday,
|
|
588
|
+
sourceFilter,
|
|
589
|
+
availableSources,
|
|
590
|
+
onToggleSourceFilter
|
|
591
|
+
}) {
|
|
592
|
+
const onToday = isSamePeriod(dayAnchor, Date.now(), granularity);
|
|
593
|
+
const [menuOpen, setMenuOpen] = reactExports.useState(false);
|
|
594
|
+
const menuRef = reactExports.useRef(null);
|
|
595
|
+
reactExports.useEffect(() => {
|
|
596
|
+
if (!menuOpen) return;
|
|
597
|
+
const onDocClick = (e) => {
|
|
598
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
599
|
+
};
|
|
600
|
+
const onKeyDown = (e) => {
|
|
601
|
+
if (e.key === "Escape") setMenuOpen(false);
|
|
602
|
+
};
|
|
603
|
+
document.addEventListener("mousedown", onDocClick);
|
|
604
|
+
document.addEventListener("keydown", onKeyDown);
|
|
605
|
+
return () => {
|
|
606
|
+
document.removeEventListener("mousedown", onDocClick);
|
|
607
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
608
|
+
};
|
|
609
|
+
}, [menuOpen]);
|
|
610
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-toolbar", children: [
|
|
611
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "gantt-legend", children: (availableSources ?? []).slice().sort((a, b) => {
|
|
612
|
+
const ia = SOURCE_ORDER.indexOf(a), ib = SOURCE_ORDER.indexOf(b);
|
|
613
|
+
return (ia === -1 ? SOURCE_ORDER.length : ia) - (ib === -1 ? SOURCE_ORDER.length : ib);
|
|
614
|
+
}).map((k) => {
|
|
615
|
+
const active2 = sourceFilter === k;
|
|
616
|
+
const dim = sourceFilter && !active2;
|
|
617
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: `legend-chip ${active2 ? "active" : ""} ${dim ? "dim" : ""}`, onClick: () => onToggleSourceFilter?.(k), children: [
|
|
618
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "swatch", style: { background: SOURCE_COLORS[k] || SOURCE_COLORS.other } }),
|
|
619
|
+
SOURCE_LABELS[k] || k
|
|
620
|
+
] }, k);
|
|
621
|
+
}) }),
|
|
622
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-toolbar-nav", children: [
|
|
623
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "gantt-nav-arrow", onClick: () => onShiftDay?.(-1), title: "Previous period", "aria-label": "Previous period", children: "‹" }),
|
|
624
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-period", ref: menuRef, children: [
|
|
625
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
626
|
+
"button",
|
|
627
|
+
{
|
|
628
|
+
type: "button",
|
|
629
|
+
className: "gantt-toolbar-title",
|
|
630
|
+
onClick: () => setMenuOpen((o) => !o),
|
|
631
|
+
"aria-haspopup": "listbox",
|
|
632
|
+
"aria-expanded": menuOpen,
|
|
633
|
+
title: "Change period granularity",
|
|
634
|
+
children: [
|
|
635
|
+
periodTitle(dayAnchor, granularity),
|
|
636
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "gantt-granularity-caret", children: "▾" })
|
|
637
|
+
]
|
|
638
|
+
}
|
|
639
|
+
),
|
|
640
|
+
menuOpen && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "gantt-granularity-menu", role: "listbox", children: GRANULARITIES.map(({ key, label }) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
641
|
+
"button",
|
|
642
|
+
{
|
|
643
|
+
type: "button",
|
|
644
|
+
role: "option",
|
|
645
|
+
"aria-selected": granularity === key,
|
|
646
|
+
className: `gantt-granularity-option${granularity === key ? " active" : ""}`,
|
|
647
|
+
onClick: () => {
|
|
648
|
+
onSetGranularity?.(key);
|
|
649
|
+
setMenuOpen(false);
|
|
650
|
+
},
|
|
651
|
+
children: label
|
|
652
|
+
},
|
|
653
|
+
key
|
|
654
|
+
)) })
|
|
655
|
+
] }),
|
|
656
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
657
|
+
"button",
|
|
658
|
+
{
|
|
659
|
+
className: "gantt-nav-arrow",
|
|
660
|
+
onClick: () => onShiftDay?.(1),
|
|
661
|
+
style: { visibility: onToday ? "hidden" : "visible" },
|
|
662
|
+
title: "Next period",
|
|
663
|
+
"aria-label": "Next period",
|
|
664
|
+
children: "›"
|
|
665
|
+
}
|
|
666
|
+
),
|
|
667
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
668
|
+
"button",
|
|
669
|
+
{
|
|
670
|
+
className: "gantt-toolbar-today",
|
|
671
|
+
onClick: onResetToday,
|
|
672
|
+
style: { visibility: onToday ? "hidden" : "visible" },
|
|
673
|
+
children: RESET_LABELS[granularity]
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
] }),
|
|
677
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", {})
|
|
678
|
+
] });
|
|
679
|
+
}
|
|
680
|
+
const HOUR = 60 * 60 * 1e3;
|
|
681
|
+
const THRESHOLDS = {
|
|
682
|
+
context: { warn: 1e5, danger: 2e5 },
|
|
683
|
+
messages: { warn: 250, danger: 500 },
|
|
684
|
+
workTime: { warn: 0.5 * HOUR, danger: 2 * HOUR },
|
|
685
|
+
cost: { warn: 3, danger: 8 },
|
|
686
|
+
tokens: { warn: 3e6, danger: 8e6 }
|
|
687
|
+
};
|
|
688
|
+
function fmtDuration(ms) {
|
|
689
|
+
if (!ms || ms < 0) return "—";
|
|
690
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
691
|
+
const { hours = 0, minutes = 0, seconds = 0 } = intervalToDuration({ start: 0, end: ms });
|
|
692
|
+
if (!hours) return `${minutes}m ${seconds}s`;
|
|
693
|
+
return `${hours}h ${minutes}m`;
|
|
694
|
+
}
|
|
695
|
+
function fmtAgo(date) {
|
|
696
|
+
const min = Math.floor((Date.now() - new Date(date)) / 6e4);
|
|
697
|
+
if (min < 1) return "now";
|
|
698
|
+
if (min < 60) return `${min}m ago`;
|
|
699
|
+
if (min < 24 * 60) return `${Math.floor(min / 60)}h ago`;
|
|
700
|
+
return `${Math.floor(min / (24 * 60))}d ago`;
|
|
701
|
+
}
|
|
702
|
+
function fmtBytes(n) {
|
|
703
|
+
if (n == null) return "—";
|
|
704
|
+
if (n < 1024) return `${n} B`;
|
|
705
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
706
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
707
|
+
}
|
|
708
|
+
function fmtNum(n) {
|
|
709
|
+
if (n == null) return "—";
|
|
710
|
+
return n.toLocaleString();
|
|
711
|
+
}
|
|
712
|
+
function fmtUSD(n) {
|
|
713
|
+
if (n == null) return "—";
|
|
714
|
+
return `$${n.toFixed(2)}`;
|
|
715
|
+
}
|
|
716
|
+
function tone(value, { warn, danger }) {
|
|
717
|
+
if (value >= danger) return "danger";
|
|
718
|
+
if (value >= warn) return "warn";
|
|
719
|
+
return "";
|
|
720
|
+
}
|
|
721
|
+
function fmtCompact(n) {
|
|
722
|
+
if (n == null) return "—";
|
|
723
|
+
if (n < 0) return `-${fmtCompact(-n)}`;
|
|
724
|
+
if (n < 1e3) return `${n}`;
|
|
725
|
+
if (n < 1e6) return `${+(n / 1e3).toFixed(1)}k`;
|
|
726
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
727
|
+
}
|
|
728
|
+
const BAR_MAX_PX = 52;
|
|
729
|
+
function CostBreakdownChart({ sessions, dayAnchor, granularity }) {
|
|
730
|
+
const { bs, max } = reactExports.useMemo(() => {
|
|
731
|
+
const start = startOfPeriod(dayAnchor, granularity);
|
|
732
|
+
let bs2;
|
|
733
|
+
if (granularity === "month") {
|
|
734
|
+
const monthStart = startOfMonth(start);
|
|
735
|
+
bs2 = Array.from({ length: getDaysInMonth(monthStart) }, (_, i) => ({ start: +addDays(monthStart, i), label: format(addDays(monthStart, i), "d"), tip: format(addDays(monthStart, i), "MMM d"), showLabel: i % 5 === 0 }));
|
|
736
|
+
} else if (granularity === "week") {
|
|
737
|
+
bs2 = Array.from({ length: 7 }, (_, i) => ({ start: +addDays(start, i), label: format(addDays(start, i), "EEEEE"), tip: format(addDays(start, i), "MMM d"), showLabel: true }));
|
|
738
|
+
} else {
|
|
739
|
+
bs2 = Array.from({ length: 24 }, (_, i) => ({ start: +addHours(start, i), label: String(i).padStart(2, "0"), tip: `${i}h`, showLabel: i % 6 === 0 }));
|
|
740
|
+
}
|
|
741
|
+
for (const b of bs2) b.cost = 0;
|
|
742
|
+
for (const s of sessions) {
|
|
743
|
+
for (const sample of s.costSamples ?? []) {
|
|
744
|
+
let i = bs2.length - 1;
|
|
745
|
+
while (i > 0 && bs2[i].start > sample.start) i--;
|
|
746
|
+
bs2[i].cost += sample.cost || 0;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return { bs: bs2, max: Math.max(0, ...bs2.map((b) => b.cost)) };
|
|
750
|
+
}, [sessions, dayAnchor, granularity]);
|
|
751
|
+
if (max <= 0) return null;
|
|
752
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "cost-chart", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "cost-chart-cols", children: bs.map((b, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "cost-chart-col", "data-tippy-content": `${b.tip} · ${fmtUSD(b.cost)}`, "data-tippy-placement": "bottom", children: [
|
|
753
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "cost-chart-cell", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "cost-chart-bar", style: { height: `${b.cost > 0 ? Math.max(1, Math.round(b.cost / max * BAR_MAX_PX)) : 0}px` } }) }),
|
|
754
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "cost-chart-label", children: b.showLabel ? b.label : "" })
|
|
755
|
+
] }, i)) }) });
|
|
756
|
+
}
|
|
757
|
+
function PeriodSummary({ sessions, dayAnchor, granularity = "day" }) {
|
|
758
|
+
const totals = reactExports.useMemo(() => {
|
|
759
|
+
const t = sessions.reduce((acc, s) => {
|
|
760
|
+
acc.cost += s.cost || 0;
|
|
761
|
+
acc.savingsUsing5mCache += s.savings?.using5mCache || 0;
|
|
762
|
+
acc.totalTokens += s.totalTokens || 0;
|
|
763
|
+
acc.input += s.tokens.input;
|
|
764
|
+
acc.output += s.tokens.output;
|
|
765
|
+
acc.cacheRead += s.tokens.cacheRead;
|
|
766
|
+
acc.cacheCreation += s.tokens.cacheCreation;
|
|
767
|
+
acc.cacheCreation5m += s.tokens.cacheCreation5m || 0;
|
|
768
|
+
acc.cacheCreation1h += s.tokens.cacheCreation1h || 0;
|
|
769
|
+
acc.activeMs += s.activeMs || 0;
|
|
770
|
+
return acc;
|
|
771
|
+
}, { cost: 0, savingsUsing5mCache: 0, totalTokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0, cacheCreation5m: 0, cacheCreation1h: 0, activeMs: 0 });
|
|
772
|
+
const cacheDenom = t.cacheRead + t.cacheCreation;
|
|
773
|
+
t.cacheHitRatio = cacheDenom > 0 ? t.cacheRead / cacheDenom : null;
|
|
774
|
+
return t;
|
|
775
|
+
}, [sessions]);
|
|
776
|
+
const byProject = reactExports.useMemo(() => {
|
|
777
|
+
const map = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const s of sessions) {
|
|
779
|
+
const key = s.project || "(no project)";
|
|
780
|
+
let g = map.get(key);
|
|
781
|
+
if (!g) {
|
|
782
|
+
g = { key, projectShort: s.projectShort, cost: 0, totalTokens: 0 };
|
|
783
|
+
map.set(key, g);
|
|
784
|
+
}
|
|
785
|
+
g.cost += s.cost || 0;
|
|
786
|
+
g.totalTokens += s.totalTokens || 0;
|
|
787
|
+
}
|
|
788
|
+
return [...map.values()];
|
|
789
|
+
}, [sessions]);
|
|
790
|
+
const [projectStat, setProjectStat] = reactExports.useState("cost");
|
|
791
|
+
const sortedByProject = reactExports.useMemo(
|
|
792
|
+
() => [...byProject].sort((a, b) => projectStat === "cost" ? b.cost - a.cost : b.totalTokens - a.totalTokens),
|
|
793
|
+
[byProject, projectStat]
|
|
794
|
+
);
|
|
795
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("aside", { className: "gantt-side", children: [
|
|
796
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-section", children: [
|
|
797
|
+
totals.cost > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(CostBreakdownChart, { sessions, dayAnchor, granularity }),
|
|
798
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
799
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Estimated cost" }),
|
|
800
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtUSD(totals.cost) })
|
|
801
|
+
] }),
|
|
802
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
803
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Total tokens" }),
|
|
804
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtCompact(totals.totalTokens) })
|
|
805
|
+
] }),
|
|
806
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
807
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Working time" }),
|
|
808
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtDuration(totals.activeMs) })
|
|
809
|
+
] })
|
|
810
|
+
] }),
|
|
811
|
+
totals.savingsUsing5mCache > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-section", children: [
|
|
812
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "gantt-side-heading", children: "Potential savings" }),
|
|
813
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
814
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
|
|
815
|
+
totals.cost > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("strong", { children: [
|
|
816
|
+
"-",
|
|
817
|
+
(totals.savingsUsing5mCache / totals.cost * 100).toFixed(0),
|
|
818
|
+
"% "
|
|
819
|
+
] }),
|
|
820
|
+
"with 5m cache"
|
|
821
|
+
] }),
|
|
822
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("b", { children: [
|
|
823
|
+
"−",
|
|
824
|
+
fmtUSD(totals.savingsUsing5mCache)
|
|
825
|
+
] })
|
|
826
|
+
] })
|
|
827
|
+
] }),
|
|
828
|
+
sortedByProject.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-section", children: [
|
|
829
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-heading", children: [
|
|
830
|
+
"By project",
|
|
831
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
832
|
+
"button",
|
|
833
|
+
{
|
|
834
|
+
className: "stats-toggle",
|
|
835
|
+
onClick: () => setProjectStat((v) => v === "cost" ? "tokens" : "cost"),
|
|
836
|
+
title: `Showing ${projectStat} — click to toggle`,
|
|
837
|
+
children: projectStat === "cost" ? "T" : "$"
|
|
838
|
+
}
|
|
839
|
+
)
|
|
840
|
+
] }),
|
|
841
|
+
sortedByProject.map((g) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
842
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: g.projectShort }),
|
|
843
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: projectStat === "cost" ? fmtUSD(g.cost) : fmtCompact(g.totalTokens) })
|
|
844
|
+
] }, g.key))
|
|
845
|
+
] }),
|
|
846
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-section", children: [
|
|
847
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "gantt-side-heading", children: "Tokens breakdown" }),
|
|
848
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
849
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Input" }),
|
|
850
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtCompact(totals.input) })
|
|
851
|
+
] }),
|
|
852
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
853
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Output" }),
|
|
854
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtCompact(totals.output) })
|
|
855
|
+
] }),
|
|
856
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
857
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Cache write (5m)" }),
|
|
858
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtCompact(totals.cacheCreation5m) })
|
|
859
|
+
] }),
|
|
860
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
861
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Cache write (1h)" }),
|
|
862
|
+
totals.cacheCreation1h > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("b", { className: "warn-badge", children: fmtCompact(totals.cacheCreation1h) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtCompact(totals.cacheCreation1h) })
|
|
863
|
+
] }),
|
|
864
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
865
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Cache read" }),
|
|
866
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: fmtCompact(totals.cacheRead) })
|
|
867
|
+
] }),
|
|
868
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-side-row", children: [
|
|
869
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Cache hit ratio" }),
|
|
870
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: totals.cacheHitRatio != null ? `${Math.round(totals.cacheHitRatio * 100)}%` : "—" })
|
|
871
|
+
] })
|
|
872
|
+
] })
|
|
873
|
+
] });
|
|
874
|
+
}
|
|
875
|
+
function flatten(items, instructions = []) {
|
|
876
|
+
const turns = [];
|
|
877
|
+
const results = {};
|
|
878
|
+
for (const it of items) {
|
|
879
|
+
if (it.type === "attachment" && it.attachment) {
|
|
880
|
+
const block = { type: "attachment", attachment: it.attachment };
|
|
881
|
+
const last = turns[turns.length - 1];
|
|
882
|
+
if (last && last.isMeta && last.blocks.every((b) => b.type === "attachment")) {
|
|
883
|
+
last.blocks.push(block);
|
|
884
|
+
if (it._tokenDelta != null) last.tokenDelta = (last.tokenDelta ?? 0) + it._tokenDelta;
|
|
885
|
+
if (it._tokenTotal != null) last.tokenTotal = it._tokenTotal;
|
|
886
|
+
} else {
|
|
887
|
+
turns.push({
|
|
888
|
+
uuid: it.uuid,
|
|
889
|
+
role: last?.role === "assistant" || last?.role === "tool" ? "assistant" : "user",
|
|
890
|
+
isMeta: true,
|
|
891
|
+
ts: it.timestamp ? Date.parse(it.timestamp) : null,
|
|
892
|
+
model: null,
|
|
893
|
+
usage: null,
|
|
894
|
+
tokenDelta: it._tokenDelta ?? null,
|
|
895
|
+
tokenTotal: it._tokenTotal ?? null,
|
|
896
|
+
blocks: [block]
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (it.type === "result" && it.key && it.agentId) {
|
|
902
|
+
const text = typeof it.result === "string" ? it.result : "```json\n" + JSON.stringify(it.result, null, 2) + "\n```";
|
|
903
|
+
turns.push({
|
|
904
|
+
uuid: `wf-${it.agentId}`,
|
|
905
|
+
role: "assistant",
|
|
906
|
+
isMeta: false,
|
|
907
|
+
ts: null,
|
|
908
|
+
model: null,
|
|
909
|
+
usage: null,
|
|
910
|
+
tokenDelta: null,
|
|
911
|
+
tokenTotal: null,
|
|
912
|
+
blocks: [{ type: "text", text: `**agent ${it.agentId}**
|
|
913
|
+
|
|
914
|
+
${text}` }]
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (it.type !== "user" && it.type !== "assistant") continue;
|
|
919
|
+
const msg = it.message || {};
|
|
920
|
+
const blocks = normalizeContent(msg.content).filter((b) => !(b.type === "thinking" && !b.thinking?.trim()));
|
|
921
|
+
if (blocks.length === 0) continue;
|
|
922
|
+
for (const b of blocks) {
|
|
923
|
+
if (b.type === "tool_result" && b.tool_use_id) results[b.tool_use_id] = b;
|
|
924
|
+
}
|
|
925
|
+
const role = it.type === "user" && blocks.every((b) => b.type === "tool_result") ? "tool" : it.type;
|
|
926
|
+
turns.push({
|
|
927
|
+
uuid: it.uuid,
|
|
928
|
+
role,
|
|
929
|
+
isMeta: !!it.isMeta,
|
|
930
|
+
ts: it.timestamp ? Date.parse(it.timestamp) : null,
|
|
931
|
+
model: msg.model || null,
|
|
932
|
+
msgId: msg.id || null,
|
|
933
|
+
usage: msg.usage || null,
|
|
934
|
+
tokenDelta: it._tokenDelta ?? null,
|
|
935
|
+
tokenTotal: it._tokenTotal ?? null,
|
|
936
|
+
blocks
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
for (const t of turns) {
|
|
940
|
+
t.blocks = t.blocks.map((b) => b.type === "tool_use" ? { ...b, result: results[b.id] } : b).filter((b) => !(b.type === "tool_result" && results[b.tool_use_id]));
|
|
941
|
+
}
|
|
942
|
+
const out = turns.filter((t) => t.blocks.length > 0);
|
|
943
|
+
for (const it of instructions) {
|
|
944
|
+
const turn = {
|
|
945
|
+
uuid: `instr-${it.hash ?? it.file_path}-${it.timestamp}`,
|
|
946
|
+
// system prompts share a file_path, their hash disambiguates
|
|
947
|
+
role: "instruction",
|
|
948
|
+
isMeta: true,
|
|
949
|
+
ts: it.timestamp ? Date.parse(it.timestamp) - 500 : null,
|
|
950
|
+
model: null,
|
|
951
|
+
usage: null,
|
|
952
|
+
tokenDelta: null,
|
|
953
|
+
tokenTotal: null,
|
|
954
|
+
blocks: [{ type: "instruction", it }]
|
|
955
|
+
};
|
|
956
|
+
const i = out.findIndex((t) => t.ts != null && t.ts > turn.ts);
|
|
957
|
+
i === -1 ? out.push(turn) : out.splice(i, 0, turn);
|
|
958
|
+
}
|
|
959
|
+
return out;
|
|
960
|
+
}
|
|
961
|
+
const currentModel = (turns) => turns.findLast((t) => t.model && t.model !== "<synthetic>")?.model;
|
|
962
|
+
function normalizeContent(content) {
|
|
963
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
964
|
+
if (!Array.isArray(content)) return [];
|
|
965
|
+
return content.map((b) => typeof b === "string" ? { type: "text", text: b } : b);
|
|
966
|
+
}
|
|
967
|
+
const clip = (s, n = 100) => {
|
|
968
|
+
s = String(s).replace(/\s+/g, " ").trim();
|
|
969
|
+
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
970
|
+
};
|
|
971
|
+
const basename = (p) => p ? String(p).split(/[\\/]/).pop() : "";
|
|
972
|
+
const TOOL_ARG_SUMMARY = {
|
|
973
|
+
Bash: (i) => i.command,
|
|
974
|
+
Read: (i) => basename(i.file_path),
|
|
975
|
+
Edit: (i) => basename(i.file_path),
|
|
976
|
+
MultiEdit: (i) => basename(i.file_path),
|
|
977
|
+
Write: (i) => basename(i.file_path),
|
|
978
|
+
NotebookEdit: (i) => basename(i.notebook_path),
|
|
979
|
+
Grep: (i) => [i.pattern, basename(i.path) || i.glob].filter(Boolean).join(" in "),
|
|
980
|
+
Glob: (i) => [i.pattern, basename(i.path)].filter(Boolean).join(" in "),
|
|
981
|
+
LS: (i) => basename(i.path),
|
|
982
|
+
WebFetch: (i) => i.url,
|
|
983
|
+
WebSearch: (i) => i.query,
|
|
984
|
+
Skill: (i) => [i.skill, i.args].filter(Boolean).join(" "),
|
|
985
|
+
SlashCommand: (i) => i.command,
|
|
986
|
+
TodoWrite: (i) => i.todos?.find((t) => t.status === "in_progress")?.content || `${i.todos?.length ?? 0} todos`,
|
|
987
|
+
AskUserQuestion: (i) => i.questions?.map((q) => q.question).join(" · "),
|
|
988
|
+
KillShell: (i) => i.shell_id,
|
|
989
|
+
BashOutput: (i) => i.bash_id
|
|
990
|
+
};
|
|
991
|
+
function toolSummary(name, input) {
|
|
992
|
+
const i = input || {};
|
|
993
|
+
const summary = i.description || TOOL_ARG_SUMMARY[name]?.(i);
|
|
994
|
+
return summary ? `${name}: ${clip(summary)}` : name;
|
|
995
|
+
}
|
|
996
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
997
|
+
function parseCommand(text) {
|
|
998
|
+
if (typeof text !== "string") return null;
|
|
999
|
+
if (!/^\s*<(command-name|command-message|local-command-stdout|local-command-caveat)>/.test(text)) return null;
|
|
1000
|
+
const field = (tag) => stripAnsi(text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1] ?? "");
|
|
1001
|
+
return {
|
|
1002
|
+
name: field("command-name"),
|
|
1003
|
+
message: field("command-message"),
|
|
1004
|
+
args: field("command-args"),
|
|
1005
|
+
stdout: field("local-command-stdout"),
|
|
1006
|
+
caveat: field("local-command-caveat")
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function instructionTitle(it, model) {
|
|
1010
|
+
const parts = [it.memory_type, it.model && it.model !== model && it.model].filter(Boolean).join(", ");
|
|
1011
|
+
return parts ? `${it.name ?? it.file_path} (${parts})` : it.name ?? it.file_path;
|
|
1012
|
+
}
|
|
1013
|
+
const isContextTurn = (t) => t.isMeta && t.blocks.every((b) => b.type === "attachment");
|
|
1014
|
+
function groupTurns(turns) {
|
|
1015
|
+
const groups = [];
|
|
1016
|
+
let cycle = null;
|
|
1017
|
+
let pending = [];
|
|
1018
|
+
const flush = () => {
|
|
1019
|
+
if (cycle) {
|
|
1020
|
+
groups.push(cycle);
|
|
1021
|
+
cycle = null;
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
for (const t of turns) {
|
|
1025
|
+
if (isContextTurn(t) && !cycle) {
|
|
1026
|
+
if (groups.at(-1)?.kind === "user") groups.at(-1).turns.push(t);
|
|
1027
|
+
else pending.push(t);
|
|
1028
|
+
} else if (t.role === "instruction") {
|
|
1029
|
+
flush();
|
|
1030
|
+
groups.push({ kind: "instruction", turn: t });
|
|
1031
|
+
} else if (t.role === "user") {
|
|
1032
|
+
flush();
|
|
1033
|
+
groups.push({ kind: "user", turns: [...pending, t] });
|
|
1034
|
+
pending = [];
|
|
1035
|
+
} else {
|
|
1036
|
+
cycle ??= { kind: "assistant", turns: pending.splice(0) };
|
|
1037
|
+
cycle.turns.push(t);
|
|
1038
|
+
if (t.blocks.some((b) => b.type === "text")) flush();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
flush();
|
|
1042
|
+
if (pending.length) groups.push({ kind: "user", turns: pending });
|
|
1043
|
+
return groups;
|
|
1044
|
+
}
|
|
1045
|
+
function cycleDurations(groups) {
|
|
1046
|
+
let prevEnd = null;
|
|
1047
|
+
return groups.map((g) => {
|
|
1048
|
+
const tss = (g.turns ?? [g.turn]).map((t) => t.ts).filter((ts) => ts != null);
|
|
1049
|
+
const end = tss.length ? tss[tss.length - 1] : null;
|
|
1050
|
+
const duration2 = g.kind === "assistant" && end != null && prevEnd != null ? end - prevEnd : null;
|
|
1051
|
+
if (end != null) prevEnd = end;
|
|
1052
|
+
return duration2;
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
function tokenPoints(groups) {
|
|
1056
|
+
let prev = 0;
|
|
1057
|
+
return groups.map((g) => {
|
|
1058
|
+
const turns = g.turns ?? [g.turn];
|
|
1059
|
+
let total = null, ctx = null;
|
|
1060
|
+
const byMsg = /* @__PURE__ */ new Map();
|
|
1061
|
+
for (const t of turns) {
|
|
1062
|
+
if (t.tokenTotal != null) total = t.tokenTotal;
|
|
1063
|
+
const u = t.usage;
|
|
1064
|
+
if (u) {
|
|
1065
|
+
ctx = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
1066
|
+
byMsg.set(t.msgId ?? t.uuid, u);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
let usage = null;
|
|
1070
|
+
for (const u of byMsg.values()) {
|
|
1071
|
+
usage ??= { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, cacheCreation5m: 0, cacheCreation1h: 0 };
|
|
1072
|
+
usage.input += u.input_tokens || 0;
|
|
1073
|
+
usage.output += u.output_tokens || 0;
|
|
1074
|
+
usage.cacheRead += u.cache_read_input_tokens || 0;
|
|
1075
|
+
usage.cacheCreation += u.cache_creation_input_tokens || 0;
|
|
1076
|
+
usage.cacheCreation5m += u.cache_creation?.ephemeral_5m_input_tokens || 0;
|
|
1077
|
+
usage.cacheCreation1h += u.cache_creation?.ephemeral_1h_input_tokens || 0;
|
|
1078
|
+
}
|
|
1079
|
+
if (total == null || total === prev) return null;
|
|
1080
|
+
const delta = total - prev;
|
|
1081
|
+
prev = total;
|
|
1082
|
+
if (ctx == null && g.kind !== "assistant") ctx = delta;
|
|
1083
|
+
const ts = turns.find((t) => t.ts != null)?.ts ?? null;
|
|
1084
|
+
return { delta, total, ts, ctx, usage, role: g.kind === "assistant" ? "assistant" : turns[0].role };
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const TIPS = {
|
|
1088
|
+
cache1h: "Billed 2× — prefer the default 5m cache",
|
|
1089
|
+
context: `Consider keeping it below ${fmtCompact(THRESHOLDS.context.warn)} tokens`,
|
|
1090
|
+
messages: `Consider sessions below ${fmtCompact(THRESHOLDS.messages.warn)} messages`,
|
|
1091
|
+
time: `Consider sessions under ${THRESHOLDS.workTime.warn / 6e4}m`
|
|
1092
|
+
};
|
|
1093
|
+
function SessionList({ sessions, selectedId, onSelect }) {
|
|
1094
|
+
const selectedRef = reactExports.useRef(null);
|
|
1095
|
+
const [sortBy, setSortBy] = useLocalStorage("session-list.sort", "time");
|
|
1096
|
+
const [filter, setFilter] = reactExports.useState("");
|
|
1097
|
+
const [, tick] = reactExports.useState(0);
|
|
1098
|
+
reactExports.useEffect(() => {
|
|
1099
|
+
selectedRef.current?.scrollIntoView({ block: "nearest" });
|
|
1100
|
+
}, [selectedId]);
|
|
1101
|
+
reactExports.useEffect(() => {
|
|
1102
|
+
const t = setInterval(() => tick((n) => n + 1), 6e4);
|
|
1103
|
+
return () => clearInterval(t);
|
|
1104
|
+
}, []);
|
|
1105
|
+
const cmd = (text) => {
|
|
1106
|
+
const c = parseCommand(text);
|
|
1107
|
+
return c ? `${c.name || c.message} ${c.args}` : "";
|
|
1108
|
+
};
|
|
1109
|
+
const sessionLabel = (s) => s.aiTitle || s.summary || s.firstUserPrompt || cmd(s.firstUserCommand) || s.sessionId || "";
|
|
1110
|
+
const q = filter.trim().toLowerCase();
|
|
1111
|
+
const filtered = q ? sessions.filter((s) => [
|
|
1112
|
+
s.customTitle,
|
|
1113
|
+
s.agentName,
|
|
1114
|
+
s.aiTitle,
|
|
1115
|
+
s.summary,
|
|
1116
|
+
s.firstUserPrompt,
|
|
1117
|
+
s.firstUserCommand,
|
|
1118
|
+
s.project,
|
|
1119
|
+
s.worktree,
|
|
1120
|
+
s.sessionId,
|
|
1121
|
+
s.model,
|
|
1122
|
+
s.gitBranch
|
|
1123
|
+
].filter(Boolean).join(" ").toLowerCase().includes(q)) : sessions;
|
|
1124
|
+
const sorted = sortBy === "cost" ? [...filtered].sort((a, b) => (b.cost || 0) - (a.cost || 0)) : sortBy === "tokens" ? [...filtered].sort((a, b) => (b.totalTokens || 0) - (a.totalTokens || 0)) : sortBy === "name" ? [...filtered].sort((a, b) => sessionLabel(a).toLowerCase().localeCompare(sessionLabel(b).toLowerCase())) : filtered;
|
|
1125
|
+
const grouped = reactExports.useMemo(() => {
|
|
1126
|
+
const byParent = subagentsByParent(sorted);
|
|
1127
|
+
return sorted.flatMap((s) => byParent.has(s.parentSessionId) ? [] : [
|
|
1128
|
+
{ session: s, isChild: false },
|
|
1129
|
+
...(byParent.get(s.sessionId) ?? []).map((c) => ({ session: c, isChild: true }))
|
|
1130
|
+
]);
|
|
1131
|
+
}, [sorted]);
|
|
1132
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-list", children: [
|
|
1133
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-list-header", children: [
|
|
1134
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "filter-wrap", children: [
|
|
1135
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
1136
|
+
"input",
|
|
1137
|
+
{
|
|
1138
|
+
className: "filter",
|
|
1139
|
+
placeholder: "Filter sessions…",
|
|
1140
|
+
value: filter,
|
|
1141
|
+
onChange: (e) => setFilter(e.target.value)
|
|
1142
|
+
}
|
|
1143
|
+
),
|
|
1144
|
+
filter && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "filter-clear", "aria-label": "Clear filter", onClick: () => setFilter(""), children: "×" })
|
|
1145
|
+
] }),
|
|
1146
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-list-header-row", children: [
|
|
1147
|
+
q && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "filter-count", children: [
|
|
1148
|
+
"Filtered ",
|
|
1149
|
+
filtered.length,
|
|
1150
|
+
"/",
|
|
1151
|
+
sessions.length
|
|
1152
|
+
] }),
|
|
1153
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "sort-toggle", role: "group", "aria-label": "Sort sessions", children: [
|
|
1154
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "sort-toggle-label", children: "Sort by:" }),
|
|
1155
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: `sort-pill ${sortBy === "time" ? "active" : ""}`, onClick: () => setSortBy("time"), children: "Time" }),
|
|
1156
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: `sort-pill ${sortBy === "name" ? "active" : ""}`, onClick: () => setSortBy("name"), children: "Name" }),
|
|
1157
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: `sort-pill ${sortBy === "cost" ? "active" : ""}`, onClick: () => setSortBy("cost"), children: "Cost" }),
|
|
1158
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: `sort-pill ${sortBy === "tokens" ? "active" : ""}`, onClick: () => setSortBy("tokens"), children: "Tokens" })
|
|
1159
|
+
] })
|
|
1160
|
+
] })
|
|
1161
|
+
] }),
|
|
1162
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-list-scroll", children: [
|
|
1163
|
+
sorted.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "empty", children: "No sessions on this day." }),
|
|
1164
|
+
grouped.map(({ session: s, isChild }) => {
|
|
1165
|
+
const isSubagent = s.sessionId.startsWith("agent-");
|
|
1166
|
+
const journal = isJournal(s);
|
|
1167
|
+
const isFork = !!s.forkedFrom;
|
|
1168
|
+
const sessionName = s.customTitle && s.agentName && s.customTitle !== s.agentName ? `${s.customTitle} [${s.agentName}]` : s.customTitle || s.agentName || "";
|
|
1169
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
1170
|
+
"div",
|
|
1171
|
+
{
|
|
1172
|
+
ref: selectedId === s.sessionId ? selectedRef : null,
|
|
1173
|
+
className: `session-row ${selectedId === s.sessionId ? "selected" : ""} ${isChild ? "is-subagent-child" : ""}`,
|
|
1174
|
+
onClick: () => onSelect(selectedId === s.sessionId ? null : s.sessionId),
|
|
1175
|
+
style: { borderLeftColor: SOURCE_COLORS[s.source] || SOURCE_COLORS.other },
|
|
1176
|
+
children: [
|
|
1177
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-row-main", children: [
|
|
1178
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-label", children: [
|
|
1179
|
+
s.tokens?.cacheCreation1h > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "warn-badge", title: `Used 1h extended cache (${fmtNum(s.tokens.cacheCreation1h)} tokens)
|
|
1180
|
+
${TIPS.cache1h}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Hourglass, { size: 11 }) }),
|
|
1181
|
+
s.lastContextTokens > THRESHOLDS.context.danger && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "danger-badge", title: `Context size: ${fmtCompact(s.lastContextTokens)} tokens
|
|
1182
|
+
${TIPS.context}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Gauge, { size: 11 }) }),
|
|
1183
|
+
s.lastContextTokens > THRESHOLDS.context.warn && s.lastContextTokens <= THRESHOLDS.context.danger && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "warn-badge", title: `Context size: ${fmtCompact(s.lastContextTokens)} tokens
|
|
1184
|
+
${TIPS.context}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Gauge, { size: 11 }) }),
|
|
1185
|
+
s.messageCount > THRESHOLDS.messages.danger && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "danger-badge", title: `${fmtNum(s.messageCount)} messages
|
|
1186
|
+
${TIPS.messages}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(MessageSquare, { size: 11 }) }),
|
|
1187
|
+
s.messageCount > THRESHOLDS.messages.warn && s.messageCount <= THRESHOLDS.messages.danger && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "warn-badge", title: `${fmtNum(s.messageCount)} messages
|
|
1188
|
+
${TIPS.messages}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(MessageSquare, { size: 11 }) }),
|
|
1189
|
+
s.activeMs > THRESHOLDS.workTime.danger && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "danger-badge", title: `Working time: ${fmtDuration(s.activeMs)}
|
|
1190
|
+
${TIPS.time}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 11 }) }),
|
|
1191
|
+
s.activeMs > THRESHOLDS.workTime.warn && s.activeMs <= THRESHOLDS.workTime.danger && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "warn-badge", title: `Working time: ${fmtDuration(s.activeMs)}
|
|
1192
|
+
${TIPS.time}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 11 }) }),
|
|
1193
|
+
s.speed === "fast" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `fast-badge ${s.fastPricingUnknown ? "fast-badge-unknown" : ""}`, title: s.fastPricingUnknown ? "Used fast mode — cost is INACCURATE: no fast-mode price multiplier known for this model" : "Used fast mode (speed: fast)", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { size: 11 }) }),
|
|
1194
|
+
s.priceUnknown && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "danger-badge", title: `No price entry for ${s.models?.join(", ") || "this model"} — cost is missing or understated`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(DollarSign, { size: 11 }) }),
|
|
1195
|
+
isSubagent && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "subagent-tag", children: "[subagent]" }),
|
|
1196
|
+
journal && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "subagent-tag", children: [
|
|
1197
|
+
"[journal: ",
|
|
1198
|
+
s.workflowAgents,
|
|
1199
|
+
" subagent",
|
|
1200
|
+
s.workflowAgents === 1 ? "" : "s",
|
|
1201
|
+
"]"
|
|
1202
|
+
] }),
|
|
1203
|
+
s.worktree && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "worktree-tag", title: `Worktree of ${s.projectShort}`, children: [
|
|
1204
|
+
"[",
|
|
1205
|
+
s.worktree,
|
|
1206
|
+
"]"
|
|
1207
|
+
] }),
|
|
1208
|
+
isFork && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "fork-tag", title: `Forked from session ${s.forkedFrom.sessionId}`, children: "↳" }),
|
|
1209
|
+
sessionName && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "session-name", children: sessionName }),
|
|
1210
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "session-label-text", children: sessionLabel(s) })
|
|
1211
|
+
] }),
|
|
1212
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-time-bottom", children: [
|
|
1213
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { title: format(s.lastActivityAt, "MMM d, HH:mm:ss"), children: fmtAgo(s.lastActivityAt) }),
|
|
1214
|
+
s.models?.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "session-model", children: [
|
|
1215
|
+
", ",
|
|
1216
|
+
s.models.map((m) => m.replace(/^claude-/, "")).join(", ")
|
|
1217
|
+
] })
|
|
1218
|
+
] })
|
|
1219
|
+
] }),
|
|
1220
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-row-stats", children: [
|
|
1221
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `${sortBy === "tokens" ? "stat-primary" : "stat-secondary"} ${tone(s.totalTokens, THRESHOLDS.tokens)}`, children: fmtCompact(s.totalTokens) }),
|
|
1222
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `${sortBy === "cost" ? "stat-primary" : "stat-secondary"} ${tone(s.cost, THRESHOLDS.cost)}`, children: s.cost ? fmtUSD(s.cost) : "—" })
|
|
1223
|
+
] })
|
|
1224
|
+
]
|
|
1225
|
+
},
|
|
1226
|
+
s.sessionId
|
|
1227
|
+
);
|
|
1228
|
+
})
|
|
1229
|
+
] })
|
|
1230
|
+
] });
|
|
1231
|
+
}
|
|
1232
|
+
function LazyMount({ children, eager = false, forceMount = false, placeholderMinHeight = 120, rootRef = null, style, ...rest }) {
|
|
1233
|
+
const ref = reactExports.useRef(null);
|
|
1234
|
+
const [mounted, setMounted] = reactExports.useState(eager);
|
|
1235
|
+
const render = mounted || forceMount;
|
|
1236
|
+
reactExports.useEffect(() => {
|
|
1237
|
+
if (mounted) return;
|
|
1238
|
+
const io = new IntersectionObserver(([e]) => {
|
|
1239
|
+
if (e.isIntersecting) {
|
|
1240
|
+
setMounted(true);
|
|
1241
|
+
io.disconnect();
|
|
1242
|
+
}
|
|
1243
|
+
}, { root: rootRef?.current ?? null, rootMargin: "600px 0px" });
|
|
1244
|
+
io.observe(ref.current);
|
|
1245
|
+
return () => io.disconnect();
|
|
1246
|
+
}, [mounted]);
|
|
1247
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref, style: render ? style : { ...style, minHeight: placeholderMinHeight }, ...rest, children: render && children });
|
|
1248
|
+
}
|
|
1249
|
+
const detectLang = (body, limit = 500) => {
|
|
1250
|
+
if (body[0] === "[") return "json";
|
|
1251
|
+
const snippet = body.length > limit ? body.slice(0, limit) : body;
|
|
1252
|
+
if (/"[^"]*"\s*:/.test(snippet)) return "json";
|
|
1253
|
+
if (/'[^']*'\s*:/.test(snippet)) return "python";
|
|
1254
|
+
return null;
|
|
1255
|
+
};
|
|
1256
|
+
const findMatchingClose = (text, start) => {
|
|
1257
|
+
const open = text[start];
|
|
1258
|
+
const close = open === "{" ? "}" : "]";
|
|
1259
|
+
let depth = 0;
|
|
1260
|
+
for (let i = start; i < text.length; i++) {
|
|
1261
|
+
const c = text[i];
|
|
1262
|
+
if (c === open) depth++;
|
|
1263
|
+
else if (c === close && --depth === 0) return i + 1;
|
|
1264
|
+
}
|
|
1265
|
+
return -1;
|
|
1266
|
+
};
|
|
1267
|
+
const insideFence = (text, i) => (text.slice(0, i).match(/^```/gm)?.length ?? 0) % 2;
|
|
1268
|
+
function fenceBlocks(text) {
|
|
1269
|
+
if (typeof text !== "string") return text;
|
|
1270
|
+
if (!text.includes("{") && !text.includes("[")) return text;
|
|
1271
|
+
const startRe = /^([^\n{[]*)[{[]/gm;
|
|
1272
|
+
let out = "", cursor = 0, m;
|
|
1273
|
+
while ((m = startRe.exec(text)) !== null) {
|
|
1274
|
+
const bodyStart = m.index + m[1].length;
|
|
1275
|
+
if (insideFence(text, bodyStart)) continue;
|
|
1276
|
+
const end = findMatchingClose(text, bodyStart);
|
|
1277
|
+
if (end < 0) continue;
|
|
1278
|
+
if (end !== text.length && text[end] !== "\n" && text[end] !== "\r") continue;
|
|
1279
|
+
const body = text.slice(bodyStart, end);
|
|
1280
|
+
const lang = detectLang(body);
|
|
1281
|
+
if (!lang) continue;
|
|
1282
|
+
out += text.slice(cursor, m.index) + m[1].trimEnd() + "\n```" + lang + "\n" + body + "\n```\n";
|
|
1283
|
+
cursor = startRe.lastIndex = end;
|
|
1284
|
+
}
|
|
1285
|
+
return out + text.slice(cursor);
|
|
1286
|
+
}
|
|
1287
|
+
function fenceTags(text) {
|
|
1288
|
+
if (typeof text !== "string" || !text.includes("<")) return text;
|
|
1289
|
+
const startRe = /^<([A-Za-z][\w-]*)(?:\s[^<>`\n]*)?>/gm;
|
|
1290
|
+
let out = "", cursor = 0, m;
|
|
1291
|
+
while ((m = startRe.exec(text)) !== null) {
|
|
1292
|
+
if (insideFence(text, m.index)) continue;
|
|
1293
|
+
const close = text.indexOf(`</${m[1]}>`, m.index);
|
|
1294
|
+
if (close < 0) continue;
|
|
1295
|
+
const end = close + m[1].length + 3;
|
|
1296
|
+
if (end !== text.length && text[end] !== "\n" && text[end] !== "\r") continue;
|
|
1297
|
+
out += text.slice(cursor, m.index) + "```xml\n" + text.slice(m.index, end) + "\n```";
|
|
1298
|
+
cursor = startRe.lastIndex = end;
|
|
1299
|
+
}
|
|
1300
|
+
out += text.slice(cursor);
|
|
1301
|
+
return out.split(/(^```[^\n]*\n[\s\S]*?\n```|`[^`\n]+`)/m).map((part, i) => i % 2 ? part : part.replace(/<\/?[A-Za-z][\w-]*(?:\s[^<>`\n]*)?\/?>/g, "`$&`")).join("");
|
|
1302
|
+
}
|
|
1303
|
+
function splitMarkdown(text, size = 1e4) {
|
|
1304
|
+
if (typeof text !== "string" || text.length <= size) return [text];
|
|
1305
|
+
const chunks = [];
|
|
1306
|
+
let buf = [], len = 0, inFence = false;
|
|
1307
|
+
for (const line of text.split("\n")) {
|
|
1308
|
+
if (line.startsWith("```")) inFence = !inFence;
|
|
1309
|
+
if (len > size && !inFence && line.trim() === "") {
|
|
1310
|
+
chunks.push(buf.join("\n"));
|
|
1311
|
+
buf = [];
|
|
1312
|
+
len = 0;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
buf.push(line);
|
|
1316
|
+
len += line.length + 1;
|
|
1317
|
+
}
|
|
1318
|
+
if (buf.length) chunks.push(buf.join("\n"));
|
|
1319
|
+
return chunks;
|
|
1320
|
+
}
|
|
1321
|
+
let active = false;
|
|
1322
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1323
|
+
const setActive = (v) => {
|
|
1324
|
+
if (v === active) return;
|
|
1325
|
+
active = v;
|
|
1326
|
+
listeners.forEach((l) => l());
|
|
1327
|
+
};
|
|
1328
|
+
window.api.onFindActive(setActive);
|
|
1329
|
+
const subscribe = (cb) => {
|
|
1330
|
+
listeners.add(cb);
|
|
1331
|
+
return () => listeners.delete(cb);
|
|
1332
|
+
};
|
|
1333
|
+
function useFindActive() {
|
|
1334
|
+
return reactExports.useSyncExternalStore(subscribe, () => active);
|
|
1335
|
+
}
|
|
1336
|
+
function closeFind() {
|
|
1337
|
+
if (!active) return;
|
|
1338
|
+
setActive(false);
|
|
1339
|
+
window.api.findClose();
|
|
1340
|
+
}
|
|
1341
|
+
const isExternal = (href) => /^(https?:|mailto:)/i.test(href || "");
|
|
1342
|
+
function buildLinkComponents(basePath) {
|
|
1343
|
+
return {
|
|
1344
|
+
a: ({ href, children, ...props }) => {
|
|
1345
|
+
const clickable = href && (isExternal(href) || basePath);
|
|
1346
|
+
if (!clickable) return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children });
|
|
1347
|
+
const onClick = (e) => {
|
|
1348
|
+
e.preventDefault();
|
|
1349
|
+
e.stopPropagation();
|
|
1350
|
+
void window.api.openLink(href, basePath);
|
|
1351
|
+
};
|
|
1352
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href, onClick, ...props, children });
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
const Chunk = React.memo(({ text, components }) => /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown$1, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeHighlight], components, children: text }));
|
|
1357
|
+
function Markdown({ text, basePath = null, autoFence = false, className = "", ...rest }) {
|
|
1358
|
+
const ref = reactExports.useRef(null);
|
|
1359
|
+
const findOpen = useFindActive();
|
|
1360
|
+
const components = reactExports.useMemo(() => buildLinkComponents(basePath), [basePath]);
|
|
1361
|
+
const chunks = reactExports.useMemo(() => splitMarkdown(autoFence ? fenceBlocks(fenceTags(text)) : text), [text, autoFence]);
|
|
1362
|
+
const cls = `markdown${className ? " " + className : ""}`;
|
|
1363
|
+
if (chunks.length === 1) return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: cls, ...rest, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Chunk, { text: chunks[0], components }) });
|
|
1364
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: cls, ref, ...rest, children: chunks.map((c, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(LazyMount, { eager: i === 0, forceMount: findOpen, rootRef: ref, placeholderMinHeight: 500, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Chunk, { text: c, components }) }, i)) });
|
|
1365
|
+
}
|
|
1366
|
+
const claudeIcon = "" + new URL("claude-icon-BqihzN4C.svg", import.meta.url).href;
|
|
1367
|
+
const ExpandAllContext = React.createContext(null);
|
|
1368
|
+
function useCollapsed(defaultOpen) {
|
|
1369
|
+
const [open, setOpen] = reactExports.useState(defaultOpen);
|
|
1370
|
+
const expandAll = reactExports.useContext(ExpandAllContext);
|
|
1371
|
+
reactExports.useEffect(() => {
|
|
1372
|
+
if (expandAll != null) setOpen(expandAll);
|
|
1373
|
+
}, [expandAll]);
|
|
1374
|
+
return [open, setOpen];
|
|
1375
|
+
}
|
|
1376
|
+
const isCommandTurn = (t) => t.blocks.some((b) => b.type === "text" && parseCommand(b.text)?.name);
|
|
1377
|
+
function ConversationView({ items, instructions = [], expandAll = null }) {
|
|
1378
|
+
const turns = reactExports.useMemo(() => flatten(items, instructions), [items, instructions]);
|
|
1379
|
+
const groups = reactExports.useMemo(() => groupTurns(turns), [turns]);
|
|
1380
|
+
const points = reactExports.useMemo(() => tokenPoints(groups), [groups]);
|
|
1381
|
+
const durations = reactExports.useMemo(() => cycleDurations(groups), [groups]);
|
|
1382
|
+
const findOpen = useFindActive();
|
|
1383
|
+
const [scale, zoomRef] = useMouseFontScale("font-scale.conversation");
|
|
1384
|
+
const hasTimeline = points.some(Boolean);
|
|
1385
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(ExpandAllContext.Provider, { value: expandAll, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: zoomRef, className: `conversation${hasTimeline ? " has-token-timeline" : ""}`, style: { "--font-scale": scale }, children: groups.map((g, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `conv-row conv-row-${g.kind}`, children: [
|
|
1386
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(LazyMount, { eager: i < 8, forceMount: findOpen, placeholderMinHeight: 80, children: g.kind === "user" ? /* @__PURE__ */ jsxRuntimeExports.jsx(UserRow, { turns: g.turns, point: points[i] }) : g.kind === "assistant" ? /* @__PURE__ */ jsxRuntimeExports.jsx(AssistantCard, { turns: g.turns, point: points[i], duration: durations[i], showAuthor: groups[i - 1]?.kind !== "assistant" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(InstructionFile, { it: g.turn.blocks[0].it, model: currentModel(turns) }) }),
|
|
1387
|
+
points[i] ? /* @__PURE__ */ jsxRuntimeExports.jsx(TokenPoint, { point: points[i] }) : hasTimeline && g.kind === "user" && g.turns.some(isCommandTurn) && /* @__PURE__ */ jsxRuntimeExports.jsx(TokenPoint, { point: { role: "command", delta: 0 } })
|
|
1388
|
+
] }, g.kind === "instruction" ? g.turn.uuid : g.turns[0].uuid)) }) });
|
|
1389
|
+
}
|
|
1390
|
+
const ClaudeIcon = () => /* @__PURE__ */ jsxRuntimeExports.jsx("img", { className: "msg-icon claude-icon", src: claudeIcon, alt: "" });
|
|
1391
|
+
const CTX_LIMIT = THRESHOLDS.context.danger;
|
|
1392
|
+
function TokenPoint({ point: p }) {
|
|
1393
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `token-point token-point-${p.role}${p.delta < 0 ? " negative" : ""}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "token-point-dot" }) });
|
|
1394
|
+
}
|
|
1395
|
+
function TokenStats({ point }) {
|
|
1396
|
+
if (!point) return null;
|
|
1397
|
+
const rows = [];
|
|
1398
|
+
if (point.ctx != null) {
|
|
1399
|
+
const width = Math.min(point.ctx / CTX_LIMIT, 1) * 100;
|
|
1400
|
+
rows.push(`<div class="token-tooltip-ctx">Context <span class="token-point-ctx-bar"><span style="width:${width}%"></span></span> <span class="token-tooltip-ctx-val">${fmtCompact(point.ctx)}</span></div>`);
|
|
1401
|
+
}
|
|
1402
|
+
const u = point.usage;
|
|
1403
|
+
if (u) {
|
|
1404
|
+
const cells = [
|
|
1405
|
+
["Input", fmtCompact(u.input)],
|
|
1406
|
+
["Output", fmtCompact(u.output)],
|
|
1407
|
+
["Cache write (5m)", fmtCompact(u.cacheCreation5m)],
|
|
1408
|
+
["Cache write (1h)", fmtCompact(u.cacheCreation1h)],
|
|
1409
|
+
["Cache read", fmtCompact(u.cacheRead)]
|
|
1410
|
+
];
|
|
1411
|
+
const sum = `<span></span><span class="token-tooltip-sum">+${fmtCompact(u.input + u.output + u.cacheRead + u.cacheCreation)}</span>`;
|
|
1412
|
+
rows.push(`<div class="token-tooltip-grid">${cells.map(([l, v]) => `<span>${l}</span><span>${v}</span>`).join("")}${sum}</div>`);
|
|
1413
|
+
}
|
|
1414
|
+
rows.push(`<div class="token-tooltip-total"><span>Accumulated tokens</span><span>${fmtCompact(point.total)}</span></div>`);
|
|
1415
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-tokens", "data-tippy-html": `<div class="token-tooltip">${rows.join("")}</div>`, children: fmtCompact(point.total) });
|
|
1416
|
+
}
|
|
1417
|
+
function UserRow({ turns, point }) {
|
|
1418
|
+
const [open, setOpen] = useCollapsed(false);
|
|
1419
|
+
const msg = turns.find((t) => !isContextTurn(t));
|
|
1420
|
+
const ctxItems = turns.filter(isContextTurn).flatMap((t) => t.blocks);
|
|
1421
|
+
if (!msg || msg.isMeta && !isCommandTurn(msg)) return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: turns.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx(TurnRow, { turn: t }, t.uuid)) });
|
|
1422
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "msg-user", children: [
|
|
1423
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "msg-header", children: [
|
|
1424
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Terminal, { className: "msg-icon" }),
|
|
1425
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-author", children: "You" }),
|
|
1426
|
+
ctxItems.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "msg-summary", onClick: () => setOpen((v) => !v), children: [
|
|
1427
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "aux-chevron", children: open ? "▾" : "▸" }),
|
|
1428
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
|
|
1429
|
+
"attachments · ",
|
|
1430
|
+
ctxItems.length,
|
|
1431
|
+
" item",
|
|
1432
|
+
ctxItems.length === 1 ? "" : "s"
|
|
1433
|
+
] })
|
|
1434
|
+
] }),
|
|
1435
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "msg-header-right", children: [
|
|
1436
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(TokenStats, { point }),
|
|
1437
|
+
msg.ts != null && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-time", children: format(msg.ts, "HH:mm:ss") })
|
|
1438
|
+
] })
|
|
1439
|
+
] }),
|
|
1440
|
+
open && ctxItems.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "msg-context", children: ctxItems.map((b, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(Attachment, { att: b.attachment }, i)) }),
|
|
1441
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(TurnRow, { turn: msg })
|
|
1442
|
+
] });
|
|
1443
|
+
}
|
|
1444
|
+
function AssistantCard({ turns, point, duration: duration2, showAuthor = true }) {
|
|
1445
|
+
const [open, setOpen] = useCollapsed(false);
|
|
1446
|
+
const toolBlocks = turns.flatMap((t) => t.blocks.filter((b) => b.type === "tool_use"));
|
|
1447
|
+
const errorCount = toolBlocks.filter((b) => b.result?.is_error).length;
|
|
1448
|
+
const isAux = (t) => !t.blocks.some((b) => b.type === "text");
|
|
1449
|
+
const foldable = toolBlocks.length > 0 && turns.some(isAux);
|
|
1450
|
+
const end = turns.findLast((t) => t.ts != null)?.ts ?? null;
|
|
1451
|
+
const [everOpen, setEverOpen] = reactExports.useState(false);
|
|
1452
|
+
reactExports.useEffect(() => {
|
|
1453
|
+
if (open) setEverOpen(true);
|
|
1454
|
+
}, [open]);
|
|
1455
|
+
const hidden = (t) => !open && foldable && isAux(t);
|
|
1456
|
+
const anyVisible = turns.some((t) => !hidden(t));
|
|
1457
|
+
const parts = [
|
|
1458
|
+
toolBlocks.length > 0 && `${toolBlocks.length} tool call${toolBlocks.length === 1 ? "" : "s"}`,
|
|
1459
|
+
errorCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "msg-errors", children: [
|
|
1460
|
+
errorCount,
|
|
1461
|
+
" error",
|
|
1462
|
+
errorCount === 1 ? "" : "s"
|
|
1463
|
+
] }, "err")
|
|
1464
|
+
].filter(Boolean);
|
|
1465
|
+
const summary = parts.map((p, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
|
|
1466
|
+
i > 0 && ", ",
|
|
1467
|
+
p
|
|
1468
|
+
] }, i));
|
|
1469
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "assistant-card", children: [
|
|
1470
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "msg-header", children: [
|
|
1471
|
+
showAuthor && /* @__PURE__ */ jsxRuntimeExports.jsx(ClaudeIcon, {}),
|
|
1472
|
+
showAuthor && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-author", children: "Claude" }),
|
|
1473
|
+
parts.length > 0 && (foldable ? /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "msg-summary", onClick: () => setOpen((v) => !v), children: [
|
|
1474
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "aux-chevron", children: open ? "▾" : "▸" }),
|
|
1475
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: summary })
|
|
1476
|
+
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-summary", children: summary })),
|
|
1477
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "msg-header-right", children: [
|
|
1478
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(TokenStats, { point }),
|
|
1479
|
+
end != null && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-time", title: duration2 > 0 ? `Response time: ${fmtDuration(duration2)}` : null, children: format(end, "HH:mm:ss") })
|
|
1480
|
+
] })
|
|
1481
|
+
] }),
|
|
1482
|
+
(anyVisible || everOpen) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `assistant-card-body${anyVisible ? "" : " turn-hidden"}`, children: turns.map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx(TurnRow, { turn: t, hidden: hidden(t) }, t.uuid)) })
|
|
1483
|
+
] });
|
|
1484
|
+
}
|
|
1485
|
+
function TurnRow({ turn, hidden = false }) {
|
|
1486
|
+
const contextBlocks = [], otherBlocks = [];
|
|
1487
|
+
const groupAttachments = turn.role === "user";
|
|
1488
|
+
for (const b of turn.blocks) {
|
|
1489
|
+
if (b.type === "attachment" && groupAttachments) contextBlocks.push(b);
|
|
1490
|
+
else otherBlocks.push(b);
|
|
1491
|
+
}
|
|
1492
|
+
const hasError = turn.blocks.some((b) => b.type === "tool_use" && b.result?.is_error);
|
|
1493
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `turn turn-${turn.role} ${turn.isMeta ? "turn-meta-note" : ""} ${hasError ? "turn-error" : ""} ${hidden ? "turn-hidden" : ""}`, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "turn-blocks", children: [
|
|
1494
|
+
contextBlocks.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(Collapsible, { className: "attachment", defaultOpen: false, title: `attachments · ${contextBlocks.length} item${contextBlocks.length === 1 ? "" : "s"}`, children: contextBlocks.map((b, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(Attachment, { att: b.attachment }, i)) }),
|
|
1495
|
+
otherBlocks.map((b, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(Block, { block: b }, i))
|
|
1496
|
+
] }) });
|
|
1497
|
+
}
|
|
1498
|
+
function Block({ block }) {
|
|
1499
|
+
if (block.type === "text") {
|
|
1500
|
+
const cmd = parseCommand(block.text);
|
|
1501
|
+
if (cmd) {
|
|
1502
|
+
if (!cmd.name && !cmd.args && !cmd.stdout && !cmd.caveat) return null;
|
|
1503
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "block-command", children: [
|
|
1504
|
+
cmd.name && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "cmd-name", children: cmd.name }),
|
|
1505
|
+
cmd.args && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "cmd-args", children: [
|
|
1506
|
+
" ",
|
|
1507
|
+
cmd.args
|
|
1508
|
+
] }),
|
|
1509
|
+
cmd.stdout && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "cmd-stdout", children: cmd.stdout }),
|
|
1510
|
+
cmd.caveat && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "cmd-stdout", children: cmd.caveat })
|
|
1511
|
+
] });
|
|
1512
|
+
}
|
|
1513
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "block-text", text: block.text, autoFence: true });
|
|
1514
|
+
}
|
|
1515
|
+
if (block.type === "thinking") {
|
|
1516
|
+
if (!block.thinking) return null;
|
|
1517
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Collapsible, { title: "thinking", defaultOpen: false, children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: block.thinking }) });
|
|
1518
|
+
}
|
|
1519
|
+
if (block.type === "tool_use") {
|
|
1520
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Collapsible, { title: toolSummary(block.name, block.input), defaultOpen: false, children: [
|
|
1521
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(JsonBlock, { value: block.input }),
|
|
1522
|
+
block.result && /* @__PURE__ */ jsxRuntimeExports.jsx(Block, { block: block.result })
|
|
1523
|
+
] });
|
|
1524
|
+
}
|
|
1525
|
+
if (block.type === "tool_result") {
|
|
1526
|
+
const c = block.content;
|
|
1527
|
+
const parts = Array.isArray(c) ? c : [typeof c === "string" ? { type: "text", text: c } : c];
|
|
1528
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `tool-result ${block.is_error ? "error" : ""}`, children: [
|
|
1529
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tool-result-label", children: block.is_error ? "Error:" : "Result:" }),
|
|
1530
|
+
parts.map((p, i) => p.type === "text" ? /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: p.text }, i) : /* @__PURE__ */ jsxRuntimeExports.jsx(Block, { block: p }, i))
|
|
1531
|
+
] });
|
|
1532
|
+
}
|
|
1533
|
+
if (block.type === "attachment") {
|
|
1534
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Attachment, { att: block.attachment });
|
|
1535
|
+
}
|
|
1536
|
+
if (block.type === "image") {
|
|
1537
|
+
const src = block.source;
|
|
1538
|
+
if (src && src.type === "base64") {
|
|
1539
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("img", { className: "block-image", src: `data:${src.media_type};base64,${src.data}`, alt: "" });
|
|
1540
|
+
}
|
|
1541
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "block-aux", children: "[image]" });
|
|
1542
|
+
}
|
|
1543
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Collapsible, { title: block.type || "block", children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: safeJson(block) }) });
|
|
1544
|
+
}
|
|
1545
|
+
const ATTACHMENT_RENDERERS = {
|
|
1546
|
+
selected_lines_in_ide: (a) => {
|
|
1547
|
+
const path = a.displayPath || a.filename || "";
|
|
1548
|
+
const range = a.lineStart === a.lineEnd ? `L${a.lineStart}` : `L${a.lineStart}-${a.lineEnd}`;
|
|
1549
|
+
return { title: `Selection in ${a.ideName || "IDE"}: ${path}:${range}`, body: a.content || "" };
|
|
1550
|
+
},
|
|
1551
|
+
opened_file_in_ide: (a) => ({ title: `Opened in IDE: ${a.displayPath || a.filename || ""}`, body: null }),
|
|
1552
|
+
file: (a) => {
|
|
1553
|
+
const f = a.content?.file;
|
|
1554
|
+
return { title: `File: ${a.displayPath || f?.filePath || a.filename || ""}`, body: f?.content ?? safeJson(a.content) };
|
|
1555
|
+
},
|
|
1556
|
+
skill_listing: (a) => ({ title: `${a.skillCount} skills`, body: a.content || "", defaultOpen: false }),
|
|
1557
|
+
deferred_tools_delta: (a) => ({
|
|
1558
|
+
title: `${a.addedNames?.length || 0} deferred tools ${a.removedNames?.length ? `(removed: ${a.removedNames.length})` : ""}`,
|
|
1559
|
+
body: safeJson({ added: a.addedNames, removed: a.removedNames }),
|
|
1560
|
+
defaultOpen: false
|
|
1561
|
+
})
|
|
1562
|
+
};
|
|
1563
|
+
function Attachment({ att }) {
|
|
1564
|
+
const render = ATTACHMENT_RENDERERS[att?.type];
|
|
1565
|
+
const { title, body, defaultOpen = false } = render ? render(att) : { title: `Attachment: ${att?.type || "unknown"}`, body: safeJson(att) };
|
|
1566
|
+
if (body == null) return /* @__PURE__ */ jsxRuntimeExports.jsx(Label, { title, className: "attachment" });
|
|
1567
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Collapsible, { title, className: "attachment", defaultOpen, children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: body }) });
|
|
1568
|
+
}
|
|
1569
|
+
function Label({ title, className }) {
|
|
1570
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `aux ${className || ""}`, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "aux-label", children: [
|
|
1571
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "aux-chevron", children: "•" }),
|
|
1572
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: title })
|
|
1573
|
+
] }) });
|
|
1574
|
+
}
|
|
1575
|
+
function Collapsible({ title, children, className, defaultOpen = true }) {
|
|
1576
|
+
const [open, setOpen] = useCollapsed(defaultOpen);
|
|
1577
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `aux ${className || ""}`, children: [
|
|
1578
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "aux-toggle", onClick: () => setOpen((v) => !v), children: [
|
|
1579
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "aux-chevron", children: open ? "▾" : "▸" }),
|
|
1580
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: title })
|
|
1581
|
+
] }),
|
|
1582
|
+
open && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "aux-body", children })
|
|
1583
|
+
] });
|
|
1584
|
+
}
|
|
1585
|
+
function safeJson(x) {
|
|
1586
|
+
try {
|
|
1587
|
+
return JSON.stringify(x, null, 2);
|
|
1588
|
+
} catch {
|
|
1589
|
+
return String(x);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function JsonBlock({ value }) {
|
|
1593
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "block-text", text: "```json\n" + safeJson(value) + "\n```" });
|
|
1594
|
+
}
|
|
1595
|
+
function InstructionFile({ it, model }) {
|
|
1596
|
+
const [open, setOpen] = useCollapsed(false);
|
|
1597
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "aux instruction-file", children: [
|
|
1598
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "aux-toggle", onClick: () => setOpen((v) => !v), children: [
|
|
1599
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "aux-chevron", children: open ? "▾" : "▸" }),
|
|
1600
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: instructionTitle(it, model) })
|
|
1601
|
+
] }),
|
|
1602
|
+
open && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "aux-body", children: [
|
|
1603
|
+
it.name && it.name !== it.file_path && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "instruction-path", children: it.file_path }),
|
|
1604
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "block-text", text: it.content || "", basePath: it.file_path })
|
|
1605
|
+
] })
|
|
1606
|
+
] });
|
|
1607
|
+
}
|
|
1608
|
+
const SHORTEN_AT = 500;
|
|
1609
|
+
const moreLabel = (hidden) => /\s/.test(hidden) ? `${hidden.split(/\s+/).filter(Boolean).length.toLocaleString()} more words` : `${hidden.length.toLocaleString()} more chars`;
|
|
1610
|
+
function ShortText({ text, ...rest }) {
|
|
1611
|
+
const [expandedText, setExpandedText] = reactExports.useState(null);
|
|
1612
|
+
const expanded = expandedText === text;
|
|
1613
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { ...rest, style: { cursor: "pointer" }, onClick: () => setExpandedText(expanded ? null : text), children: [
|
|
1614
|
+
'"',
|
|
1615
|
+
expanded ? text : `${text.slice(0, SHORTEN_AT)}...`,
|
|
1616
|
+
'"',
|
|
1617
|
+
!expanded && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "shorten-more", children: [
|
|
1618
|
+
" (",
|
|
1619
|
+
moreLabel(text.slice(SHORTEN_AT)),
|
|
1620
|
+
")"
|
|
1621
|
+
] })
|
|
1622
|
+
] });
|
|
1623
|
+
}
|
|
1624
|
+
const renderShortened = (shorten) => ({ children, ...rest }, { type, value }) => shorten && type === "value" && typeof value === "string" && value.length > SHORTEN_AT ? /* @__PURE__ */ jsxRuntimeExports.jsx(ShortText, { ...rest, text: value }) : void 0;
|
|
1625
|
+
function highlight(text, q) {
|
|
1626
|
+
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1627
|
+
return String(text ?? "").split(new RegExp(`(${escaped})`, "i")).map((p, i) => i % 2 ? /* @__PURE__ */ jsxRuntimeExports.jsx("mark", { className: "jsonl-viewer-mark", children: p }, i) : p);
|
|
1628
|
+
}
|
|
1629
|
+
function prune(value, q) {
|
|
1630
|
+
if (!value || typeof value !== "object") {
|
|
1631
|
+
return String(value ?? "").toLowerCase().includes(q) ? value : void 0;
|
|
1632
|
+
}
|
|
1633
|
+
const out = Array.isArray(value) ? [] : {};
|
|
1634
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1635
|
+
const sub = k.toLowerCase().includes(q) ? v : prune(v, q);
|
|
1636
|
+
if (sub !== void 0) Array.isArray(out) ? out.push(sub) : out[k] = sub;
|
|
1637
|
+
}
|
|
1638
|
+
return Object.keys(out).length ? out : void 0;
|
|
1639
|
+
}
|
|
1640
|
+
function labelFor(entry) {
|
|
1641
|
+
if (!entry || typeof entry !== "object") return "entry";
|
|
1642
|
+
const type = entry.type || entry.message?.role;
|
|
1643
|
+
if (!type) return "entry";
|
|
1644
|
+
const sub = entry.message?.role !== type ? entry.message?.role : null;
|
|
1645
|
+
return sub ? `${type}:${sub}` : type;
|
|
1646
|
+
}
|
|
1647
|
+
function JsonlView({ items, expandAll = null }) {
|
|
1648
|
+
const [query, setQuery] = reactExports.useState("");
|
|
1649
|
+
const [scale, bodyRef] = useMouseFontScale("font-scale.jsonl");
|
|
1650
|
+
const findOpen = useFindActive();
|
|
1651
|
+
const scrollTo = (i) => {
|
|
1652
|
+
const el = bodyRef.current?.querySelector(`[data-entry="${i}"]`);
|
|
1653
|
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1654
|
+
};
|
|
1655
|
+
const q = query.trim().toLowerCase();
|
|
1656
|
+
const entries = (items ?? []).map((parsed) => {
|
|
1657
|
+
const value = q ? prune(parsed, q) : parsed;
|
|
1658
|
+
return value === void 0 ? void 0 : { value, label: labelFor(parsed) };
|
|
1659
|
+
}).filter((e) => e !== void 0);
|
|
1660
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "jsonl-viewer", children: [
|
|
1661
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "jsonl-viewer-toolbar", children: /* @__PURE__ */ jsxRuntimeExports.jsx("input", { className: "jsonl-viewer-search", type: "search", placeholder: "Filter…", onInput: (e) => setQuery(e.target.value) }) }),
|
|
1662
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "jsonl-viewer-content", children: [
|
|
1663
|
+
entries.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "jsonl-viewer-toc", children: entries.map(({ label }, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("li", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", onClick: () => scrollTo(i), title: label, children: [
|
|
1664
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "jsonl-viewer-toc-index", children: i + 1 }),
|
|
1665
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "jsonl-viewer-toc-label", children: label })
|
|
1666
|
+
] }) }, i)) }),
|
|
1667
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "jsonl-viewer-body", ref: bodyRef, style: { "--font-scale": scale }, children: [
|
|
1668
|
+
!items && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "jsonl-viewer-loading", children: "Loading…" }),
|
|
1669
|
+
entries.map(({ value }, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(LazyMount, { eager: i < 8, forceMount: findOpen, "data-entry": i, className: "jsonl-viewer-entry", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
1670
|
+
JsonView,
|
|
1671
|
+
{
|
|
1672
|
+
value,
|
|
1673
|
+
style: vscodeTheme,
|
|
1674
|
+
collapsed: false,
|
|
1675
|
+
displayDataTypes: false,
|
|
1676
|
+
displayObjectSize: false,
|
|
1677
|
+
shortenTextAfterLength: 0,
|
|
1678
|
+
highlightUpdates: false,
|
|
1679
|
+
enableClipboard: false,
|
|
1680
|
+
children: [
|
|
1681
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(JsonView.String, { render: ({ children, ...rest }, ctx) => q && ctx.type === "value" ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { ...rest, children: [
|
|
1682
|
+
'"',
|
|
1683
|
+
highlight(children, q),
|
|
1684
|
+
'"'
|
|
1685
|
+
] }) : renderShortened(!q && !expandAll)({ children, ...rest }, ctx) }),
|
|
1686
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(JsonView.KeyName, { render: ({ children, ...rest }) => q ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { ...rest, children: highlight(children, q) }) : void 0 })
|
|
1687
|
+
]
|
|
1688
|
+
}
|
|
1689
|
+
) }, `${q}-${i}`))
|
|
1690
|
+
] })
|
|
1691
|
+
] })
|
|
1692
|
+
] });
|
|
1693
|
+
}
|
|
1694
|
+
const time = (ts) => ts ? new Date(ts).toLocaleTimeString([], { hour12: false }) : "";
|
|
1695
|
+
const duration = (ms) => ms == null ? "" : ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
1696
|
+
const splitUrl = (url) => {
|
|
1697
|
+
const [method, ...path] = (url || "").split(" ");
|
|
1698
|
+
return [method, path.join(" ")];
|
|
1699
|
+
};
|
|
1700
|
+
const tokens = (n) => n == null ? "" : n < 1e3 ? `${n}` : `${(n / 1e3).toFixed(1)}k`;
|
|
1701
|
+
const totalTokens = (rec) => {
|
|
1702
|
+
const u = rec?.response?.usage;
|
|
1703
|
+
if (!u) return null;
|
|
1704
|
+
return (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
1705
|
+
};
|
|
1706
|
+
const collapsedHint = (value, keyName) => {
|
|
1707
|
+
if (value?.role) {
|
|
1708
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1709
|
+
for (const b of Array.isArray(value.content) ? value.content : [{ type: "text" }]) counts.set(b.type, (counts.get(b.type) || 0) + 1);
|
|
1710
|
+
return `${value.role}:`.padEnd(11) + [...counts].map(([t, n]) => n > 1 ? `${n} ${t}` : t).join(", ");
|
|
1711
|
+
}
|
|
1712
|
+
if (typeof keyName === "number") return value?.name ?? value?.type ?? null;
|
|
1713
|
+
if (Array.isArray(value)) return `${value.length} ${keyName === "tools" ? "tools" : keyName === "system" ? "blocks" : "items"}`;
|
|
1714
|
+
return null;
|
|
1715
|
+
};
|
|
1716
|
+
function Tree({ value, expandAll, collapsed = false, seen = null }) {
|
|
1717
|
+
const live = reactExports.useRef({});
|
|
1718
|
+
live.current = { collapsed, seen };
|
|
1719
|
+
const expandInitially = (_, { keys }) => {
|
|
1720
|
+
const { collapsed: collapsed2, seen: seen2 } = live.current;
|
|
1721
|
+
return collapsed2 ? keys.length > 0 : !seen2?.has(keys.join("."));
|
|
1722
|
+
};
|
|
1723
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
1724
|
+
JsonView,
|
|
1725
|
+
{
|
|
1726
|
+
value,
|
|
1727
|
+
style: vscodeTheme,
|
|
1728
|
+
displayDataTypes: false,
|
|
1729
|
+
displayObjectSize: false,
|
|
1730
|
+
shortenTextAfterLength: 0,
|
|
1731
|
+
highlightUpdates: false,
|
|
1732
|
+
enableClipboard: false,
|
|
1733
|
+
shouldExpandNodeInitially: expandInitially,
|
|
1734
|
+
children: [
|
|
1735
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(JsonView.String, { render: renderShortened(!expandAll) }),
|
|
1736
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(JsonView.Ellipsis, { render: ({ "data-expanded": isCollapsed, style, ...props }, { value: v, keyName }) => {
|
|
1737
|
+
const hint = isCollapsed ? collapsedHint(v, keyName) : null;
|
|
1738
|
+
return hint ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { ...props, className: "requests-hint", children: hint }) : void 0;
|
|
1739
|
+
} }),
|
|
1740
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(JsonView.KeyName, { render: (props, { keys }) => live.current.seen?.has(keys.join(".")) ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { ...props, className: "requests-seen-key" }) : void 0 })
|
|
1741
|
+
]
|
|
1742
|
+
}
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const Pane = React.memo(function Pane2({ value, headers, seen, expandAll }) {
|
|
1746
|
+
const [scale, paneRef] = useMouseFontScale("font-scale.requests");
|
|
1747
|
+
const findOpen = useFindActive();
|
|
1748
|
+
const messages = !Array.isArray(value) ? value?.messages : null;
|
|
1749
|
+
const paths = seen?.paths ?? [];
|
|
1750
|
+
const bodySeen = new Set(paths.filter((p) => !p.startsWith("messages")));
|
|
1751
|
+
const msgSeen = (i) => new Set(paths.filter((p) => p.startsWith(`messages.${i}.`)).map((p) => p.slice(`messages.${i}.`.length)));
|
|
1752
|
+
const { messages: _, ...bodyNoMsgs } = value || {};
|
|
1753
|
+
const [open, close] = Array.isArray(value) ? "[]" : "{}";
|
|
1754
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-pane", ref: paneRef, style: { "--font-scale": scale }, children: [
|
|
1755
|
+
headers && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-headers", children: [
|
|
1756
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-label", children: "Headers" }),
|
|
1757
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Tree, { value: headers, expandAll, collapsed: true })
|
|
1758
|
+
] }),
|
|
1759
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-label", children: [
|
|
1760
|
+
"Body",
|
|
1761
|
+
(paths.length > 0 || seen?.messages?.some(Boolean)) && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-label-hint", children: "Content repeated from earlier requests is faded and collapsed" })
|
|
1762
|
+
] }),
|
|
1763
|
+
value == null ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-pane-empty", children: "Not captured" }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-doc", style: vscodeTheme, children: [
|
|
1764
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bracket", children: open }),
|
|
1765
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Tree, { value: messages ? bodyNoMsgs : value, expandAll, seen: bodySeen }),
|
|
1766
|
+
messages && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-msgs", children: [
|
|
1767
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
|
|
1768
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "key", children: '"messages"' }),
|
|
1769
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "colon", children: ": " }),
|
|
1770
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bracket", children: "[" })
|
|
1771
|
+
] }),
|
|
1772
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "items", children: messages.map((m, i) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
1773
|
+
LazyMount,
|
|
1774
|
+
{
|
|
1775
|
+
eager: i < 10,
|
|
1776
|
+
forceMount: findOpen,
|
|
1777
|
+
rootRef: paneRef,
|
|
1778
|
+
placeholderMinHeight: 22,
|
|
1779
|
+
className: seen?.messages?.[i] ? "requests-seen" : void 0,
|
|
1780
|
+
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Tree, { value: m, expandAll, collapsed: !!seen?.messages?.[i], seen: msgSeen(i) })
|
|
1781
|
+
},
|
|
1782
|
+
i
|
|
1783
|
+
)) }),
|
|
1784
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bracket", children: "]" })
|
|
1785
|
+
] }),
|
|
1786
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bracket", children: close })
|
|
1787
|
+
] })
|
|
1788
|
+
] });
|
|
1789
|
+
});
|
|
1790
|
+
function RequestsView({ sessionId, date, granularity = "day", fileSize = 0, expandAll = null }) {
|
|
1791
|
+
const [records, setRecords] = reactExports.useState(null);
|
|
1792
|
+
const [selected, setSelected] = reactExports.useState(0);
|
|
1793
|
+
const [tab, setTab] = reactExports.useState("request");
|
|
1794
|
+
const rec = records?.[selected];
|
|
1795
|
+
const body = reactExports.useDeferredValue(tab === "request" ? rec?.request ?? rec : rec?.response);
|
|
1796
|
+
const headers = reactExports.useDeferredValue(tab === "request" ? rec?.requestHeaders : rec?.responseHeaders);
|
|
1797
|
+
const seen = reactExports.useDeferredValue(tab === "request" ? rec?.$seen : null);
|
|
1798
|
+
reactExports.useEffect(() => {
|
|
1799
|
+
setRecords(null);
|
|
1800
|
+
setSelected(0);
|
|
1801
|
+
}, [sessionId, date, granularity]);
|
|
1802
|
+
reactExports.useEffect(() => {
|
|
1803
|
+
let cancelled = false;
|
|
1804
|
+
window.api.readRequests(sessionId, date || null, granularity).then((res) => {
|
|
1805
|
+
if (!cancelled) setRecords(res);
|
|
1806
|
+
});
|
|
1807
|
+
return () => {
|
|
1808
|
+
cancelled = true;
|
|
1809
|
+
};
|
|
1810
|
+
}, [sessionId, date, granularity, fileSize]);
|
|
1811
|
+
if (!records) return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-empty", children: "Loading…" }) });
|
|
1812
|
+
if (!records.length) {
|
|
1813
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-empty", children: "No captured requests for this session. Requests are recorded only while the capture proxy is running." }) });
|
|
1814
|
+
}
|
|
1815
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-view", children: [
|
|
1816
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "requests-list", children: records.map((r, i) => {
|
|
1817
|
+
const [method, path] = splitUrl(r.url);
|
|
1818
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: i > 0 && r.kind?.[0] === "main" ? "new-turn" : "", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: i === selected ? "active" : "", onClick: () => setSelected(i), children: [
|
|
1819
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-method ${method.toLowerCase()}`, children: method }),
|
|
1820
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-url", children: path }),
|
|
1821
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-status ${!r.status || r.status >= 400 ? "error" : "ok"}`, children: r.status ?? "—" }),
|
|
1822
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta", children: time(r.timestamp) }),
|
|
1823
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-duration", children: duration(r.durationMs) }),
|
|
1824
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-kind ${r.kind ? `kind-${r.kind[0]}` : ""}`, children: r.kind?.[1] }),
|
|
1825
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-tokens", title: "total tokens (input + output + cache)", children: tokens(totalTokens(r)) })
|
|
1826
|
+
] }) }, i);
|
|
1827
|
+
}) }),
|
|
1828
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-detail", children: [
|
|
1829
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-tabs", children: ["request", "response"].map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: t === tab ? "active" : "", onClick: () => setTab(t), children: t }, t)) }),
|
|
1830
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Pane, { value: body, headers, seen, expandAll })
|
|
1831
|
+
] })
|
|
1832
|
+
] });
|
|
1833
|
+
}
|
|
1834
|
+
const CONTEXT_WINDOW$1 = THRESHOLDS.context.danger;
|
|
1835
|
+
let MAX_LINES = 10;
|
|
1836
|
+
let MAX_LINE_CHARS = 200;
|
|
1837
|
+
let MODEL = null;
|
|
1838
|
+
function truncateLine(line) {
|
|
1839
|
+
if (line.length <= MAX_LINE_CHARS) return line;
|
|
1840
|
+
return line.slice(0, MAX_LINE_CHARS) + ` …(${line.length - MAX_LINE_CHARS} chars truncated)`;
|
|
1841
|
+
}
|
|
1842
|
+
function truncate(text) {
|
|
1843
|
+
const lines = String(text ?? "").split("\n").map(truncateLine);
|
|
1844
|
+
if (lines.length <= MAX_LINES) return lines.join("\n");
|
|
1845
|
+
return lines.slice(0, MAX_LINES).join("\n") + `
|
|
1846
|
+
--- ${lines.length - MAX_LINES} lines truncated ---`;
|
|
1847
|
+
}
|
|
1848
|
+
function fence(text, lang = "") {
|
|
1849
|
+
return "```" + lang + "\n" + truncate(text) + "\n```";
|
|
1850
|
+
}
|
|
1851
|
+
function resultText(r) {
|
|
1852
|
+
const parts = Array.isArray(r?.content) ? r.content : [r?.content];
|
|
1853
|
+
return parts.map((p) => typeof p === "string" ? p : p?.text ?? JSON.stringify(p)).join("\n");
|
|
1854
|
+
}
|
|
1855
|
+
function renderBlock(b) {
|
|
1856
|
+
if (b.type === "text") return truncate(b.text || "");
|
|
1857
|
+
if (b.type === "thinking") return `thinking:
|
|
1858
|
+
${fence(b.thinking || "")}`;
|
|
1859
|
+
if (b.type === "tool_use") {
|
|
1860
|
+
const tail = b.result ? `
|
|
1861
|
+
${b.result.is_error ? "error" : "result"}:
|
|
1862
|
+
${fence(resultText(b.result))}` : "";
|
|
1863
|
+
const title = toolSummary(b.name, b.input);
|
|
1864
|
+
return `[${title}]
|
|
1865
|
+
${fence(JSON.stringify(b.input, null, 2), "json")}${tail}`;
|
|
1866
|
+
}
|
|
1867
|
+
if (b.type === "tool_result") return `${b.is_error ? "error" : "result"}:
|
|
1868
|
+
${fence(resultText(b))}`;
|
|
1869
|
+
if (b.type === "image") return "[image]";
|
|
1870
|
+
if (b.type === "instruction") {
|
|
1871
|
+
const it = b.it;
|
|
1872
|
+
const path = it.name && it.name !== it.file_path ? `
|
|
1873
|
+
\`${it.file_path}\`` : "";
|
|
1874
|
+
return `_${instructionTitle(it, MODEL)}_${path}
|
|
1875
|
+
${fence(it.error ? `error: ${it.error}` : it.content || "")}`;
|
|
1876
|
+
}
|
|
1877
|
+
return fence(JSON.stringify(b, null, 2), "json");
|
|
1878
|
+
}
|
|
1879
|
+
function renderTurn(t) {
|
|
1880
|
+
if (t.role === "instruction") return `**Instructions loaded:**
|
|
1881
|
+
|
|
1882
|
+
${t.blocks.map(renderBlock).join("\n\n")}`;
|
|
1883
|
+
const role = t.role === "user" ? "User" : `Assistant`;
|
|
1884
|
+
const tokens2 = t.tokenTotal > 0 && t.tokenDelta != null ? ` (${t.tokenDelta >= 0 ? "+" : ""}${fmtCompact(t.tokenDelta)} / ${fmtCompact(t.tokenTotal)} tokens)` : "";
|
|
1885
|
+
return `**${role}${tokens2}:**
|
|
1886
|
+
|
|
1887
|
+
${t.blocks.map(renderBlock).join("\n\n")}`;
|
|
1888
|
+
}
|
|
1889
|
+
const TRUNCATE_LINES = 10;
|
|
1890
|
+
const TRUNCATE_LINE_CHARS = 200;
|
|
1891
|
+
function markdownSession(meta, items, truncated, instructions = []) {
|
|
1892
|
+
MAX_LINES = truncated ? TRUNCATE_LINES : Infinity;
|
|
1893
|
+
MAX_LINE_CHARS = truncated ? TRUNCATE_LINE_CHARS : Infinity;
|
|
1894
|
+
const t = meta.tokens;
|
|
1895
|
+
const stu = meta.serverToolUse;
|
|
1896
|
+
const wallDuration = meta.lastActivityAt - meta.startedAt;
|
|
1897
|
+
const contextPct = Math.round(meta.lastContextTokens / CONTEXT_WINDOW$1 * 100);
|
|
1898
|
+
const cacheHitPct = (meta.cacheHitRatio * 100).toFixed(0);
|
|
1899
|
+
const turns = items ? flatten(items, instructions) : null;
|
|
1900
|
+
MODEL = turns ? currentModel(turns) : null;
|
|
1901
|
+
const transcript = turns ? turns.map(renderTurn).join("\n\n---\n\n") : "_Loading…_";
|
|
1902
|
+
const conversation = `
|
|
1903
|
+
# Conversation
|
|
1904
|
+
|
|
1905
|
+
${transcript}`;
|
|
1906
|
+
const summary = `
|
|
1907
|
+
# Session: ${meta.sessionId}
|
|
1908
|
+
|
|
1909
|
+
# Summary
|
|
1910
|
+
- Estimated cost: ${fmtUSD(meta.cost)}
|
|
1911
|
+
- Total tokens: ${fmtCompact(meta.totalTokens)}
|
|
1912
|
+
- Working time: ${fmtDuration(meta.activeMs)}
|
|
1913
|
+
- Context size: ${fmtCompact(meta.lastContextTokens)} / ${fmtCompact(CONTEXT_WINDOW$1)} (${contextPct}%)${meta.savings.using5mCache > 0 ? `
|
|
1914
|
+
|
|
1915
|
+
## Potential savings
|
|
1916
|
+
- with 5m cache: ${meta.cost > 0 ? `(${(meta.savings.using5mCache / meta.cost * 100).toFixed(0)}%) ` : ""}−${fmtUSD(meta.savings.using5mCache)}` : ""}
|
|
1917
|
+
|
|
1918
|
+
## Tokens
|
|
1919
|
+
- Input: ${fmtNum(t.input)}
|
|
1920
|
+
- Output: ${fmtNum(t.output)}
|
|
1921
|
+
- Cache write (5m): ${fmtNum(t.cacheCreation5m || 0)}
|
|
1922
|
+
- Cache write (1h): ${fmtNum(t.cacheCreation1h || 0)}
|
|
1923
|
+
- Cache read: ${fmtNum(t.cacheRead)}
|
|
1924
|
+
- Cache hit ratio: ${cacheHitPct}%
|
|
1925
|
+
- Server tools (search / fetch): ${fmtNum(stu.webSearch)} / ${fmtNum(stu.webFetch)}
|
|
1926
|
+
|
|
1927
|
+
## Activity
|
|
1928
|
+
- Started: ${format(meta.startedAt, "pp")}
|
|
1929
|
+
- Last activity: ${format(meta.lastActivityAt, "pp")}
|
|
1930
|
+
- Wall duration: ${fmtDuration(wallDuration)}
|
|
1931
|
+
- Active periods: ${fmtNum(meta.activityPeriods.length)}
|
|
1932
|
+
- Messages: ${fmtNum(meta.messageCount)}
|
|
1933
|
+
- Tool calls: ${fmtNum(meta.toolCalls)}
|
|
1934
|
+
|
|
1935
|
+
## Identity
|
|
1936
|
+
- Model: ${meta.models.join(", ") || "—"}
|
|
1937
|
+
- Service tier: ${meta.serviceTier || "—"}
|
|
1938
|
+
- Speed: ${meta.speed ? meta.speed === "fast" ? meta.fastPricingUnknown ? "fast (pricing unknown)" : "fast" : meta.speed : "—"}
|
|
1939
|
+
- Git branch: ${meta.gitBranch || "—"}
|
|
1940
|
+
- Source: ${meta.source || meta.entrypoint || "—"}
|
|
1941
|
+
- Scheduled: ${meta.hasScheduledTask ? "yes" : "no"}
|
|
1942
|
+
- CLI version: ${meta.version || "—"}
|
|
1943
|
+
- Project: ${meta.project || "—"}
|
|
1944
|
+
${meta.worktreePath ? `- Worktree: ${meta.worktreePath}
|
|
1945
|
+
` : ""}- Log file: ${meta.filePath}
|
|
1946
|
+
- File size: ${fmtBytes(meta.fileSize)}
|
|
1947
|
+
`;
|
|
1948
|
+
const body = `
|
|
1949
|
+
<summary>
|
|
1950
|
+
${summary}
|
|
1951
|
+
</summary>
|
|
1952
|
+
|
|
1953
|
+
|
|
1954
|
+
<transcript>
|
|
1955
|
+
${conversation}
|
|
1956
|
+
</transcript>`.trim();
|
|
1957
|
+
return { body };
|
|
1958
|
+
}
|
|
1959
|
+
function Toggle({ checked, onChange, label, className = "" }) {
|
|
1960
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `toggle ${className}`, children: [
|
|
1961
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
1962
|
+
"input",
|
|
1963
|
+
{
|
|
1964
|
+
type: "checkbox",
|
|
1965
|
+
checked,
|
|
1966
|
+
onChange: (e) => onChange(e.target.checked)
|
|
1967
|
+
}
|
|
1968
|
+
),
|
|
1969
|
+
label && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "toggle-label", children: label })
|
|
1970
|
+
] });
|
|
1971
|
+
}
|
|
1972
|
+
function PromptEditor({ source, edited, setEdited, pretty }) {
|
|
1973
|
+
const [editing, setEditing] = reactExports.useState(false);
|
|
1974
|
+
const value = edited || source;
|
|
1975
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "prompt-editor-wrap", children: [
|
|
1976
|
+
edited && edited !== source && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "prompt-editor-reset", onClick: (e) => {
|
|
1977
|
+
e.stopPropagation();
|
|
1978
|
+
setEdited("");
|
|
1979
|
+
}, children: "Reset" }),
|
|
1980
|
+
pretty && !editing ? /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "prompt-editor-styled", text: value, onClick: () => setEditing(true), title: "Click to edit" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
1981
|
+
"textarea",
|
|
1982
|
+
{
|
|
1983
|
+
className: "prompt-editor-raw",
|
|
1984
|
+
value,
|
|
1985
|
+
spellCheck: true,
|
|
1986
|
+
autoFocus: pretty,
|
|
1987
|
+
onChange: (e) => setEdited(e.target.value),
|
|
1988
|
+
onBlur: pretty ? () => setEditing(false) : void 0
|
|
1989
|
+
}
|
|
1990
|
+
)
|
|
1991
|
+
] });
|
|
1992
|
+
}
|
|
1993
|
+
function createTypewriter(onText) {
|
|
1994
|
+
let buffer = "";
|
|
1995
|
+
const tick = () => {
|
|
1996
|
+
const n = Math.max(1, Math.ceil(buffer.length / 24));
|
|
1997
|
+
onText(buffer.slice(0, n));
|
|
1998
|
+
buffer = buffer.slice(n);
|
|
1999
|
+
if (buffer) requestAnimationFrame(tick);
|
|
2000
|
+
};
|
|
2001
|
+
const push = (chunk) => {
|
|
2002
|
+
if (!chunk) return;
|
|
2003
|
+
const wasIdle = !buffer;
|
|
2004
|
+
buffer += chunk;
|
|
2005
|
+
if (wasIdle) requestAnimationFrame(tick);
|
|
2006
|
+
};
|
|
2007
|
+
push.reset = () => {
|
|
2008
|
+
buffer = "";
|
|
2009
|
+
};
|
|
2010
|
+
return push;
|
|
2011
|
+
}
|
|
2012
|
+
function AgentOutput({ output, pretty, running, error }) {
|
|
2013
|
+
const [displayed, setDisplayed] = reactExports.useState(output);
|
|
2014
|
+
const lastOutputRef = reactExports.useRef(output);
|
|
2015
|
+
const scrollRef = reactExports.useRef(null);
|
|
2016
|
+
const typer = reactExports.useRef(createTypewriter((chunk) => setDisplayed((p) => p + chunk)));
|
|
2017
|
+
reactExports.useEffect(() => {
|
|
2018
|
+
const prev = lastOutputRef.current;
|
|
2019
|
+
lastOutputRef.current = output;
|
|
2020
|
+
if (output.startsWith(prev)) {
|
|
2021
|
+
const diff = output.slice(prev.length);
|
|
2022
|
+
if (diff) typer.current(diff);
|
|
2023
|
+
} else {
|
|
2024
|
+
typer.current.reset();
|
|
2025
|
+
setDisplayed(output);
|
|
2026
|
+
}
|
|
2027
|
+
}, [output]);
|
|
2028
|
+
reactExports.useEffect(() => {
|
|
2029
|
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
2030
|
+
}, [displayed, running, error]);
|
|
2031
|
+
if (!displayed && !running && !error) return null;
|
|
2032
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-prompt-output", ref: scrollRef, children: [
|
|
2033
|
+
pretty ? /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { text: displayed }) : /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "agent-view-raw", children: displayed }),
|
|
2034
|
+
running && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-prompt-typing", "aria-label": "Running", children: [
|
|
2035
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "dot" }),
|
|
2036
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "dot" }),
|
|
2037
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "dot" })
|
|
2038
|
+
] }),
|
|
2039
|
+
error && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "agent-prompt-error", children: error })
|
|
2040
|
+
] });
|
|
2041
|
+
}
|
|
2042
|
+
function AgentView({ meta, items, instructions, agent, onClose }) {
|
|
2043
|
+
const [pretty, setPretty] = useLocalStorage("agent.pretty", true);
|
|
2044
|
+
const [copied, setCopied] = reactExports.useState(false);
|
|
2045
|
+
const { body } = markdownSession(meta, items, agent.truncated, instructions);
|
|
2046
|
+
const combinedText = `${agent.prompt}
|
|
2047
|
+
|
|
2048
|
+
---
|
|
2049
|
+
${body}`;
|
|
2050
|
+
const onCopy = async () => {
|
|
2051
|
+
await navigator.clipboard.writeText(combinedText);
|
|
2052
|
+
setCopied(true);
|
|
2053
|
+
setTimeout(() => setCopied(false), 1500);
|
|
2054
|
+
};
|
|
2055
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-view", children: [
|
|
2056
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-view-actions", children: [
|
|
2057
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Toggle, { checked: agent.truncated, onChange: agent.setTruncated, label: "truncated" }),
|
|
2058
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Toggle, { checked: pretty, onChange: setPretty, label: "human-friendly" }),
|
|
2059
|
+
onClose && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "agent-view-close", onClick: onClose, "aria-label": "Close", children: "✕" })
|
|
2060
|
+
] }),
|
|
2061
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-view-left", children: [
|
|
2062
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "agent-view-label", children: "Session" }),
|
|
2063
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "agent-view-block", children: pretty ? /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { text: body }) : /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "agent-view-raw", children: body }) })
|
|
2064
|
+
] }),
|
|
2065
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-view-right", children: [
|
|
2066
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-prompt-wrap", children: [
|
|
2067
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "agent-view-label", children: "Prompt" }),
|
|
2068
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "agent-view-block", children: /* @__PURE__ */ jsxRuntimeExports.jsx(PromptEditor, { source: agent.defaultPrompt, edited: agent.editedPrompt, setEdited: agent.setEditedPrompt, pretty }) }),
|
|
2069
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-prompt-actions", children: [
|
|
2070
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "button-primary", onClick: onCopy, children: copied ? "Copied" : "Copy" }),
|
|
2071
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "button-primary", onClick: () => agent.send(combinedText), disabled: agent.running, children: "Send" })
|
|
2072
|
+
] })
|
|
2073
|
+
] }),
|
|
2074
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "agent-view-output-wrap", children: [
|
|
2075
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "agent-view-label", children: "AI Result" }),
|
|
2076
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(AgentOutput, { output: agent.output, pretty, running: agent.running, error: agent.error })
|
|
2077
|
+
] })
|
|
2078
|
+
] })
|
|
2079
|
+
] });
|
|
2080
|
+
}
|
|
2081
|
+
const CONTEXT_WINDOW = THRESHOLDS.context.danger;
|
|
2082
|
+
function SessionSummary({ meta, items, instructions, agent, onOpenAgent, granularity = "day" }) {
|
|
2083
|
+
const timeFormat = granularity === "day" ? "pp" : "MMM d, pp";
|
|
2084
|
+
const onAnalyze = () => {
|
|
2085
|
+
const { body } = markdownSession(meta, items, agent.truncated, instructions);
|
|
2086
|
+
agent.send(`${agent.prompt}
|
|
2087
|
+
|
|
2088
|
+
---
|
|
2089
|
+
${body}`);
|
|
2090
|
+
};
|
|
2091
|
+
const t = meta.tokens;
|
|
2092
|
+
const totalTokens2 = meta.totalTokens;
|
|
2093
|
+
const wallDuration = meta.lastActivityAt - meta.startedAt;
|
|
2094
|
+
const activeDuration = meta.activeMs;
|
|
2095
|
+
const cost = meta.cost;
|
|
2096
|
+
const cacheHitRatio = meta.cacheHitRatio;
|
|
2097
|
+
const stu = meta.serverToolUse;
|
|
2098
|
+
const hasServerTools = stu.webSearch > 0 || stu.webFetch > 0;
|
|
2099
|
+
const modelLabel = meta.models.join(", ") || "—";
|
|
2100
|
+
const contextRatio = meta.lastContextTokens / CONTEXT_WINDOW;
|
|
2101
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-summary", children: [
|
|
2102
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "summary-ai", children: [
|
|
2103
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "summary-ai-row", children: [
|
|
2104
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2105
|
+
"button",
|
|
2106
|
+
{
|
|
2107
|
+
type: "button",
|
|
2108
|
+
className: "button-primary",
|
|
2109
|
+
onClick: onAnalyze,
|
|
2110
|
+
disabled: agent.running || !items,
|
|
2111
|
+
children: agent.running ? "⚡︎ Analyzing…" : "⚡︎ AI Analyze"
|
|
2112
|
+
}
|
|
2113
|
+
),
|
|
2114
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2115
|
+
"button",
|
|
2116
|
+
{
|
|
2117
|
+
type: "button",
|
|
2118
|
+
className: "summary-ai-gear",
|
|
2119
|
+
onClick: onOpenAgent,
|
|
2120
|
+
title: "Open Agent tab",
|
|
2121
|
+
"aria-label": "Open Agent tab",
|
|
2122
|
+
children: "⚙"
|
|
2123
|
+
}
|
|
2124
|
+
)
|
|
2125
|
+
] }),
|
|
2126
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "summary-ai-output", children: /* @__PURE__ */ jsxRuntimeExports.jsx(AgentOutput, { output: agent.output, pretty: true, running: agent.running, error: agent.error }) })
|
|
2127
|
+
] }),
|
|
2128
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Section, { title: "Summary", children: [
|
|
2129
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Estimated cost", value: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
2130
|
+
meta.priceUnknown && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "danger-badge", title: `No price entry for ${modelLabel} — cost is missing or understated`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(DollarSign, { size: 11 }) }),
|
|
2131
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: tone(cost, THRESHOLDS.cost), children: fmtUSD(cost) })
|
|
2132
|
+
] }) }),
|
|
2133
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Total tokens", value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: tone(totalTokens2, THRESHOLDS.tokens), children: fmtCompact(totalTokens2) }) }),
|
|
2134
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Working time", value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: tone(activeDuration, THRESHOLDS.workTime), children: fmtDuration(activeDuration) }) }),
|
|
2135
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Context size", value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: tone(meta.lastContextTokens, THRESHOLDS.context), children: `${fmtCompact(meta.lastContextTokens)} / ${fmtCompact(CONTEXT_WINDOW)}` }), below: /* @__PURE__ */ jsxRuntimeExports.jsx(Bar, { ratio: contextRatio, tone: tone(meta.lastContextTokens, THRESHOLDS.context) }) })
|
|
2136
|
+
] }),
|
|
2137
|
+
meta.savings.using5mCache > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(Section, { title: "Potential savings", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2138
|
+
Field,
|
|
2139
|
+
{
|
|
2140
|
+
label: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
2141
|
+
cost > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("strong", { children: [
|
|
2142
|
+
"-",
|
|
2143
|
+
(meta.savings.using5mCache / cost * 100).toFixed(0),
|
|
2144
|
+
"% "
|
|
2145
|
+
] }),
|
|
2146
|
+
"with 5m cache"
|
|
2147
|
+
] }),
|
|
2148
|
+
value: `−${fmtUSD(meta.savings.using5mCache)}`
|
|
2149
|
+
}
|
|
2150
|
+
) }),
|
|
2151
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Section, { title: "Tokens", children: [
|
|
2152
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Input", value: fmtNum(t.input) }),
|
|
2153
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Output", value: fmtNum(t.output) }),
|
|
2154
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Cache write (5m)", value: fmtNum(t.cacheCreation5m || 0) }),
|
|
2155
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2156
|
+
Field,
|
|
2157
|
+
{
|
|
2158
|
+
label: "Cache write (1h)",
|
|
2159
|
+
value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: t.cacheCreation1h > 0 ? "warn" : "", children: fmtNum(t.cacheCreation1h || 0) })
|
|
2160
|
+
}
|
|
2161
|
+
),
|
|
2162
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Cache read", value: fmtNum(t.cacheRead) }),
|
|
2163
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Cache hit ratio", value: `${(cacheHitRatio * 100).toFixed(0)}%` }),
|
|
2164
|
+
hasServerTools && /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2165
|
+
Field,
|
|
2166
|
+
{
|
|
2167
|
+
label: "Server tools (search / fetch)",
|
|
2168
|
+
value: `${fmtNum(stu.webSearch)} / ${fmtNum(stu.webFetch)}`
|
|
2169
|
+
}
|
|
2170
|
+
)
|
|
2171
|
+
] }),
|
|
2172
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Section, { title: "Activity", children: [
|
|
2173
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Started", value: format(meta.startedAt, timeFormat) }),
|
|
2174
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Last activity", value: format(meta.lastActivityAt, timeFormat) }),
|
|
2175
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Wall duration", value: fmtDuration(wallDuration) }),
|
|
2176
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Active periods", value: fmtNum(meta.activityPeriods.length) }),
|
|
2177
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Messages", value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: tone(meta.messageCount, THRESHOLDS.messages), children: fmtNum(meta.messageCount) }) }),
|
|
2178
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Tool calls", value: fmtNum(meta.toolCalls) })
|
|
2179
|
+
] }),
|
|
2180
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Section, { title: "Identity", children: [
|
|
2181
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Model", value: modelLabel, mono: true }),
|
|
2182
|
+
meta.serviceTier && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Service tier", value: meta.serviceTier }),
|
|
2183
|
+
meta.speed && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Speed", value: meta.speed === "fast" ? meta.fastPricingUnknown ? "fast (pricing unknown)" : "fast" : meta.speed }),
|
|
2184
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Git branch", value: meta.gitBranch || "—", mono: true }),
|
|
2185
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Source", value: meta.source || meta.entrypoint || "—" }),
|
|
2186
|
+
meta.hasScheduledTask && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Scheduled", value: "yes" }),
|
|
2187
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "CLI version", value: meta.version || "—", mono: true }),
|
|
2188
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Session ID", value: meta.sessionId, mono: true, full: true, autoselect: true }),
|
|
2189
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Project", value: meta.project || "—", mono: true, full: true, autoselect: true }),
|
|
2190
|
+
meta.worktreePath && /* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Worktree", value: meta.worktreePath, mono: true, full: true, autoselect: true }),
|
|
2191
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "Log file", value: meta.filePath, mono: true, full: true, autoselect: true }),
|
|
2192
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { label: "File size", value: fmtBytes(meta.fileSize) })
|
|
2193
|
+
] })
|
|
2194
|
+
] });
|
|
2195
|
+
}
|
|
2196
|
+
function Section({ title, children }) {
|
|
2197
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "summary-section", children: [
|
|
2198
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "summary-section-title", children: title }),
|
|
2199
|
+
children
|
|
2200
|
+
] });
|
|
2201
|
+
}
|
|
2202
|
+
function Bar({ ratio, tone: tone2 }) {
|
|
2203
|
+
const pct = Math.max(0, Math.min(1, ratio)) * 100;
|
|
2204
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `progress-bar tone-${tone2}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "progress-bar-fill", style: { width: `${pct}%` } }) });
|
|
2205
|
+
}
|
|
2206
|
+
function Field({ label, value, mono, full, below, autoselect }) {
|
|
2207
|
+
const selectAll = autoselect ? (e) => {
|
|
2208
|
+
const range = document.createRange();
|
|
2209
|
+
range.selectNodeContents(e.currentTarget);
|
|
2210
|
+
const sel = window.getSelection();
|
|
2211
|
+
sel.removeAllRanges();
|
|
2212
|
+
sel.addRange(range);
|
|
2213
|
+
} : void 0;
|
|
2214
|
+
if (below) {
|
|
2215
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "field has-below", children: [
|
|
2216
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "field-row", children: [
|
|
2217
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "field-label", children: label }),
|
|
2218
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `field-value ${mono ? "mono" : ""}`, onClick: selectAll, children: value })
|
|
2219
|
+
] }),
|
|
2220
|
+
below
|
|
2221
|
+
] });
|
|
2222
|
+
}
|
|
2223
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `field ${full ? "full" : ""}`, children: [
|
|
2224
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "field-label", children: label }),
|
|
2225
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `field-value ${mono ? "mono" : ""}`, onClick: selectAll, children: value })
|
|
2226
|
+
] });
|
|
2227
|
+
}
|
|
2228
|
+
const ANALYZE_PROMPT = "# Role\r\n\r\nYou are reviewing a Claude Code session and giving the user practical advice on how to run a similar session more cheaply next time.\r\n\r\nThe user controls the prompts and the request settings around them: **model** (Opus / Sonnet / Haiku), **prompt caching**, enabled **system tools** (web search, web fetch, bash, MCP, etc.), context-window size, and CLAUDE.md. They cannot change Claude's internal reasoning. Look for missed opportunities to apply Anthropic's official Claude Code guidance.\r\n\r\n# Levers\r\n\r\nThe list below covers the most common waste patterns and should be your starting point — but it is **not exhaustive**. If you spot a different concrete inefficiency in this session that the user could change next time, raise it. Don't force-fit a session into a listed lever, and don't ignore real waste just because it isn't named here.\r\n\r\n**Out of scope — do NOT suggest these.** Anything about *how Claude itself works internally* is not under user control and must not appear in the output:\r\n\r\n - How Claude batches or sequences tool calls (e.g. \"group edits per file\", \"batch related changes into one pass\", \"make parallel tool calls\").\r\n - How Claude searches, reads, or explores (e.g. \"use Grep before Read\", \"use Glob to narrow first\").\r\n - How Claude reasons, plans, or thinks.\r\n - Anything the user would have to micromanage Claude into doing turn-by-turn — even if technically prompt-able, it isn't a realistic next-time change.\r\n\r\nStick to changes the user makes in *their* prompt, *their* settings, or *their* session structure.\r\n\r\n### Better initial prompt\r\nThe single biggest lever — vague prompts cause exploration, wrong-problem code, and correction rounds.\r\n\r\n - Scope the task, name the file (`@path/to/file`), describe the symptom and likely location, reference an existing pattern.\r\n - Provide verification criteria upfront: tests, expected outputs, screenshots, error messages.\r\n - Paste rich content directly (errors, logs, screenshots, URLs) instead of asking Claude to reproduce, search, or guess.\r\n - Use plan mode for uncertain / multi-file work; skip it for trivial fixes.\r\n\r\n### Context management\r\nEach later turn re-pays the full prior context.\r\n\r\n - `/clear` between unrelated tasks (\"kitchen sink session\" anti-pattern) — often the biggest win.\r\n - `/compact <focus>` with explicit preserve-instructions before context drifts past ~50%.\r\n - `/rewind` to redirect early. After two failed corrections, `/clear` and restart with a better prompt.\r\n - Subagents for investigation — they explore in a separate context and report a summary.\r\n - Bloated CLAUDE.md is loaded every turn. If the session shows Claude ignoring a rule that's *in* CLAUDE.md, the file is probably too long.\r\n\r\n\r\n### Request settings\r\nVisible in `<summary>`.\r\n\r\n - **Model**: Opus used for work Sonnet or Haiku could have done.\r\n - **Caching**: a low cache-hit ratio means re-paying for the same context every turn. Bursty sessions, mid-session CLAUDE.md edits, tool-list changes, or long idle gaps bust the cache.\r\n - **5m vs 1h cache**: 5m is the right default for almost all sessions and should be used most of the time. A 1h write costs ~2× a 5m write, so it only pays off in narrow cases — when the session reliably has gaps >5 min between turns (scheduled / background tasks, long manual review breaks, sessions resumed hours later). Most interactive coding does NOT qualify. Only flag 1h-caching as a fix when the activity timestamps in `<summary>` clearly show long gaps AND the cache-hit ratio is poor. Conversely, if the session is tight (<5 min gaps) but `Cache write (1h)` is non-trivial, that's overspend — recommend dropping back to 5m.\r\n - **Unused system tools** (web search/fetch, MCP, bash, etc.) add tokens every turn even when they don't fire — disable what wasn't needed. Expensive web-search/fetch loops can also be short-circuited by pasting a URL or doc.\r\n - **Context cap**: 1M-context for work that fits in 200k makes every cached token more expensive.\r\n\r\n\r\n\r\n# Input\r\n\r\n`<summary>` is rich — the app pre-computes it for you. Read every field and reason from numbers, not vibes\r\n`<transcript>` shows the actual turns, labeled with per-turn token deltas like `(+12k / 340k tokens)`. Use the deltas to find expensive moments and to ground each recommendation in a concrete event. **Instructions loaded** rows are CLAUDE.md / memory files Claude Code injected into the system prompt — they re-pay on every turn, so their size and relevance are a direct cost lever.\r\n{{TRUNCATION_NOTE}}\r\n\r\n# How to analyze\r\nSilently — don't show your work.\r\n\r\n 1. **Read `<summary>` first.** Several recommendations come straight from these numbers without needing the transcript.\r\n 2. **Then scan `<transcript>`** for the 2–4 most expensive moments via token deltas. What was happening? Was the prompt vague? Was context already large?\r\n 3. Cross-reference: do the transcript moments confirm or contradict the `<summary>` signals? E.g. low cache-hit ratio + bursty activity timestamps + small mid-session edits → cache-busting pattern.\r\n 4. Match findings against the levers above, OR identify other concrete inefficiencies the user could fix. Pick up to 3 changes that would have saved the most tokens *for this specific session*.\r\n 5. Drop any item you can't tie to a concrete signal (a number from `<summary>` or a moment in `<transcript>`).\r\n 6. Estimate a single combined savings figure (conservative). No per-item dollars.\r\n\r\n# Output\r\n\r\n## Rules\r\n - Audience: a technical user in a hurry. Skimmable in ~10 seconds. No preamble, no closing remarks, no hedging. Avoid jargon, model internals, and acronyms.\r\n - Each item: short instruction and why, tied to this session. No second sentence, no dollar figure.\r\n - Rank by impact. Fewer items are fine — one strong item beats many weak ones.\r\n - If the session was already efficient (no meaningful waste), output the one-line summary, then `I don't see any meaningful savings.` on its own line, and stop. Do not invent items to fill the list.\r\n\r\n## Format\r\n\r\n<one-line session summary, ≤20 words>\r\n\r\n**How to optimize?**\r\n\r\nN. **<instruction, ≤8 words>** — <why, tied to this session, ≤10 words>\r\n\r\n**Estimated savings: X% (about $X)**\r\n";
|
|
2229
|
+
const TRUNCATE_NOTE = ` - Note: long content was truncated for display (lines >${TRUNCATE_LINE_CHARS} chars, blocks >${TRUNCATE_LINES} lines). The original session had the full content — base advice on what was clearly happening, not on the truncation.`;
|
|
2230
|
+
function useAgent(resetKey) {
|
|
2231
|
+
const [truncated, setTruncated] = useLocalStorage("agent.truncated", true);
|
|
2232
|
+
const [editedPrompt, setEditedPrompt] = useLocalStorage("agent.prompt", "");
|
|
2233
|
+
const [running, setRunning] = reactExports.useState(false);
|
|
2234
|
+
const [output, setOutput] = reactExports.useState("");
|
|
2235
|
+
const [error, setError] = reactExports.useState("");
|
|
2236
|
+
const defaultPrompt = String(ANALYZE_PROMPT).replace("{{TRUNCATION_NOTE}}", truncated ? TRUNCATE_NOTE : "");
|
|
2237
|
+
const prompt = editedPrompt || defaultPrompt;
|
|
2238
|
+
reactExports.useEffect(() => window.api.onAgentOutput((chunk) => setOutput((p) => p + chunk)), []);
|
|
2239
|
+
reactExports.useEffect(() => {
|
|
2240
|
+
setError("");
|
|
2241
|
+
setRunning(false);
|
|
2242
|
+
setOutput("");
|
|
2243
|
+
}, [resetKey]);
|
|
2244
|
+
const send = async (text) => {
|
|
2245
|
+
setError("");
|
|
2246
|
+
setOutput("");
|
|
2247
|
+
setRunning(true);
|
|
2248
|
+
try {
|
|
2249
|
+
const { code } = await window.api.runAgentPrompt(text);
|
|
2250
|
+
if (code !== 0) setError(`Claude exited with code ${code}`);
|
|
2251
|
+
} catch (err) {
|
|
2252
|
+
setError(err.message || String(err));
|
|
2253
|
+
} finally {
|
|
2254
|
+
setRunning(false);
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
return { running, error, send, prompt, editedPrompt, setEditedPrompt, output, truncated, setTruncated, defaultPrompt };
|
|
2258
|
+
}
|
|
2259
|
+
const TABS = [
|
|
2260
|
+
{ key: "conversation", label: "Conversation" },
|
|
2261
|
+
{ key: "jsonl", label: "Raw Logs" },
|
|
2262
|
+
{ key: "requests", label: "API Requests" }
|
|
2263
|
+
];
|
|
2264
|
+
function Session({ meta, date, granularity = "day" }) {
|
|
2265
|
+
const [items, setItems] = reactExports.useState(null);
|
|
2266
|
+
const [instructions, setInstructions] = reactExports.useState([]);
|
|
2267
|
+
const [mode, setMode] = useLocalStorage("session.view-mode", "conversation");
|
|
2268
|
+
const [agentOpen, setAgentOpen] = reactExports.useState(false);
|
|
2269
|
+
const [expandAll, setExpandAll] = reactExports.useState(null);
|
|
2270
|
+
const sessionId = meta?.sessionId;
|
|
2271
|
+
const fileSize = meta?.fileSize;
|
|
2272
|
+
const agent = useAgent(`${sessionId}|${date}`);
|
|
2273
|
+
reactExports.useEffect(() => {
|
|
2274
|
+
setItems(null);
|
|
2275
|
+
setInstructions([]);
|
|
2276
|
+
}, [sessionId, date, granularity]);
|
|
2277
|
+
reactExports.useEffect(() => {
|
|
2278
|
+
if (!sessionId) return;
|
|
2279
|
+
let cancelled = false;
|
|
2280
|
+
window.api.readSession(sessionId, date || null, granularity).then((res) => {
|
|
2281
|
+
if (cancelled || !res) return;
|
|
2282
|
+
setItems(res.items);
|
|
2283
|
+
setInstructions(res.instructions || []);
|
|
2284
|
+
});
|
|
2285
|
+
return () => {
|
|
2286
|
+
cancelled = true;
|
|
2287
|
+
};
|
|
2288
|
+
}, [sessionId, date, granularity, fileSize]);
|
|
2289
|
+
if (!meta) {
|
|
2290
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "session-view empty", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Select a session to inspect." }) });
|
|
2291
|
+
}
|
|
2292
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "session-view", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-body", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-conversation", children: agentOpen ? /* @__PURE__ */ jsxRuntimeExports.jsx(AgentView, { meta, items, instructions, agent, onClose: () => setAgentOpen(false) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-tab-pane", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "view-tab-pane-row", children: [
|
|
2293
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "view-tab-pane-main", children: [
|
|
2294
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "view-tabs-bar", children: [
|
|
2295
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-tabs", children: TABS.map(({ key, label }) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2296
|
+
"button",
|
|
2297
|
+
{
|
|
2298
|
+
type: "button",
|
|
2299
|
+
className: `view-tab${mode === key ? " active" : ""}`,
|
|
2300
|
+
onClick: () => setMode(key),
|
|
2301
|
+
children: label
|
|
2302
|
+
},
|
|
2303
|
+
key
|
|
2304
|
+
)) }),
|
|
2305
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2306
|
+
Toggle,
|
|
2307
|
+
{
|
|
2308
|
+
checked: !!expandAll,
|
|
2309
|
+
onChange: (v) => setExpandAll(v),
|
|
2310
|
+
label: mode === "conversation" ? "Expand all" : "Full text"
|
|
2311
|
+
}
|
|
2312
|
+
)
|
|
2313
|
+
] }),
|
|
2314
|
+
mode === "conversation" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-tab-pane-content", children: items ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConversationView, { items, instructions, expandAll }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "empty", children: "Loading conversation…" }) }) : mode === "jsonl" ? /* @__PURE__ */ jsxRuntimeExports.jsx(JsonlView, { items, expandAll }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RequestsView, { sessionId, date, granularity, fileSize, expandAll })
|
|
2315
|
+
] }),
|
|
2316
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(SessionSummary, { meta, items, instructions, agent, onOpenAgent: () => setAgentOpen(true), granularity })
|
|
2317
|
+
] }) }) }) }) });
|
|
2318
|
+
}
|
|
2319
|
+
function useSwitch({ name, isOn = () => false }) {
|
|
2320
|
+
const [status, setStatus] = reactExports.useState(null);
|
|
2321
|
+
const [busy, setBusy] = reactExports.useState(false);
|
|
2322
|
+
reactExports.useEffect(() => {
|
|
2323
|
+
let alive = true;
|
|
2324
|
+
const check = () => window.api.getSwitchStatus(name).then((s) => alive && setStatus(s));
|
|
2325
|
+
check();
|
|
2326
|
+
const timer = setInterval(check, 5e3);
|
|
2327
|
+
return () => {
|
|
2328
|
+
alive = false;
|
|
2329
|
+
clearInterval(timer);
|
|
2330
|
+
};
|
|
2331
|
+
}, []);
|
|
2332
|
+
const toggle = async () => {
|
|
2333
|
+
setBusy(true);
|
|
2334
|
+
const next = await (isOn(status) ? window.api.deactivateSwitch(name) : window.api.activateSwitch(name));
|
|
2335
|
+
setStatus(next);
|
|
2336
|
+
setBusy(false);
|
|
2337
|
+
if (next.error) alert(next.error);
|
|
2338
|
+
};
|
|
2339
|
+
const setKeepActive = async (value) => {
|
|
2340
|
+
const next = await window.api.setSwitchKeepActive(name, value);
|
|
2341
|
+
setStatus(next);
|
|
2342
|
+
if (next.error) alert(next.error);
|
|
2343
|
+
};
|
|
2344
|
+
return { status, busy, toggle, setKeepActive };
|
|
2345
|
+
}
|
|
2346
|
+
function StatusSwitch({ service, on, warn, button, className = "", tooltip, changes, changesTitle = "Changes in settings.json", children }) {
|
|
2347
|
+
const ref = reactExports.useRef(null);
|
|
2348
|
+
const [tip] = reactExports.useState(() => document.createElement("div"));
|
|
2349
|
+
const [showChanges, setChanges] = reactExports.useState(false);
|
|
2350
|
+
reactExports.useEffect(() => {
|
|
2351
|
+
const t = tippy(ref.current, { content: tip, interactive: true, delay: [0, 0], appendTo: () => document.body });
|
|
2352
|
+
return () => t.destroy();
|
|
2353
|
+
}, []);
|
|
2354
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `statusbar-group statusbar-seg ${className} ${on ? "statusbar-seg-on" : ""}`, ref, children: [
|
|
2355
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `statusbar-msg ${button ? "" : on ? "statusbar-active" : "statusbar-off"}`, children: [
|
|
2356
|
+
warn !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "statusbar-warn", "data-on": warn ? "" : void 0, children: "⚠ " }),
|
|
2357
|
+
children
|
|
2358
|
+
] }),
|
|
2359
|
+
reactDomExports.createPortal(
|
|
2360
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "statusbar-tooltip", children: [
|
|
2361
|
+
tooltip,
|
|
2362
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "statusbar-tooltip-footer", children: [
|
|
2363
|
+
changes && showChanges && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "statusbar-tooltip-changes", children: [
|
|
2364
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "statusbar-tooltip-changes-title", children: changesTitle }),
|
|
2365
|
+
changes
|
|
2366
|
+
] }),
|
|
2367
|
+
service.status?.keepActive !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "statusbar-tooltip-keep", title: service.status.ephemeral ? "Keep active is not available for a temporary npx run.\nInstall the app globally to enable it." : void 0, children: [
|
|
2368
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: service.status.keepActive, disabled: service.status.ephemeral, onChange: (e) => service.setKeepActive(e.target.checked) }),
|
|
2369
|
+
"Keep active after the app closes"
|
|
2370
|
+
] }),
|
|
2371
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "statusbar-tooltip-actions", children: [
|
|
2372
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: `button-primary statusbar-tooltip-btn ${button ? "statusbar-tooltip-btn-blue" : on ? "statusbar-tooltip-btn-red" : "statusbar-tooltip-btn-green"}`, disabled: service.busy || !service.status, onClick: service.toggle, children: service.busy ? "…" : button || (on ? "Deactivate" : "Activate") }),
|
|
2373
|
+
changes && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "statusbar-tooltip-help", "aria-expanded": showChanges, onClick: () => setChanges((v) => !v), children: "?" })
|
|
2374
|
+
] })
|
|
2375
|
+
] })
|
|
2376
|
+
] }),
|
|
2377
|
+
tip
|
|
2378
|
+
)
|
|
2379
|
+
] });
|
|
2380
|
+
}
|
|
2381
|
+
const proxyTooltip = /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [
|
|
2382
|
+
"Captures Claude Code's raw API traffic - system prompt, tool definitions, injected reminders, responses.",
|
|
2383
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", {}),
|
|
2384
|
+
"Applies only to newly started Claude Code sessions."
|
|
2385
|
+
] });
|
|
2386
|
+
const proxyChanges = /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
2387
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `"env": {
|
|
2388
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:41414",
|
|
2389
|
+
"ENABLE_TOOL_SEARCH": "true"
|
|
2390
|
+
},
|
|
2391
|
+
"hooks": {
|
|
2392
|
+
"SessionStart": ["…/bin/claude/hooks.mjs"]
|
|
2393
|
+
}` }),
|
|
2394
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [
|
|
2395
|
+
"Note: ",
|
|
2396
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: "/remote-control" }),
|
|
2397
|
+
" is disabled while proxy is active."
|
|
2398
|
+
] })
|
|
2399
|
+
] });
|
|
2400
|
+
const retentionTooltip = /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [
|
|
2401
|
+
"Claude Code deletes transcripts older than ",
|
|
2402
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: "cleanupPeriodDays" }),
|
|
2403
|
+
" (default 30) - this app can only show what's left. Raise it to keep a year of history browsable."
|
|
2404
|
+
] });
|
|
2405
|
+
const retentionChanges = /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `"cleanupPeriodDays": 365` });
|
|
2406
|
+
const statuslineTooltip = /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "This app ships a status line for Claude Code showing context/token usage and rate limits." });
|
|
2407
|
+
const statuslineChanges = /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `"statusLine": {
|
|
2408
|
+
"type": "command",
|
|
2409
|
+
"command": "node …/bin/claude/statusline.mjs"
|
|
2410
|
+
}` });
|
|
2411
|
+
const claudeDirTooltip = /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
2412
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [
|
|
2413
|
+
"The folder this app reads everything from - your Claude Code sessions and settings (usually ",
|
|
2414
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: "~/.claude" }),
|
|
2415
|
+
")."
|
|
2416
|
+
] }),
|
|
2417
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Previous directories are listed in the File menu (press Alt)." })
|
|
2418
|
+
] });
|
|
2419
|
+
const claudeDirChanges = /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `"claudeDir": "…the chosen folder"` });
|
|
2420
|
+
const ONE_YEAR_DAYS = 365;
|
|
2421
|
+
function humanizeDays(days) {
|
|
2422
|
+
if (days < ONE_YEAR_DAYS) return `${days}d`;
|
|
2423
|
+
const years = days / ONE_YEAR_DAYS;
|
|
2424
|
+
return `${Number.isInteger(years) ? years : years.toFixed(1)}y`;
|
|
2425
|
+
}
|
|
2426
|
+
function StatusBar({ progress, sessionCount = 0 }) {
|
|
2427
|
+
const { claudeDir } = window.api.claudeSettings;
|
|
2428
|
+
const proxy = useSwitch({ name: "proxy", isOn: (s) => s?.running });
|
|
2429
|
+
const statusline = useSwitch({ name: "statusline", isOn: (s) => s?.installed });
|
|
2430
|
+
const retention = useSwitch({ name: "retention", isOn: (s) => s?.raised });
|
|
2431
|
+
const claudedir = useSwitch({ name: "claudedir" });
|
|
2432
|
+
const proxyRunning = proxy.status?.running;
|
|
2433
|
+
const proxyDown = proxy.status?.configured && proxyRunning === false;
|
|
2434
|
+
const retentionRaised = retention.status?.raised;
|
|
2435
|
+
const scanning = progress?.scanning && progress.total > 0;
|
|
2436
|
+
const finished = progress && !progress.scanning;
|
|
2437
|
+
const pct = scanning ? progress.done / progress.total * 100 : 0;
|
|
2438
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "statusbar", children: [
|
|
2439
|
+
(scanning || finished) && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "statusbar-loading", children: [
|
|
2440
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "statusbar-loading-text", children: [
|
|
2441
|
+
scanning ? "Loading" : "Loaded",
|
|
2442
|
+
" ",
|
|
2443
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "statusbar-loading-num", children: sessionCount }),
|
|
2444
|
+
" sessions",
|
|
2445
|
+
scanning ? "…" : ""
|
|
2446
|
+
] }),
|
|
2447
|
+
scanning && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "progress-bar", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "progress-bar-fill", style: { width: `${pct}%` } }) })
|
|
2448
|
+
] }),
|
|
2449
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: retention, on: retentionRaised, warn: !!(retention.status && !retentionRaised), tooltip: retentionTooltip, changes: retentionChanges, children: [
|
|
2450
|
+
"Session logs ",
|
|
2451
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "statusbar-state", children: retention.status ? humanizeDays(retention.status.days) : "…" })
|
|
2452
|
+
] }),
|
|
2453
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: proxy, on: proxyRunning, warn: !!proxyDown, tooltip: proxyTooltip, changes: proxyChanges, children: [
|
|
2454
|
+
"Capture proxy : ",
|
|
2455
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "statusbar-state", children: proxyRunning ? "ON" : proxyDown ? "DOWN" : "off" })
|
|
2456
|
+
] }),
|
|
2457
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: statusline, on: statusline.status?.installed, tooltip: statuslineTooltip, changes: statuslineChanges, children: [
|
|
2458
|
+
"Status line : ",
|
|
2459
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "statusbar-state", children: statusline.status?.installed ? "ON" : "off" })
|
|
2460
|
+
] }),
|
|
2461
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: claudedir, button: "Change directory", className: "statusbar-claude-dir", tooltip: claudeDirTooltip, changes: claudeDirChanges, changesTitle: "Changes in ~/.claude-discover/config.json", children: [
|
|
2462
|
+
"Claude dir: ",
|
|
2463
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("code", { children: claudeDir })
|
|
2464
|
+
] })
|
|
2465
|
+
] });
|
|
2466
|
+
}
|
|
2467
|
+
function App() {
|
|
2468
|
+
const [sessions, setSessions] = reactExports.useState([]);
|
|
2469
|
+
const [loading, setLoading] = reactExports.useState(true);
|
|
2470
|
+
const [scanProgress, setScanProgress] = reactExports.useState(null);
|
|
2471
|
+
const [granularity, setGranularity] = useLocalStorage("gantt.granularity", "day");
|
|
2472
|
+
const [anchor, setAnchor] = reactExports.useState(() => startOfPeriod(Date.now(), granularity));
|
|
2473
|
+
const [selectedId, setSelectedId] = reactExports.useState(null);
|
|
2474
|
+
const [sourceFilter, setSourceFilter] = reactExports.useState(null);
|
|
2475
|
+
const [projectFilter, setProjectFilter] = reactExports.useState(null);
|
|
2476
|
+
const { defaultLayout, onLayoutChanged } = sn({ id: "app.body", panelIds: ["list", "detail"], storage: localStorage });
|
|
2477
|
+
const { defaultLayout: rootLayout, onLayoutChanged: onRootLayoutChanged } = sn({ id: "app.root", panelIds: ["gantt", "body"], storage: localStorage });
|
|
2478
|
+
const dayRange = reactExports.useMemo(() => {
|
|
2479
|
+
const start = anchor;
|
|
2480
|
+
const end = endOfPeriod(anchor, granularity);
|
|
2481
|
+
return { start, end };
|
|
2482
|
+
}, [anchor, granularity]);
|
|
2483
|
+
reactExports.useEffect(() => {
|
|
2484
|
+
let cancelled = false;
|
|
2485
|
+
setScanProgress(null);
|
|
2486
|
+
const offP = window.api.onScanProgress((p) => {
|
|
2487
|
+
if (!cancelled) setScanProgress(p);
|
|
2488
|
+
});
|
|
2489
|
+
const timer = setTimeout(() => {
|
|
2490
|
+
window.api.listSessions(format(anchor, "yyyy-MM-dd"), granularity).then((s) => {
|
|
2491
|
+
if (!cancelled) {
|
|
2492
|
+
setSessions(s || []);
|
|
2493
|
+
setLoading(false);
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
}, 120);
|
|
2497
|
+
const off = window.api.onSessionsUpdate((s) => {
|
|
2498
|
+
if (!cancelled) setSessions(s || []);
|
|
2499
|
+
});
|
|
2500
|
+
return () => {
|
|
2501
|
+
cancelled = true;
|
|
2502
|
+
clearTimeout(timer);
|
|
2503
|
+
off();
|
|
2504
|
+
offP();
|
|
2505
|
+
};
|
|
2506
|
+
}, [anchor, granularity]);
|
|
2507
|
+
const dayItems = reactExports.useMemo(() => {
|
|
2508
|
+
const sourceFiltered = sourceFilter ? sessions.filter((s) => (s.source || "other") === sourceFilter) : sessions;
|
|
2509
|
+
const past = projectFilter ? sourceFiltered.filter((s) => (s.project || "(no project)") === projectFilter) : sourceFiltered;
|
|
2510
|
+
const availableSources = [...new Set(sessions.map((s) => s.source || "other"))];
|
|
2511
|
+
return { sourceFiltered, past, availableSources };
|
|
2512
|
+
}, [sessions, sourceFilter, projectFilter]);
|
|
2513
|
+
const selected = reactExports.useMemo(
|
|
2514
|
+
() => sessions.find((s) => s.sessionId === selectedId) || null,
|
|
2515
|
+
[sessions, selectedId]
|
|
2516
|
+
);
|
|
2517
|
+
const selectSession = reactExports.useCallback((id) => {
|
|
2518
|
+
closeFind();
|
|
2519
|
+
setSelectedId(id);
|
|
2520
|
+
}, []);
|
|
2521
|
+
const shiftPeriod = reactExports.useCallback((delta) => {
|
|
2522
|
+
setProjectFilter(null);
|
|
2523
|
+
setAnchor((a) => startOfPeriod(addPeriod(a, granularity, delta), granularity));
|
|
2524
|
+
}, [granularity]);
|
|
2525
|
+
const changeGranularity = reactExports.useCallback((g) => {
|
|
2526
|
+
setProjectFilter(null);
|
|
2527
|
+
setAnchor((a) => startOfPeriod(Math.min(endOfPeriod(a, granularity), Date.now()), g));
|
|
2528
|
+
setGranularity(g);
|
|
2529
|
+
}, [granularity, setGranularity]);
|
|
2530
|
+
if (loading) {
|
|
2531
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "app-loading", children: "Loading sessions…" });
|
|
2532
|
+
}
|
|
2533
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "app", children: [
|
|
2534
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
2535
|
+
Xt,
|
|
2536
|
+
{
|
|
2537
|
+
orientation: "vertical",
|
|
2538
|
+
className: "app-main",
|
|
2539
|
+
defaultLayout: rootLayout,
|
|
2540
|
+
onLayoutChanged: onRootLayoutChanged,
|
|
2541
|
+
children: [
|
|
2542
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Zt, { id: "gantt", defaultSize: 400, minSize: 180, className: "gantt-pane", children: [
|
|
2543
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2544
|
+
Toolbar,
|
|
2545
|
+
{
|
|
2546
|
+
granularity,
|
|
2547
|
+
onSetGranularity: changeGranularity,
|
|
2548
|
+
dayAnchor: anchor,
|
|
2549
|
+
onShiftDay: shiftPeriod,
|
|
2550
|
+
onResetToday: () => {
|
|
2551
|
+
setProjectFilter(null);
|
|
2552
|
+
setAnchor(startOfPeriod(Date.now(), granularity));
|
|
2553
|
+
},
|
|
2554
|
+
sourceFilter,
|
|
2555
|
+
availableSources: dayItems.availableSources,
|
|
2556
|
+
onToggleSourceFilter: (src) => setSourceFilter((cur) => cur === src ? null : src)
|
|
2557
|
+
}
|
|
2558
|
+
),
|
|
2559
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "gantt-body", children: [
|
|
2560
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2561
|
+
GanttChart,
|
|
2562
|
+
{
|
|
2563
|
+
dayRange,
|
|
2564
|
+
sessions: dayItems.sourceFiltered,
|
|
2565
|
+
onSelect: selectSession,
|
|
2566
|
+
selectedId,
|
|
2567
|
+
dayAnchor: anchor,
|
|
2568
|
+
granularity,
|
|
2569
|
+
projectFilter,
|
|
2570
|
+
onToggleProjectFilter: (project) => setProjectFilter((cur) => cur === project ? null : project)
|
|
2571
|
+
}
|
|
2572
|
+
),
|
|
2573
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(PeriodSummary, { sessions: dayItems.past, dayAnchor: anchor, granularity })
|
|
2574
|
+
] })
|
|
2575
|
+
] }),
|
|
2576
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(tn, { className: "resize-handle resize-handle-h" }),
|
|
2577
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Zt, { id: "body", minSize: 20, className: "body-outer", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
2578
|
+
Xt,
|
|
2579
|
+
{
|
|
2580
|
+
orientation: "horizontal",
|
|
2581
|
+
className: "body",
|
|
2582
|
+
defaultLayout,
|
|
2583
|
+
onLayoutChanged,
|
|
2584
|
+
children: [
|
|
2585
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Zt, { id: "list", defaultSize: 350, minSize: 200, maxSize: 700, className: "body-pane", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
2586
|
+
SessionList,
|
|
2587
|
+
{
|
|
2588
|
+
sessions: dayItems.past,
|
|
2589
|
+
selectedId,
|
|
2590
|
+
onSelect: selectSession
|
|
2591
|
+
}
|
|
2592
|
+
) }),
|
|
2593
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(tn, { className: "resize-handle resize-handle-v" }),
|
|
2594
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Zt, { id: "detail", minSize: 30, className: "body-pane", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Session, { meta: selected, date: format(anchor, "yyyy-MM-dd"), granularity }) })
|
|
2595
|
+
]
|
|
2596
|
+
}
|
|
2597
|
+
) })
|
|
2598
|
+
]
|
|
2599
|
+
}
|
|
2600
|
+
),
|
|
2601
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(StatusBar, { progress: scanProgress, sessionCount: sessions.length })
|
|
2602
|
+
] });
|
|
2603
|
+
}
|
|
2604
|
+
clearOutdatedLocalStorage();
|
|
2605
|
+
delegate("body", {
|
|
2606
|
+
target: "[title], [data-tippy-content]",
|
|
2607
|
+
delay: [0, 0],
|
|
2608
|
+
content(reference) {
|
|
2609
|
+
const title = reference.getAttribute("title");
|
|
2610
|
+
reference.removeAttribute("title");
|
|
2611
|
+
return title;
|
|
2612
|
+
},
|
|
2613
|
+
onShow(instance) {
|
|
2614
|
+
const content = instance.reference.getAttribute("data-tippy-content");
|
|
2615
|
+
if (content != null) instance.setContent(content);
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
delegate("body", {
|
|
2619
|
+
target: "[data-tippy-html]",
|
|
2620
|
+
delay: [0, 0],
|
|
2621
|
+
allowHTML: true,
|
|
2622
|
+
content: (reference) => reference.getAttribute("data-tippy-html"),
|
|
2623
|
+
onShow(instance) {
|
|
2624
|
+
instance.setContent(instance.reference.getAttribute("data-tippy-html"));
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
clientExports.createRoot(document.getElementById("root")).render(
|
|
2628
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(React.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(App, {}) })
|
|
2629
|
+
);
|