dsh-plugin-token-heatmap 0.1.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/lib/client.js ADDED
@@ -0,0 +1,900 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-plugin-token-heatmap",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name2 in all)
14
+ __defProp(target, name2, { get: all[name2], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // src/client/index.tsx
27
+ var index_exports = {};
28
+ __export(index_exports, {
29
+ apply: () => apply,
30
+ inject: () => inject,
31
+ name: () => name
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/client/TokenHeatmapOverlay.tsx
36
+ var import_react3 = require("react");
37
+
38
+ // src/day.ts
39
+ var DAY_MS = 864e5;
40
+ function dayKeyOf(time) {
41
+ const d = new Date(time);
42
+ const m = String(d.getMonth() + 1).padStart(2, "0");
43
+ const day = String(d.getDate()).padStart(2, "0");
44
+ return d.getFullYear() + "-" + m + "-" + day;
45
+ }
46
+ function parseDayKey(key) {
47
+ const parts = key.split("-").map(Number);
48
+ return new Date(parts[0], parts[1] - 1, parts[2]).getTime();
49
+ }
50
+ function isoWeekday(time) {
51
+ const wd = new Date(time).getDay();
52
+ return wd === 0 ? 7 : wd;
53
+ }
54
+ function cutoffKey(now) {
55
+ const d = new Date(now);
56
+ return dayKeyOf(new Date(d.getFullYear(), d.getMonth() - 12, 1).getTime());
57
+ }
58
+
59
+ // src/client/palette.ts
60
+ var LEVEL_COLORS = [
61
+ "var(--dsw-alias-bg-skeleton)",
62
+ "#C9DDF9",
63
+ "#8FB9F0",
64
+ "#5E8FE8",
65
+ "#396BE2"
66
+ ];
67
+ var GLYPH_COLORS = [
68
+ "var(--dsw-alias-label-primary)",
69
+ // L0 骨架底色随主题反转 → 字形跟主题文字色
70
+ "#25407A",
71
+ // L1 浅蓝 → 深蓝灰
72
+ "#25407A",
73
+ // L2 → 深蓝灰(白色对比不足)
74
+ "rgba(255,255,255,0.95)",
75
+ // L3
76
+ "rgba(255,255,255,0.95)"
77
+ // L4
78
+ ];
79
+ function glyphColorFor(level) {
80
+ return GLYPH_COLORS[level];
81
+ }
82
+ function levelsFor(values) {
83
+ const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
84
+ const n = nonZero.length;
85
+ if (n === 0) return () => 0;
86
+ const at = (p) => nonZero[Math.min(n - 1, Math.floor(p * (n - 1)))];
87
+ const q25 = at(0.25);
88
+ const q50 = at(0.5);
89
+ const q75 = at(0.75);
90
+ if (q25 === q75) return (value) => value > 0 ? 3 : 0;
91
+ return (value) => {
92
+ if (value <= 0) return 0;
93
+ if (value >= q75) return 4;
94
+ if (value >= q50) return 3;
95
+ if (value >= q25) return 2;
96
+ return 1;
97
+ };
98
+ }
99
+
100
+ // src/client/HeatmapGrid.tsx
101
+ var import_react = require("react");
102
+
103
+ // src/client/grid.ts
104
+ function buildWeeks(endKey, count = 53) {
105
+ const end = parseDayKey(endKey);
106
+ const endMonday = end - (isoWeekday(end) - 1) * DAY_MS;
107
+ const weeks = [];
108
+ for (let w = 0; w < count; w += 1) {
109
+ const monday = endMonday - w * 7 * DAY_MS;
110
+ const week = [];
111
+ for (let d = 0; d < 7; d += 1) {
112
+ const t = monday + d * DAY_MS;
113
+ week.push({
114
+ key: dayKeyOf(t),
115
+ weekday: d + 1,
116
+ month: new Date(t).getMonth(),
117
+ future: t > end
118
+ });
119
+ }
120
+ weeks.push(week);
121
+ }
122
+ return weeks;
123
+ }
124
+ function monthLabels(weeks) {
125
+ let previous = null;
126
+ return weeks.map((week) => {
127
+ const first = week.find((c) => !c.future);
128
+ if (first === void 0) return null;
129
+ if (previous === first.month) return null;
130
+ previous = first.month;
131
+ return first.month + 1 + "\u6708";
132
+ });
133
+ }
134
+ function sumRange(days, fromKey, toKey) {
135
+ let sum = 0;
136
+ for (const [key, value] of Object.entries(days)) {
137
+ if (key >= fromKey && key <= toKey) sum += value.total;
138
+ }
139
+ return sum;
140
+ }
141
+ function formatTokens(n) {
142
+ if (n >= 1e6) return Math.floor(n / 1e6) + "M";
143
+ if (n >= 1e3) return Math.floor(n / 1e3) + "K";
144
+ return String(Math.floor(n));
145
+ }
146
+
147
+ // src/client/HeatmapGrid.tsx
148
+ var import_jsx_runtime = require("react/jsx-runtime");
149
+ var CELL = 16;
150
+ var GAP = 4;
151
+ var STEP = CELL + GAP;
152
+ var WEEKDAYS = ["\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u65E5"];
153
+ var FADE_PX = 10;
154
+ var style = {
155
+ root: {
156
+ display: "flex",
157
+ flexDirection: "column",
158
+ gap: 8,
159
+ fontSize: 12
160
+ },
161
+ // 网格块拉伸到窗口内容宽(minWidth 保证月份标签有地儿放),格子组在其内精确居中:
162
+ // 月份标签绝对定位在行左缘,脱离流,不再把格子整体推右。
163
+ gridBlock: {
164
+ display: "flex",
165
+ flexDirection: "column",
166
+ gap: GAP,
167
+ alignSelf: "stretch",
168
+ minWidth: 208,
169
+ position: "relative"
170
+ },
171
+ scroll: {
172
+ overflowY: "auto",
173
+ display: "flex",
174
+ flexDirection: "column",
175
+ gap: GAP,
176
+ // 行与行之间的固定间距(此前块级堆叠导致行间距为 0)
177
+ // 内边距给悬停放大留出空间,避免首末行/左右列的放大被 scrollport 裁剪;
178
+ // maxHeight 相应加上下 padding,可见行数仍为 5。
179
+ padding: 4,
180
+ maxHeight: 5 * STEP - GAP + 8,
181
+ scrollbarWidth: "none",
182
+ msOverflowStyle: "none"
183
+ },
184
+ row: { display: "flex", alignItems: "center", gap: GAP, height: CELL, justifyContent: "center", position: "relative" },
185
+ label: { width: 30, flex: "0 0 30px", fontSize: 10, color: "var(--dsw-alias-label-tertiary)", textAlign: "right", paddingRight: 4 },
186
+ cell: { width: CELL, height: CELL, borderRadius: 3, transformOrigin: "center" },
187
+ // 与行同规则:星期标注居中于格子列上方(隐藏占位标签绝对定位,不参与居中计算)
188
+ header: { display: "flex", alignItems: "center", gap: GAP, justifyContent: "center", position: "relative" },
189
+ footer: { display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 12 },
190
+ // 两行两列网格:无中点分隔符,列轨道跨行共享(上下对齐);
191
+ // 窗口宽度稳定性由 gridBlock minWidth 208 兑底(footer 自然宽 ≤ 208,不随数字位数变化)。
192
+ summary: { display: "grid", gridTemplateColumns: "max-content max-content", columnGap: 12, rowGap: 4, fontSize: 11, color: "var(--dsw-alias-label-secondary)" },
193
+ legendBlock: { display: "flex", flexDirection: "column", gap: 3 },
194
+ swatches: { display: "flex", gap: 3 },
195
+ legendLabels: { display: "flex", justifyContent: "space-between", width: 62, fontSize: 10, color: "var(--dsw-alias-label-tertiary)" },
196
+ chip: {
197
+ position: "absolute",
198
+ right: 0,
199
+ bottom: 6,
200
+ border: "1px solid var(--dsw-alias-border-l)",
201
+ background: "var(--dsw-alias-bg-overlay)",
202
+ color: "var(--dsw-alias-label-secondary)",
203
+ borderRadius: 999,
204
+ padding: "3px 10px",
205
+ fontSize: 10,
206
+ cursor: "pointer",
207
+ boxShadow: "0 2px 8px rgba(0,0,0,0.12)"
208
+ }
209
+ };
210
+ function maskOf(topFade, bottomFade) {
211
+ const head = topFade ? `transparent 0, black ${FADE_PX}px` : `black 0`;
212
+ const tail = bottomFade ? `black calc(100% - ${FADE_PX}px), transparent 100%` : `black 100%`;
213
+ return `linear-gradient(to bottom, ${head}, black, ${tail})`;
214
+ }
215
+ function HeatmapGrid({ days, endKey }) {
216
+ const end = endKey ?? dayKeyOf(Date.now());
217
+ const weeks = (0, import_react.useMemo)(() => buildWeeks(end), [end]);
218
+ const levelOf = (0, import_react.useMemo)(() => levelsFor(Object.values(days).map((d) => d.total)), [days]);
219
+ const labels = (0, import_react.useMemo)(() => monthLabels(weeks), [weeks]);
220
+ const scrollRef = (0, import_react.useRef)(null);
221
+ const [edgeMask, setEdgeMask] = (0, import_react.useState)({ top: false, bottom: true });
222
+ const [hover, setHover] = (0, import_react.useState)(null);
223
+ const updateMask = (0, import_react.useCallback)(() => {
224
+ const el = scrollRef.current;
225
+ if (!el) return;
226
+ setEdgeMask({
227
+ top: el.scrollTop > 0,
228
+ bottom: el.scrollTop + el.clientHeight < el.scrollHeight - 1
229
+ });
230
+ }, []);
231
+ (0, import_react.useEffect)(() => {
232
+ updateMask();
233
+ const el = scrollRef.current;
234
+ el?.addEventListener("scroll", updateMask, { passive: true });
235
+ return () => el?.removeEventListener("scroll", updateMask);
236
+ }, [updateMask]);
237
+ const backToLatest = (0, import_react.useCallback)(() => {
238
+ const el = scrollRef.current;
239
+ if (!el) return;
240
+ if (typeof el.scrollTo === "function") el.scrollTo({ top: 0, behavior: "smooth" });
241
+ else el.scrollTop = 0;
242
+ }, []);
243
+ const endTime = parseDayKey(end);
244
+ const todayKey = dayKeyOf(endTime);
245
+ const weekMonday = endTime - (isoWeekday(endTime) - 1) * DAY_MS;
246
+ const monthFirst = dayKeyOf(new Date(endTime).setDate(1));
247
+ const cutoff = cutoffKey(endTime);
248
+ const totals = {
249
+ today: sumRange(days, todayKey, todayKey),
250
+ week: sumRange(days, dayKeyOf(weekMonday), todayKey),
251
+ month: sumRange(days, monthFirst, todayKey),
252
+ year: sumRange(days, cutoff, todayKey)
253
+ };
254
+ const maskImage = maskOf(edgeMask.top, edgeMask.bottom);
255
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.root, children: [
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
257
+ [data-th-scroll]::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }
258
+ [data-th-cell] { transition: transform 120ms ease-out; transform-origin: center; }
259
+ [data-th-cell]:hover { transform: scale(1.3); }
260
+ ` }),
261
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.gridBlock, children: [
262
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.header, children: [
263
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...style.label, visibility: "hidden", position: "absolute", left: 0 }, children: "x" }),
264
+ WEEKDAYS.map((w) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: CELL, textAlign: "center", fontSize: 10, color: "var(--dsw-alias-label-tertiary)" }, children: w }, w))
265
+ ] }),
266
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
267
+ "div",
268
+ {
269
+ ref: scrollRef,
270
+ "data-th-scroll": true,
271
+ style: { ...style.scroll, maskImage, WebkitMaskImage: maskImage },
272
+ children: weeks.map((week, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-testid": "week-row", style: style.row, children: [
273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...style.label, position: "absolute", left: 0 }, children: labels[i] ?? "" }),
274
+ week.map((cell) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DayCell, { cell, days, levelOf, onHover: setHover }, cell.key))
275
+ ] }, week[0].key))
276
+ }
277
+ ),
278
+ edgeMask.top && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "data-testid": "back-to-latest", style: style.chip, onClick: backToLatest, children: "\u56DE\u5230\u6700\u65B0" })
279
+ ] }),
280
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.footer, children: [
281
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-testid": "summary", style: style.summary, children: [
282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
283
+ "\u4ECA\u65E5 ",
284
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "sum-today", style: { color: "var(--dsw-alias-label-primary)", fontWeight: 600 }, children: formatTokens(totals.today) })
285
+ ] }),
286
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
287
+ "\u672C\u5468 ",
288
+ formatTokens(totals.week)
289
+ ] }),
290
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
291
+ "\u672C\u6708 ",
292
+ formatTokens(totals.month)
293
+ ] }),
294
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
295
+ "12\u4E2A\u6708 ",
296
+ formatTokens(totals.year)
297
+ ] })
298
+ ] }),
299
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-testid": "legend", style: style.legendBlock, children: [
300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: style.swatches, children: LEVEL_COLORS.map((color) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { width: 10, height: 10, borderRadius: 2, background: color, display: "inline-block" } }, color)) }),
301
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.legendLabels, children: [
302
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u5C11" }),
303
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u591A" })
304
+ ] })
305
+ ] })
306
+ ] }),
307
+ hover !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
308
+ "div",
309
+ {
310
+ "data-testid": "cell-tooltip",
311
+ style: {
312
+ position: "fixed",
313
+ left: Math.min(hover.x + 12, Math.max(8, window.innerWidth - 220)),
314
+ top: Math.min(Math.max(8, hover.y - 66), Math.max(8, window.innerHeight - 80)),
315
+ zIndex: 9999,
316
+ pointerEvents: "none",
317
+ background: "var(--dsw-alias-bg-overlay)",
318
+ border: "1px solid var(--dsw-alias-border-l)",
319
+ borderRadius: 8,
320
+ padding: "6px 8px",
321
+ fontSize: 11,
322
+ color: "var(--dsw-alias-label-primary)",
323
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
324
+ whiteSpace: "nowrap"
325
+ },
326
+ children: [
327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600 }, children: hover.key }),
328
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
329
+ formatTokens(hover.total),
330
+ " tokens\uFF08\u8F93\u5165 ",
331
+ formatTokens(hover.input),
332
+ " / \u8F93\u51FA ",
333
+ formatTokens(hover.output),
334
+ " / \u7F13\u5B58 ",
335
+ formatTokens(hover.cache),
336
+ "\uFF09"
337
+ ] })
338
+ ]
339
+ }
340
+ )
341
+ ] });
342
+ }
343
+ function DayCell({
344
+ cell,
345
+ days,
346
+ levelOf,
347
+ onHover
348
+ }) {
349
+ const entry = days[cell.key];
350
+ const total = entry?.total ?? 0;
351
+ const level = cell.future ? 0 : levelOf(total);
352
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
353
+ "div",
354
+ {
355
+ "data-testid": "day-cell",
356
+ "data-day": cell.key,
357
+ "data-level": level,
358
+ "data-th-cell": true,
359
+ onMouseEnter: (e) => {
360
+ if (entry !== void 0 && !cell.future) {
361
+ onHover({
362
+ key: cell.key,
363
+ total: entry.total,
364
+ input: entry.input,
365
+ output: entry.output,
366
+ cache: entry.cacheRead + entry.cacheWrite,
367
+ x: e.clientX,
368
+ y: e.clientY
369
+ });
370
+ }
371
+ },
372
+ onMouseLeave: () => onHover(null),
373
+ style: { ...style.cell, background: LEVEL_COLORS[level], opacity: cell.future ? 0.25 : 1 }
374
+ }
375
+ );
376
+ }
377
+
378
+ // src/client/ToggleIcon.tsx
379
+ var import_react2 = require("react");
380
+ var import_jsx_runtime2 = require("react/jsx-runtime");
381
+ var ICON_SIZE = 40;
382
+ var ICON_MARGIN = 28;
383
+ var DRAG_THRESHOLD = 5;
384
+ var baseIconStyle = {
385
+ position: "fixed",
386
+ width: ICON_SIZE,
387
+ height: ICON_SIZE,
388
+ borderRadius: 10,
389
+ border: "none",
390
+ cursor: "grab",
391
+ touchAction: "none",
392
+ userSelect: "none",
393
+ display: "flex",
394
+ alignItems: "center",
395
+ justifyContent: "center",
396
+ boxShadow: "0 4px 12px rgba(0,0,0,0.18)",
397
+ zIndex: 9990
398
+ };
399
+ function ToggleIcon({
400
+ level,
401
+ todayTokens,
402
+ onClick,
403
+ position = null,
404
+ onPositionChange
405
+ }) {
406
+ const drag = (0, import_react2.useRef)(null);
407
+ const lastPos = (0, import_react2.useRef)(null);
408
+ const movedRef = (0, import_react2.useRef)(false);
409
+ const cornerPosition = () => ({
410
+ x: window.innerWidth - ICON_MARGIN - ICON_SIZE,
411
+ y: window.innerHeight - ICON_MARGIN - ICON_SIZE
412
+ });
413
+ const handlePointerDown = (event) => {
414
+ const base = position ?? cornerPosition();
415
+ drag.current = { startX: event.clientX, startY: event.clientY, baseX: base.x, baseY: base.y, moved: false };
416
+ movedRef.current = false;
417
+ const onMove = (e) => {
418
+ const d = drag.current;
419
+ if (!d) return;
420
+ const dx = e.clientX - d.startX;
421
+ const dy = e.clientY - d.startY;
422
+ if (!d.moved && Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return;
423
+ d.moved = true;
424
+ movedRef.current = true;
425
+ const pos = {
426
+ x: Math.min(Math.max(0, d.baseX + dx), window.innerWidth - ICON_SIZE),
427
+ y: Math.min(Math.max(0, d.baseY + dy), window.innerHeight - ICON_SIZE)
428
+ };
429
+ lastPos.current = pos;
430
+ onPositionChange?.(pos, false);
431
+ };
432
+ const onUp = () => {
433
+ window.removeEventListener("pointermove", onMove);
434
+ window.removeEventListener("pointerup", onUp);
435
+ const d = drag.current;
436
+ drag.current = null;
437
+ if (d?.moved && onPositionChange) {
438
+ const end = lastPos.current ?? cornerPosition();
439
+ onPositionChange(end, true);
440
+ }
441
+ };
442
+ window.addEventListener("pointermove", onMove);
443
+ window.addEventListener("pointerup", onUp);
444
+ };
445
+ const handleClick = () => {
446
+ if (movedRef.current) {
447
+ movedRef.current = false;
448
+ return;
449
+ }
450
+ onClick();
451
+ };
452
+ const iconStyle = position === null ? { ...baseIconStyle, background: LEVEL_COLORS[level], right: ICON_MARGIN, bottom: ICON_MARGIN } : { ...baseIconStyle, background: LEVEL_COLORS[level], left: position.x, top: position.y };
453
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
454
+ "button",
455
+ {
456
+ "data-testid": "toggle-icon",
457
+ type: "button",
458
+ onClick: handleClick,
459
+ onPointerDown: handlePointerDown,
460
+ title: "\u4ECA\u65E5 token\uFF1A" + formatTokens(todayTokens),
461
+ style: iconStyle,
462
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": "true", style: { color: glyphColorFor(level) }, children: [
463
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "1", y: "10", width: "4", height: "7", rx: "1", fill: "currentColor" }),
464
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "7", y: "5", width: "4", height: "12", rx: "1", fill: "currentColor" }),
465
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "13", y: "1", width: "4", height: "16", rx: "1", fill: "currentColor" })
466
+ ] })
467
+ }
468
+ );
469
+ }
470
+
471
+ // src/client/TokenHeatmapOverlay.tsx
472
+ var import_jsx_runtime3 = require("react/jsx-runtime");
473
+ var LS_VISIBLE = "dsh.tokenHeatmap.visible";
474
+ var LS_POSITION = "dsh.tokenHeatmap.position";
475
+ var LS_VIEW = "dsh.tokenHeatmap.view";
476
+ var POLL_MS = 3e3;
477
+ var RETRY_MIN_MS = 300;
478
+ var RETRY_MAX_MS = 1e4;
479
+ var WINDOW_W = 232;
480
+ var WINDOW_H = 220;
481
+ var WINDOW_ICON_GAP = 16;
482
+ var LS_ICON_POSITION = "dsh.tokenHeatmap.iconPosition";
483
+ function readVisible() {
484
+ try {
485
+ return localStorage.getItem(LS_VISIBLE) === "1";
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+ function readPosition() {
491
+ try {
492
+ const raw = localStorage.getItem(LS_POSITION);
493
+ if (raw === null) return null;
494
+ const parsed = JSON.parse(raw);
495
+ if (typeof parsed.x === "number" && typeof parsed.y === "number") return parsed;
496
+ } catch {
497
+ }
498
+ return null;
499
+ }
500
+ function readIconPosition() {
501
+ try {
502
+ const raw = localStorage.getItem(LS_ICON_POSITION);
503
+ if (raw === null) return null;
504
+ const parsed = JSON.parse(raw);
505
+ if (typeof parsed.x === "number" && typeof parsed.y === "number") return { x: parsed.x, y: parsed.y };
506
+ } catch {
507
+ }
508
+ return null;
509
+ }
510
+ function readView() {
511
+ try {
512
+ return localStorage.getItem(LS_VIEW) === "session" ? "session" : "global";
513
+ } catch {
514
+ return "global";
515
+ }
516
+ }
517
+ var headerStyle = {
518
+ display: "flex",
519
+ alignItems: "center",
520
+ justifyContent: "space-between",
521
+ gap: 6,
522
+ cursor: "grab",
523
+ userSelect: "none",
524
+ fontSize: 13,
525
+ fontWeight: 600
526
+ };
527
+ var windowStyle = {
528
+ position: "fixed",
529
+ width: "max-content",
530
+ zIndex: 9990,
531
+ display: "flex",
532
+ flexDirection: "column",
533
+ gap: 8,
534
+ padding: 12,
535
+ background: "var(--dsw-alias-bg-overlay)",
536
+ border: "1px solid var(--dsw-alias-border-l)",
537
+ borderRadius: 12,
538
+ boxShadow: "0 8px 24px rgba(0,0,0,0.16)",
539
+ color: "var(--dsw-alias-label-primary)",
540
+ fontSize: 12
541
+ };
542
+ var windowShownStyle = {
543
+ opacity: 1,
544
+ transform: "none",
545
+ visibility: "visible",
546
+ pointerEvents: "auto",
547
+ transition: "opacity 160ms ease, transform 160ms ease, visibility 0s"
548
+ };
549
+ var windowHiddenStyle = {
550
+ opacity: 0,
551
+ transform: "translateY(6px) scale(0.96)",
552
+ visibility: "hidden",
553
+ pointerEvents: "none",
554
+ transition: "opacity 160ms ease, transform 160ms ease, visibility 0s linear 160ms"
555
+ };
556
+ var segmentedStyle = {
557
+ display: "inline-flex",
558
+ alignItems: "center",
559
+ gap: 2,
560
+ border: "1px solid var(--dsw-alias-border-l)",
561
+ borderRadius: 8,
562
+ padding: 2
563
+ };
564
+ function segButtonStyle(active) {
565
+ return {
566
+ border: "none",
567
+ background: active ? "var(--dsw-alias-bg-skeleton)" : "transparent",
568
+ color: active ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary)",
569
+ fontWeight: active ? 700 : 400,
570
+ borderRadius: 6,
571
+ padding: "2px 8px",
572
+ fontSize: 11,
573
+ cursor: "pointer"
574
+ };
575
+ }
576
+ var retryButtonStyle = {
577
+ border: "1px solid var(--dsw-alias-border-l)",
578
+ background: "transparent",
579
+ color: "var(--dsw-alias-label-secondary)",
580
+ borderRadius: 999,
581
+ padding: "2px 10px",
582
+ fontSize: 11,
583
+ cursor: "pointer"
584
+ };
585
+ var stateStyle = {
586
+ display: "flex",
587
+ alignItems: "center",
588
+ gap: 8,
589
+ padding: "4px 2px",
590
+ fontSize: 12,
591
+ color: "var(--dsw-alias-label-secondary)"
592
+ };
593
+ function TokenHeatmapOverlay({ api, sessions }) {
594
+ const [visible, setVisible] = (0, import_react3.useState)(readVisible);
595
+ const [view, setView] = (0, import_react3.useState)(readView);
596
+ const [position, setPosition] = (0, import_react3.useState)(readPosition);
597
+ const [iconPos, setIconPos] = (0, import_react3.useState)(readIconPosition);
598
+ const [data, setData] = (0, import_react3.useState)({ version: -1, days: {} });
599
+ const [backfill, setBackfill] = (0, import_react3.useState)(null);
600
+ const [error, setError] = (0, import_react3.useState)(false);
601
+ const [loading, setLoading] = (0, import_react3.useState)(true);
602
+ const dataKeyRef = (0, import_react3.useRef)("");
603
+ const [currentId, setCurrentId] = (0, import_react3.useState)(() => sessions.list.getSnapshot().current);
604
+ const positionRef = (0, import_react3.useRef)(position);
605
+ positionRef.current = position;
606
+ const windowRef = (0, import_react3.useRef)(null);
607
+ const [windowWidth, setWindowWidth] = (0, import_react3.useState)(null);
608
+ (0, import_react3.useLayoutEffect)(() => {
609
+ const el = windowRef.current;
610
+ if (!el) return;
611
+ const update = () => {
612
+ const w = el.offsetWidth;
613
+ if (w > 0) setWindowWidth(w);
614
+ };
615
+ update();
616
+ if (typeof ResizeObserver === "undefined") return;
617
+ const ro = new ResizeObserver(update);
618
+ ro.observe(el);
619
+ return () => ro.disconnect();
620
+ }, []);
621
+ (0, import_react3.useEffect)(() => {
622
+ return sessions.list.subscribe(() => setCurrentId(sessions.list.getSnapshot().current));
623
+ }, [sessions]);
624
+ (0, import_react3.useEffect)(() => {
625
+ if (!visible) return;
626
+ let cancelled = false;
627
+ let timer = null;
628
+ let delay = 0;
629
+ const schedule = () => {
630
+ timer = setTimeout(run, delay);
631
+ };
632
+ const run = async () => {
633
+ if (cancelled) return;
634
+ try {
635
+ const [payload, status] = await Promise.all([
636
+ view === "global" ? api.getGlobalUsage() : currentId !== void 0 ? api.getSessionUsage(currentId) : Promise.resolve({ version: -1, days: {} }),
637
+ api.getBackfillStatus()
638
+ ]);
639
+ if (cancelled) return;
640
+ const key = view + ":" + payload.version;
641
+ if (dataKeyRef.current !== key) {
642
+ dataKeyRef.current = key;
643
+ setData(payload);
644
+ }
645
+ setBackfill(status);
646
+ setError(false);
647
+ setLoading(false);
648
+ delay = POLL_MS;
649
+ } catch (err) {
650
+ if (cancelled) return;
651
+ const message = err instanceof Error ? err.message : String(err);
652
+ console.warn("[token-heatmap] poll failed:", message);
653
+ setError(true);
654
+ setLoading(false);
655
+ delay = Math.min(Math.max(delay * 2, RETRY_MIN_MS), RETRY_MAX_MS);
656
+ }
657
+ if (!cancelled) schedule();
658
+ };
659
+ schedule();
660
+ return () => {
661
+ cancelled = true;
662
+ if (timer !== null) clearTimeout(timer);
663
+ };
664
+ }, [visible, view, currentId, api]);
665
+ const toggle = (0, import_react3.useCallback)(() => {
666
+ setVisible((v) => {
667
+ const next = !v;
668
+ try {
669
+ localStorage.setItem(LS_VISIBLE, next ? "1" : "0");
670
+ } catch {
671
+ }
672
+ return next;
673
+ });
674
+ }, []);
675
+ const switchView = (0, import_react3.useCallback)((next) => {
676
+ setView(next);
677
+ try {
678
+ localStorage.setItem(LS_VIEW, next);
679
+ } catch {
680
+ }
681
+ }, []);
682
+ const defaultPosition = (0, import_react3.useCallback)(() => {
683
+ const width = windowWidth ?? WINDOW_W;
684
+ const iconX = iconPos?.x ?? window.innerWidth - ICON_MARGIN - ICON_SIZE;
685
+ const iconY = iconPos?.y ?? window.innerHeight - ICON_MARGIN - ICON_SIZE;
686
+ const x = Math.min(Math.max(8, iconX + (ICON_SIZE - width) / 2), Math.max(8, window.innerWidth - width - 8));
687
+ const above = iconY - WINDOW_H - WINDOW_ICON_GAP;
688
+ const y = above >= 8 ? above : Math.min(Math.max(8, iconY + ICON_SIZE + WINDOW_ICON_GAP), Math.max(8, window.innerHeight - WINDOW_H - 8));
689
+ return { x, y };
690
+ }, [iconPos, windowWidth]);
691
+ const onPointerDown = (0, import_react3.useCallback)((event) => {
692
+ const start = positionRef.current ?? defaultPosition();
693
+ const origin = { x: event.clientX, y: event.clientY };
694
+ const onMove = (e) => {
695
+ const x = Math.min(Math.max(0, start.x + e.clientX - origin.x), window.innerWidth - 60);
696
+ const y = Math.min(Math.max(0, start.y + e.clientY - origin.y), window.innerHeight - 60);
697
+ setPosition({ x, y });
698
+ };
699
+ const onUp = () => {
700
+ window.removeEventListener("pointermove", onMove);
701
+ window.removeEventListener("pointerup", onUp);
702
+ try {
703
+ localStorage.setItem(LS_POSITION, JSON.stringify(positionRef.current));
704
+ } catch {
705
+ }
706
+ };
707
+ window.addEventListener("pointermove", onMove);
708
+ window.addEventListener("pointerup", onUp);
709
+ }, [defaultPosition]);
710
+ const days = data.days;
711
+ const todayKey = dayKeyOf(Date.now());
712
+ const todayTotal = days[todayKey]?.total ?? 0;
713
+ const levelOf = (0, import_react3.useMemo)(() => levelsFor(Object.values(days).map((d) => d.total)), [days]);
714
+ const winPos = position ?? defaultPosition();
715
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
716
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `@media (prefers-reduced-motion: reduce) { [data-th-window] { transition: none !important; } }` }),
717
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
718
+ ToggleIcon,
719
+ {
720
+ level: levelOf(todayTotal),
721
+ todayTokens: todayTotal,
722
+ onClick: toggle,
723
+ position: iconPos,
724
+ onPositionChange: (pos, committed) => {
725
+ setIconPos(pos);
726
+ if (committed) {
727
+ try {
728
+ localStorage.setItem(LS_ICON_POSITION, JSON.stringify(pos));
729
+ } catch {
730
+ }
731
+ setPosition(null);
732
+ }
733
+ }
734
+ }
735
+ ),
736
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
737
+ "div",
738
+ {
739
+ ref: windowRef,
740
+ "data-th-window": true,
741
+ style: { ...windowStyle, left: winPos.x, top: winPos.y, ...visible ? windowShownStyle : windowHiddenStyle },
742
+ children: [
743
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: headerStyle, onPointerDown, children: [
744
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Token \u70ED\u529B\u56FE" }),
745
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: segmentedStyle, children: [
746
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", style: segButtonStyle(view === "global"), "aria-pressed": view === "global", onClick: () => switchView("global"), children: "\u5168\u5C40" }),
747
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", style: segButtonStyle(view === "session"), "aria-pressed": view === "session", onClick: () => switchView("session"), children: "\u4F1A\u8BDD" })
748
+ ] })
749
+ ] }),
750
+ error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: stateStyle, children: [
751
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "\u6570\u636E\u52A0\u8F7D\u5931\u8D25" }),
752
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", style: retryButtonStyle, onClick: () => {
753
+ setError(false);
754
+ setLoading(true);
755
+ }, children: "\u91CD\u8BD5" })
756
+ ] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: stateStyle, children: "\u6570\u636E\u52A0\u8F7D\u4E2D\u2026" }) : view === "session" && currentId === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: stateStyle, children: "\u5F53\u524D\u6CA1\u6709\u6253\u5F00\u7684\u4F1A\u8BDD" }) : Object.keys(days).length === 0 && backfill !== null && !backfill.done ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: stateStyle, children: "\u6B63\u5728\u56DE\u586B\u5386\u53F2\u6570\u636E\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HeatmapGrid, { days }),
757
+ backfill !== null && backfill.done && backfill.skipped > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)" }, children: [
758
+ "\u56DE\u586B\u5B8C\u6210\uFF0C\u8DF3\u8FC7 ",
759
+ backfill.skipped,
760
+ " \u6761\u8BB0\u5F55"
761
+ ] })
762
+ ]
763
+ }
764
+ )
765
+ ] });
766
+ }
767
+
768
+ // src/client/remote.ts
769
+ function createTokenHeatmapRemote(ns) {
770
+ const call = async (pending, method) => {
771
+ const result = await pending;
772
+ if (!result.ok) {
773
+ throw new Error(`tokenHeatmap.${method} failed: ${result.error.code}: ${result.error.message}`);
774
+ }
775
+ return result.value;
776
+ };
777
+ return {
778
+ getGlobalUsage: () => call(ns.getGlobalUsage(), "getGlobalUsage"),
779
+ getSessionUsage: (sessionId) => call(ns.getSessionUsage(sessionId), "getSessionUsage"),
780
+ getBackfillStatus: () => call(ns.getBackfillStatus(), "getBackfillStatus")
781
+ };
782
+ }
783
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
784
+ var daysSchema = {
785
+ parse(value) {
786
+ if (!isRecord(value)) throw new Error("usage days must be an object");
787
+ const out = {};
788
+ for (const [key, entry] of Object.entries(value)) {
789
+ if (!isRecord(entry)) throw new Error(`usage day ${key} must be an object`);
790
+ const num = (field) => {
791
+ const n = entry[field];
792
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 0) {
793
+ throw new Error(`usage day ${key}.${field} invalid`);
794
+ }
795
+ return n;
796
+ };
797
+ out[key] = {
798
+ total: num("total"),
799
+ input: num("input"),
800
+ output: num("output"),
801
+ cacheRead: num("cacheRead"),
802
+ cacheWrite: num("cacheWrite")
803
+ };
804
+ }
805
+ return out;
806
+ }
807
+ };
808
+ var usageSchema = {
809
+ parse(value) {
810
+ if (!isRecord(value)) throw new Error("usage payload must be an object");
811
+ if (typeof value.version !== "number") throw new Error("usage version invalid");
812
+ return { version: value.version, days: daysSchema.parse(value.days) };
813
+ }
814
+ };
815
+ var backfillSchema = {
816
+ parse(value) {
817
+ if (!isRecord(value)) throw new Error("backfill status must be an object");
818
+ const num = (field) => {
819
+ const n = value[field];
820
+ return typeof n === "number" ? n : 0;
821
+ };
822
+ return {
823
+ done: value.done === true,
824
+ scanned: num("scanned"),
825
+ skipped: num("skipped"),
826
+ startedAt: typeof value.startedAt === "number" ? value.startedAt : null,
827
+ finishedAt: typeof value.finishedAt === "number" ? value.finishedAt : null
828
+ };
829
+ }
830
+ };
831
+ var jsonSchema = {
832
+ parse(value) {
833
+ if (typeof value !== "string") throw new Error("expected string");
834
+ return value;
835
+ }
836
+ };
837
+ function descriptor(method, parameters, result) {
838
+ return {
839
+ id: `tokenHeatmap/${method}`,
840
+ service: "tokenHeatmap",
841
+ namespace: "tokenHeatmap",
842
+ method,
843
+ invocation: { kind: "direct" },
844
+ parameters: parameters.map((p) => ({
845
+ name: p.name,
846
+ wire: p.wire,
847
+ source: "json",
848
+ codec: { mode: "strict", typeSymbol: "string", schema: jsonSchema }
849
+ })),
850
+ result: { mode: "strict", typeSymbol: "tokenHeatmap", schema: result }
851
+ };
852
+ }
853
+ function mountTokenHeatmapRemote(ctx, remote) {
854
+ const contribution = {
855
+ package: "dsh-plugin-token-heatmap",
856
+ descriptors: [
857
+ descriptor("getGlobalUsage", [], usageSchema),
858
+ descriptor("getSessionUsage", [{ name: "sessionId", wire: "sessionId" }], usageSchema),
859
+ descriptor("getBackfillStatus", [], backfillSchema)
860
+ ]
861
+ };
862
+ let dispose = () => {
863
+ };
864
+ void remote.$mount(contribution).then(
865
+ (disposer) => {
866
+ dispose = disposer;
867
+ },
868
+ (error) => {
869
+ console.error("[token-heatmap] remote mount failed:", error);
870
+ }
871
+ );
872
+ ctx.effect(() => () => dispose());
873
+ return () => dispose();
874
+ }
875
+
876
+ // src/client/index.tsx
877
+ var name = "token-heatmap";
878
+ var inject = ["slots", "remote", "sessions"];
879
+ function apply(ctx) {
880
+ ctx.inject(["slots", "remote", "sessions"], (baseCtx) => {
881
+ const slots = baseCtx.get("slots");
882
+ const remote = baseCtx.get("remote");
883
+ const sessions = baseCtx.get("sessions");
884
+ if (slots === void 0 || remote === void 0 || sessions === void 0) return;
885
+ mountTokenHeatmapRemote(baseCtx, remote);
886
+ baseCtx.inject(["remote.tokenHeatmap"], (nsCtx) => {
887
+ const ns = nsCtx.get("remote.tokenHeatmap");
888
+ if (ns === void 0) return;
889
+ const api = createTokenHeatmapRemote(ns);
890
+ slots.inject(
891
+ "shell.overlay",
892
+ () => slots.register({ name: "shell.overlay", id: "token-heatmap", inject: () => ({ api, sessions }) }, TokenHeatmapOverlay)
893
+ );
894
+ });
895
+ });
896
+ }
897
+
898
+ return module.exports;
899
+ },
900
+ });