dsh-codinput 0.3.6 → 0.3.8

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/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "dsh-codinput",
3
- "version": "0.3.6",
4
- "description": "DSH WebUI 插件:把聊天输入框变成代码编辑器一样的输入风格(CodeMirror 6 接管 conversation.composer.bar)。",
3
+ "version": "0.3.8",
4
+ "description": "DSH 插件:把聊天输入框变成代码编辑器一样的输入风格(CodeMirror 6 接管 conversation.composer.bar),web 与桌面端通用。",
5
5
  "license": "MIT",
6
6
  "author": "Witherwithwinter",
7
+ "icon": "./assets/icon.png",
7
8
  "keywords": [
8
9
  "dsh",
9
10
  "deepseek",
@@ -27,7 +27,7 @@ const FILE_FOLD =
27
27
  'M26.3909 7.37445L19.0525 0V3.77445C19.0525 4.89271 19.0525 5.45184 19.2352 5.89289C19.4788 6.48096 19.946 6.94818 20.5341 7.19176C20.9751 7.37445 21.5342 7.37445 22.6525 7.37445H26.3909Z';
28
28
 
29
29
  /** 官方 FileTypeIcon 的通用文件 glyph(28×28 视图框,fill 自带轮廓)。 */
30
- function FileGlyph({ size = 28 }: { size?: number }): JSX.Element {
30
+ export function FileGlyph({ size = 28 }: { size?: number }): JSX.Element {
31
31
  return (
32
32
  <svg width={size} height={size} viewBox="0 0 28 28" fill="none" aria-hidden="true">
33
33
  <path d={FILE_BODY} fill="currentColor" />
@@ -37,14 +37,14 @@ function FileGlyph({ size = 28 }: { size?: number }): JSX.Element {
37
37
  }
38
38
 
39
39
  /** 官方 fileExtension:取 basename 最后一个点之后的部分。 */
40
- function fileExtension(name: string): string {
40
+ export function fileExtension(name: string): string {
41
41
  const base = name.split(/[\\/]/).pop() ?? name;
42
42
  const dot = base.lastIndexOf('.');
43
43
  return dot < 0 ? '' : base.slice(dot + 1);
44
44
  }
45
45
 
46
46
  /** 官方 fileSizeText 逐值复刻。 */
47
- function fileSizeText(bytes: number): string {
47
+ export function fileSizeText(bytes: number): string {
48
48
  if (bytes < 1024) return `${bytes}B`;
49
49
  const kb = bytes / 1024;
50
50
  if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)}KB`;
@@ -1,9 +1,14 @@
1
1
  /** 数据行(卡片下方):官方 composer dock 复刻——两枚胶囊(仪表盘「轮/步 · tok/s」、
2
2
  * 数据库「累计 tok · 缓存命中」)+ 上下文小圈(0.1.7-rc.2 起官方把它挪进本行,圈+百分比
3
- * 药丸),点击弹出官方同构详情面板(会话统计 / Token 用量 / 上下文分段),锚点上方弹出、
4
- * 点外关闭。 */
3
+ * 药丸),点击弹出官方同构详情面板(会话统计 / Token 用量 / 上下文分段)。面板照官方
4
+ * ContextMeter / useStatDialog 机制:portal 到 body + fixed,经 useAnchoredPanel(官方
5
+ * useAnchoredPosition 插件侧移植)按锚点视口矩形定位(side top / align start、gap 8、
6
+ * 视口 margin 12 夹取,滚动(捕获)/resize/面板尺寸变化重算);每枚面板一个
7
+ * useStatDialogSeat(锚点/面板 ref + 定位 + 点外与 Escape 关闭),三枚共用 StatsRow 的
8
+ * 单一展开态互斥(与官方 chat StatsPills 的 openPill 同语义)。 */
5
9
 
6
- import { useEffect, useRef, useState } from 'react';
10
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
11
+ import { createPortal } from 'react-dom';
7
12
  import { useSessionStats, formatTokens, formatTokensPerSecond, formatCacheHitPercent, formatDuration, type SessionStats } from '../stats';
8
13
  import type { UseProjection } from '../host-types';
9
14
  import { t } from '../i18n';
@@ -37,16 +42,114 @@ export function DatabaseIcon({ size = 14 }: { size?: number }): JSX.Element {
37
42
  );
38
43
  }
39
44
 
40
- /** 官方详情面板(bRhRbq:标题行 + 发丝线 + dt/dd 栅格)。 */
45
+ /** 官方 useAnchoredPosition 的插件侧移植(primitives 不可 import):fixed 面板按锚点
46
+ * 视口矩形定位(side top / align start:面板左缘对齐锚点左缘、底距锚点顶 gap),
47
+ * 两侧按 margin 夹取(顶边用官方 overlayTopMargin:帧顶 clearance+20 与 margin 取大),
48
+ * 滚动(捕获相位)/resize/面板自身尺寸变化重算。返回 null = 尚未量得(面板隐藏)。 */
49
+ function useAnchoredPanel(
50
+ open: boolean,
51
+ anchorRef: React.RefObject<HTMLElement | null>,
52
+ panelRef: React.RefObject<HTMLElement | null>,
53
+ gap: number,
54
+ margin: number,
55
+ ): { left: number; top: number } | null {
56
+ const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
57
+ useLayoutEffect(() => {
58
+ if (!open) {
59
+ setPosition(null);
60
+ return;
61
+ }
62
+ // 官方 overlayTopMargin:帧顶 clearance(data-fullscreen 归零)+20,与 margin 取大。
63
+ const topMargin = (): number => {
64
+ const clearance = Number.parseFloat(
65
+ getComputedStyle(document.documentElement).getPropertyValue('--dsh-frame-top-clearance'),
66
+ );
67
+ if (Number.isNaN(clearance)) return margin;
68
+ return Math.max(margin, (document.documentElement.hasAttribute('data-fullscreen') ? 0 : clearance) + 20);
69
+ };
70
+ // 与官方 place() 同构:rect 存在就落位,宽/高为 0 时只跳过对应轴的夹取。
71
+ const place = (): void => {
72
+ const rect = anchorRef.current?.getBoundingClientRect();
73
+ if (rect === undefined) return;
74
+ const panel = panelRef.current;
75
+ const width = panel?.offsetWidth ?? 0;
76
+ const height = panel?.offsetHeight ?? 0;
77
+ let left = rect.left;
78
+ let top = rect.top - gap - height;
79
+ if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
80
+ if (height > 0) top = Math.min(Math.max(top, topMargin()), window.innerHeight - height - margin);
81
+ setPosition((prev) => (prev && prev.left === left && prev.top === top ? prev : { left, top }));
82
+ };
83
+ place();
84
+ window.addEventListener('scroll', place, true);
85
+ window.addEventListener('resize', place);
86
+ const panel = panelRef.current;
87
+ let observer: ResizeObserver | null = null;
88
+ if (typeof ResizeObserver !== 'undefined' && panel !== null) {
89
+ observer = new ResizeObserver(place);
90
+ observer.observe(panel);
91
+ }
92
+ return () => {
93
+ observer?.disconnect();
94
+ window.removeEventListener('scroll', place, true);
95
+ window.removeEventListener('resize', place);
96
+ };
97
+ }, [open, anchorRef, panelRef, gap, margin]);
98
+ return position;
99
+ }
100
+
101
+ /** 官方 useStatDialog 的插件侧等价 seat(primitives/chat 的 useStatDialog 不可 import):
102
+ * 每枚面板自持锚点/面板 ref + 定位,并在打开期间挂「点外关闭 + Escape 关闭」(官方语义:
103
+ * 锚点或 portal 面板内的 pointerdown 算「内」;Escape 走 document 冒泡)。父级用受控
104
+ * open 维持三枚面板互斥,与官方 chat StatsPills 的 openPill 同构。 */
105
+ interface StatDialogSeat {
106
+ rootRef: React.MutableRefObject<HTMLSpanElement | null>;
107
+ panelRef: React.MutableRefObject<HTMLDivElement | null>;
108
+ position: { left: number; top: number } | null;
109
+ }
110
+
111
+ function useStatDialogSeat(open: boolean, onClose: () => void): StatDialogSeat {
112
+ const rootRef = useRef<HTMLSpanElement | null>(null);
113
+ const panelRef = useRef<HTMLDivElement | null>(null);
114
+ const position = useAnchoredPanel(open, rootRef, panelRef, 8, 12);
115
+ useEffect(() => {
116
+ if (!open) return;
117
+ const onDown = (event: PointerEvent): void => {
118
+ if (!(event.target instanceof Node)) return;
119
+ if (rootRef.current?.contains(event.target) === true) return;
120
+ if (panelRef.current?.contains(event.target) === true) return;
121
+ onClose();
122
+ };
123
+ const onKeyDown = (event: KeyboardEvent): void => {
124
+ if (event.key === 'Escape') onClose();
125
+ };
126
+ document.addEventListener('pointerdown', onDown);
127
+ document.addEventListener('keydown', onKeyDown);
128
+ return () => {
129
+ document.removeEventListener('pointerdown', onDown);
130
+ document.removeEventListener('keydown', onKeyDown);
131
+ };
132
+ }, [open, onClose]);
133
+ return { rootRef, panelRef, position };
134
+ }
135
+
136
+ /** 面板定位样式(未量得前官方同款:隐藏在原点)。 */
137
+ function panelStyle(position: { left: number; top: number } | null): React.CSSProperties {
138
+ return position ?? { visibility: 'hidden', left: 0, top: 0 };
139
+ }
140
+
141
+ /** 官方详情面板(bRhRbq:标题行 + 发丝线 + dt/dd 栅格)。portal 到 body,fixed 定位。 */
41
142
  function StatPanel(props: {
42
143
  label: string;
43
144
  icon: JSX.Element;
44
145
  titleValue?: string;
45
146
  rows: readonly [string, string][];
147
+ panelRef: React.MutableRefObject<HTMLDivElement | null>;
148
+ position: { left: number; top: number } | null;
46
149
  }): JSX.Element {
47
- const { label, icon, titleValue, rows } = props;
48
- return (
49
- <div className="dci-stat-panel" role="dialog" aria-label={label}>
150
+ const { label, icon, titleValue, rows, panelRef, position } = props;
151
+ return createPortal(
152
+ <div className="dci-stat-panel" role="dialog" aria-label={label} ref={panelRef} style={panelStyle(position)}>
50
153
  <div className="dci-stat-panelTitle">
51
154
  <span className="dci-stat-panelLabel">
52
155
  {icon}
@@ -63,7 +166,8 @@ function StatPanel(props: {
63
166
  </div>
64
167
  ))}
65
168
  </dl>
66
- </div>
169
+ </div>,
170
+ document.body,
67
171
  );
68
172
  }
69
173
 
@@ -76,8 +180,9 @@ function exactCount(value: number | undefined): string {
76
180
  }
77
181
 
78
182
  /** 官方 TimePill:轮/步 + · tok/s(decodeMs>0 才出),面板 = 模型/工具用时、TTFT 均值、TPS。 */
79
- function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }): JSX.Element {
80
- const { stats, open, onToggle } = props;
183
+ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void; onClose(): void }): JSX.Element {
184
+ const { stats, open, onToggle, onClose } = props;
185
+ const { rootRef, panelRef, position } = useStatDialogSeat(open, onClose);
81
186
  const roundsPart = t('stats.turns', { turns: stats.turns ?? 0, steps: stats.steps ?? 0 });
82
187
  const tps =
83
188
  (stats.decodeMs ?? 0) > 0 ? formatTokensPerSecond((stats.decodeTokens ?? 0) / ((stats.decodeMs ?? 1) / 1000)) : null;
@@ -97,7 +202,7 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
97
202
  // 官方:无任何计时数据时胶囊不可点(无面板可看)。
98
203
  if (!hasTimings) {
99
204
  return (
100
- <span className="dci-stat-anchor">
205
+ <span className="dci-stat-anchor" ref={rootRef}>
101
206
  <span className="dci-stat-pill">
102
207
  <GaugeIcon />
103
208
  {label}
@@ -106,7 +211,7 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
106
211
  );
107
212
  }
108
213
  return (
109
- <span className="dci-stat-anchor">
214
+ <span className="dci-stat-anchor" ref={rootRef}>
110
215
  <button
111
216
  type="button"
112
217
  className="dci-stat-pill"
@@ -122,6 +227,8 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
122
227
  <StatPanel
123
228
  label={t("stats.session")}
124
229
  icon={<GaugeIcon />}
230
+ panelRef={panelRef}
231
+ position={position}
125
232
  rows={[
126
233
  [t('stats.modelTime'), (stats.llmMs ?? 0) > 0 ? formatDuration(stats.llmMs!) : '—'],
127
234
  [t('stats.toolTime'), (stats.toolMs ?? 0) > 0 ? formatDuration(stats.toolMs!) : '—'],
@@ -140,8 +247,9 @@ function Pill(props: { stats: SessionStats; open: boolean; onToggle(): void }):
140
247
  }
141
248
 
142
249
  /** 官方 UsagePill:总量 · 缓存命中(缓存命中用官方百分比精度规则)。 */
143
- function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void }): JSX.Element | null {
144
- const { stats, open, onToggle } = props;
250
+ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void; onClose(): void }): JSX.Element | null {
251
+ const { stats, open, onToggle, onClose } = props;
252
+ const { rootRef, panelRef, position } = useStatDialogSeat(open, onClose);
145
253
  if (stats.totalTokens === undefined) return null;
146
254
  const total = stats.totalTokens;
147
255
  const cacheHitText =
@@ -151,7 +259,7 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
151
259
  (stats.cacheReadTokens ?? 0) + (stats.cacheWriteTokens ?? 0) + (stats.uncachedInputTokens ?? 0),
152
260
  );
153
261
  return (
154
- <span className="dci-stat-anchor">
262
+ <span className="dci-stat-anchor" ref={rootRef}>
155
263
  <button
156
264
  type="button"
157
265
  className="dci-stat-pill"
@@ -180,6 +288,8 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
180
288
  label={t("stats.usage")}
181
289
  icon={<DatabaseIcon />}
182
290
  titleValue={exactCount(total)}
291
+ panelRef={panelRef}
292
+ position={position}
183
293
  rows={[
184
294
  [t('stats.cacheHitLabel'), cacheHitText !== null ? `${cacheHitText}%` : '—'],
185
295
  [t('stats.uncached'), exactCount(stats.uncachedInputTokens)],
@@ -197,36 +307,42 @@ function TokenPill(props: { stats: SessionStats; open: boolean; onToggle(): void
197
307
 
198
308
  export function StatsRow(props: StatsRowProps): JSX.Element | null {
199
309
  const { useProjection, sessionId, chatNodes } = props;
200
- const [open, setOpen] = useState<'rounds' | 'tokens' | null>(null);
201
- const rootRef = useRef<HTMLDivElement | null>(null);
310
+ // 唯一展开:三枚(轮步/用量/上下文小圈)共用一个 open 态——任意一枚展开
311
+ // 即收起其余(官方 chat StatsPills 的 openPill 同语义:详情面板同时只存在一个)。
312
+ const [open, setOpen] = useState<'rounds' | 'tokens' | 'gauge' | null>(null);
313
+ const close = useCallback(() => setOpen(null), []);
314
+ const toggle = useCallback((key: 'rounds' | 'tokens' | 'gauge') => {
315
+ setOpen((current) => (current === key ? null : key));
316
+ }, []);
202
317
  const stats = useSessionStats(useProjection, sessionId, chatNodes);
203
318
 
204
- // 点外面关闭(面板内/胶囊上不关)。
205
- useEffect(() => {
206
- if (open === null) return;
207
- const onDown = (event: PointerEvent): void => {
208
- if (rootRef.current && event.target instanceof Node && rootRef.current.contains(event.target)) return;
209
- setOpen(null);
210
- };
211
- window.addEventListener('pointerdown', onDown, true);
212
- return () => window.removeEventListener('pointerdown', onDown, true);
213
- }, [open]);
214
-
215
319
  // 官方 rc.2:上下文小圈独立于 stats 胶囊渲染(无 stats 也要出圈)。
216
320
  const pressure = useProjection
217
321
  ? (useProjection('contextPressure') as PressureSnapshot | undefined)
218
322
  : undefined;
219
323
  const hasGauge = pressure !== undefined && !!pressure.contextWindow;
324
+ // 面板打开期间其数据源消失(切换会话 / 投影短暂缺席)时自动收起,避免恢复后「自己重开」。
325
+ useEffect(() => {
326
+ if (open === null) return;
327
+ const valid = open === 'gauge' ? hasGauge : stats !== null && stats !== undefined;
328
+ if (!valid) setOpen(null);
329
+ }, [open, hasGauge, stats]);
330
+
220
331
  if (!stats && !hasGauge) return null;
221
332
  return (
222
- <div className="dci-stat-root" data-composer-stats="codinput" ref={rootRef}>
333
+ <div className="dci-stat-root" data-composer-stats="codinput">
223
334
  {stats ? (
224
335
  <>
225
- <Pill stats={stats} open={open === 'rounds'} onToggle={() => setOpen(open === 'rounds' ? null : 'rounds')} />
226
- <TokenPill stats={stats} open={open === 'tokens'} onToggle={() => setOpen(open === 'tokens' ? null : 'tokens')} />
336
+ <Pill stats={stats} open={open === 'rounds'} onToggle={() => toggle('rounds')} onClose={close} />
337
+ <TokenPill stats={stats} open={open === 'tokens'} onToggle={() => toggle('tokens')} onClose={close} />
227
338
  </>
228
339
  ) : null}
229
- <ContextGauge useProjection={useProjection} />
340
+ <ContextGauge
341
+ useProjection={useProjection}
342
+ open={open === 'gauge'}
343
+ onToggle={() => toggle('gauge')}
344
+ onClose={close}
345
+ />
230
346
  </div>
231
347
  );
232
348
  }
@@ -252,27 +368,23 @@ function shortTokens(n: number): string {
252
368
  }
253
369
 
254
370
  /** 上下文占用小圈(官方 JObwrW 复刻):双环仪表 + 百分比,点击展开分段面板。
255
- * 0.1.7-rc.2 起官方渲染在 dock 行 stats 胶囊之后(原先在工具行 trailing)。 */
256
- export function ContextGauge(props: { useProjection: UseProjection | undefined }): JSX.Element | null {
257
- const { useProjection } = props;
258
- const [open, setOpen] = useState(false);
259
- const rootRef = useRef<HTMLSpanElement | null>(null);
371
+ * 0.1.7-rc.2 起官方渲染在 dock 行 stats 胶囊之后(原先在工具行 trailing)。
372
+ * 展开态由 StatsRow 持有(三枚详情面板唯一展开);面板 portal 到 body +
373
+ * useAnchoredPanel 定位(官方 ContextMeter:side top / align start / gap 8 / margin 12)。 */
374
+ export function ContextGauge(props: {
375
+ useProjection: UseProjection | undefined;
376
+ open: boolean;
377
+ onToggle(): void;
378
+ onClose(): void;
379
+ }): JSX.Element | null {
380
+ const { useProjection, open, onToggle, onClose } = props;
381
+ const { rootRef, panelRef, position } = useStatDialogSeat(open, onClose);
260
382
  const pressure = useProjection
261
383
  ? (useProjection('contextPressure') as PressureSnapshot | undefined)
262
384
  : undefined;
263
385
  const breakdown = useProjection
264
386
  ? (useProjection('contextBreakdown') as BreakdownSnapshot | undefined)
265
387
  : undefined;
266
- // 点外面关闭。
267
- useEffect(() => {
268
- if (!open) return;
269
- const onDown = (event: PointerEvent): void => {
270
- if (rootRef.current && event.target instanceof Node && rootRef.current.contains(event.target)) return;
271
- setOpen(false);
272
- };
273
- window.addEventListener('pointerdown', onDown, true);
274
- return () => window.removeEventListener('pointerdown', onDown, true);
275
- }, [open]);
276
388
 
277
389
  if (!pressure || !pressure.contextWindow) return null;
278
390
  const windowTokens = pressure.contextWindow;
@@ -294,7 +406,7 @@ export function ContextGauge(props: { useProjection: UseProjection | undefined }
294
406
  aria-haspopup="dialog"
295
407
  aria-expanded={open}
296
408
  title={t("tool.context", { percent: Math.round(pct) })}
297
- onClick={() => setOpen(!open)}
409
+ onClick={onToggle}
298
410
  >
299
411
  <svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
300
412
  <circle className="dci-gauge-track" cx="7" cy="7" r="5.5" />
@@ -307,49 +419,58 @@ export function ContextGauge(props: { useProjection: UseProjection | undefined }
307
419
  </svg>
308
420
  <span>{Math.round(pct)}%</span>
309
421
  </button>
310
- {open ? (
311
- <div className="dci-gauge-panel" role="dialog" aria-label={t("tool.contextTitle")}>
312
- <div className="dci-gauge-header">
313
- <span className="dci-gauge-headline">{t("tool.contextTitle")}</span>
314
- <span className="dci-gauge-percent">{Math.round(pct)}%</span>
315
- <span className="dci-gauge-figures">
316
- ~{shortTokens(usedTokens)} / {shortTokens(windowTokens)}
317
- </span>
318
- </div>
319
- <div className="dci-gauge-bar">
320
- {segments.map(([cls, tokens]) => (
321
- <div
322
- key={cls}
323
- className={`dci-gauge-segment ${cls}`}
324
- style={{ width: `${(tokens / windowTokens) * 100}%` }}
325
- />
326
- ))}
327
- </div>
328
- <dl className="dci-gauge-rows">
329
- <div className="dci-gauge-row">
330
- <dt>
331
- <span className={`dci-gauge-swatch dci-gauge-colorSystem`} aria-hidden="true" />
332
- {t("stats.prompt")}
333
- </dt>
334
- <dd>~{shortTokens(breakdown?.systemTokens ?? 0)}</dd>
335
- </div>
336
- <div className="dci-gauge-row">
337
- <dt>
338
- <span className={`dci-gauge-swatch dci-gauge-colorTools`} aria-hidden="true" />
339
- {t("stats.tools")}
340
- </dt>
341
- <dd>~{shortTokens(breakdown?.toolsTokens ?? 0)}</dd>
342
- </div>
343
- <div className="dci-gauge-row">
344
- <dt>
345
- <span className={`dci-gauge-swatch dci-gauge-colorMessages`} aria-hidden="true" />
346
- {t("stats.messages")}
347
- </dt>
348
- <dd>~{shortTokens(breakdown?.messageTokens ?? 0)}</dd>
349
- </div>
350
- </dl>
351
- </div>
352
- ) : null}
422
+ {open
423
+ ? createPortal(
424
+ <div
425
+ className="dci-gauge-panel"
426
+ role="dialog"
427
+ aria-label={t("tool.contextTitle")}
428
+ ref={panelRef}
429
+ style={panelStyle(position)}
430
+ >
431
+ <div className="dci-gauge-header">
432
+ <span className="dci-gauge-headline">{t("tool.contextTitle")}</span>
433
+ <span className="dci-gauge-percent">{Math.round(pct)}%</span>
434
+ <span className="dci-gauge-figures">
435
+ ~{shortTokens(usedTokens)} / {shortTokens(windowTokens)}
436
+ </span>
437
+ </div>
438
+ <div className="dci-gauge-bar">
439
+ {segments.map(([cls, tokens]) => (
440
+ <div
441
+ key={cls}
442
+ className={`dci-gauge-segment ${cls}`}
443
+ style={{ width: `${(tokens / windowTokens) * 100}%` }}
444
+ />
445
+ ))}
446
+ </div>
447
+ <dl className="dci-gauge-rows">
448
+ <div className="dci-gauge-row">
449
+ <dt>
450
+ <span className={`dci-gauge-swatch dci-gauge-colorSystem`} aria-hidden="true" />
451
+ {t("stats.prompt")}
452
+ </dt>
453
+ <dd>~{shortTokens(breakdown?.systemTokens ?? 0)}</dd>
454
+ </div>
455
+ <div className="dci-gauge-row">
456
+ <dt>
457
+ <span className={`dci-gauge-swatch dci-gauge-colorTools`} aria-hidden="true" />
458
+ {t("stats.tools")}
459
+ </dt>
460
+ <dd>~{shortTokens(breakdown?.toolsTokens ?? 0)}</dd>
461
+ </div>
462
+ <div className="dci-gauge-row">
463
+ <dt>
464
+ <span className={`dci-gauge-swatch dci-gauge-colorMessages`} aria-hidden="true" />
465
+ {t("stats.messages")}
466
+ </dt>
467
+ <dd>~{shortTokens(breakdown?.messageTokens ?? 0)}</dd>
468
+ </div>
469
+ </dl>
470
+ </div>,
471
+ document.body,
472
+ )
473
+ : null}
353
474
  </span>
354
475
  );
355
476
  }