@mingozhou/dsh-replay 0.3.3

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +164 -0
  3. package/README.zh.md +164 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/client.js +2478 -0
  6. package/lib/core.js +720 -0
  7. package/lib/index.js +111 -0
  8. package/lib/types/demo/main.d.ts +1 -0
  9. package/lib/types/demo/samples.d.ts +21 -0
  10. package/lib/types/src/client/api.d.ts +43 -0
  11. package/lib/types/src/client/components/AuditView.d.ts +8 -0
  12. package/lib/types/src/client/components/CompareView.d.ts +9 -0
  13. package/lib/types/src/client/components/ForkTreeView.d.ts +7 -0
  14. package/lib/types/src/client/components/ItemDetail.d.ts +6 -0
  15. package/lib/types/src/client/components/OverviewView.d.ts +8 -0
  16. package/lib/types/src/client/components/ReplayApp.d.ts +12 -0
  17. package/lib/types/src/client/components/ReplayModal.d.ts +8 -0
  18. package/lib/types/src/client/components/SessionPicker.d.ts +6 -0
  19. package/lib/types/src/client/components/TimelineView.d.ts +6 -0
  20. package/lib/types/src/client/export.d.ts +13 -0
  21. package/lib/types/src/client/i18n.d.ts +24 -0
  22. package/lib/types/src/client/index.d.ts +12 -0
  23. package/lib/types/src/client/mascot-image.d.ts +1 -0
  24. package/lib/types/src/client/mascot.d.ts +17 -0
  25. package/lib/types/src/client/viewer-entry.d.ts +1 -0
  26. package/lib/types/src/core/audit.d.ts +39 -0
  27. package/lib/types/src/core/compare.d.ts +31 -0
  28. package/lib/types/src/core/cost.d.ts +30 -0
  29. package/lib/types/src/core/decode.d.ts +25 -0
  30. package/lib/types/src/core/forktree.d.ts +33 -0
  31. package/lib/types/src/core/index.d.ts +7 -0
  32. package/lib/types/src/core/timeline.d.ts +96 -0
  33. package/lib/types/src/core/types.d.ts +88 -0
  34. package/lib/types/src/index.d.ts +20 -0
  35. package/lib/viewer.js +812 -0
  36. package/package.json +107 -0
  37. package/scripts/build.mjs +131 -0
  38. package/src/client/api.ts +119 -0
  39. package/src/client/components/AuditView.tsx +57 -0
  40. package/src/client/components/CompareView.tsx +186 -0
  41. package/src/client/components/ForkTreeView.tsx +123 -0
  42. package/src/client/components/ItemDetail.tsx +117 -0
  43. package/src/client/components/OverviewView.tsx +258 -0
  44. package/src/client/components/ReplayApp.tsx +205 -0
  45. package/src/client/components/ReplayModal.tsx +53 -0
  46. package/src/client/components/SessionPicker.tsx +60 -0
  47. package/src/client/components/TimelineView.tsx +296 -0
  48. package/src/client/export.ts +43 -0
  49. package/src/client/i18n.ts +360 -0
  50. package/src/client/index.ts +146 -0
  51. package/src/client/mascot-image.ts +1 -0
  52. package/src/client/mascot.tsx +192 -0
  53. package/src/client/styles.css +719 -0
  54. package/src/client/viewer-entry.tsx +66 -0
  55. package/src/core/audit.ts +227 -0
  56. package/src/core/compare.ts +0 -0
  57. package/src/core/cost.ts +61 -0
  58. package/src/core/decode.ts +125 -0
  59. package/src/core/forktree.ts +104 -0
  60. package/src/core/index.ts +7 -0
  61. package/src/core/timeline.ts +396 -0
  62. package/src/core/types.ts +90 -0
  63. package/src/index.ts +157 -0
  64. package/tsconfig.json +18 -0
  65. package/types.d.ts +15 -0
@@ -0,0 +1,123 @@
1
+ import React, { useMemo } from 'react'
2
+ import type { SessionEntry } from '../api.js'
3
+ import { shortId } from '../api.js'
4
+ import { buildForkForest, type ForkNode } from '../../core/forktree.js'
5
+ import { useI18n, type T } from '../i18n.js'
6
+ import { MascotState } from '../mascot.js'
7
+
8
+ function formatWhen(createdAt: number): string {
9
+ if (!Number.isFinite(createdAt) || createdAt <= 0) return ''
10
+ const date = new Date(createdAt)
11
+ const pad = (n: number): string => String(n).padStart(2, '0')
12
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
13
+ }
14
+
15
+ function NodeCard({
16
+ node,
17
+ currentId,
18
+ onSelect,
19
+ t,
20
+ edgeLabel,
21
+ }: {
22
+ node: ForkNode
23
+ currentId?: string
24
+ onSelect: (id: string) => void
25
+ t: T
26
+ edgeLabel?: string
27
+ }): React.ReactElement {
28
+ const tags: string[] = []
29
+ if (node.header.origin === 'subagent') tags.push(t('forks.subagent'))
30
+ else if (node.header.parentSession !== undefined && !node.orphan) tags.push(t('forks.forkTag'))
31
+ if (node.orphan) tags.push(t('forks.orphan'))
32
+ return (
33
+ <button
34
+ className="dshr-forkcard"
35
+ data-current={node.id === currentId || undefined}
36
+ onClick={() => onSelect(node.id)}
37
+ title={node.title ?? node.id}
38
+ >
39
+ <span className="dshr-forkcard-title">
40
+ {node.title ?? `${t('forks.untitled')} · ${formatWhen(node.header.createdAt)}`}
41
+ </span>
42
+ <span className="dshr-forkcard-sub">
43
+ {edgeLabel !== undefined ? `${edgeLabel} · ` : ''}
44
+ {node.title !== undefined ? `${formatWhen(node.header.createdAt)} · ` : ''}
45
+ {shortId(node.id)}
46
+ {tags.join('')}
47
+ </span>
48
+ </button>
49
+ )
50
+ }
51
+
52
+ function FamilyBlock({
53
+ node,
54
+ currentId,
55
+ onSelect,
56
+ t,
57
+ edgeLabel,
58
+ }: {
59
+ node: ForkNode
60
+ currentId?: string
61
+ onSelect: (id: string) => void
62
+ t: T
63
+ edgeLabel?: string
64
+ }): React.ReactElement {
65
+ return (
66
+ <div className="dshr-family-node">
67
+ <NodeCard node={node} currentId={currentId} onSelect={onSelect} t={t} edgeLabel={edgeLabel} />
68
+ {node.children.map(child => (
69
+ <div key={child.id} className="dshr-branch">
70
+ <FamilyBlock
71
+ node={child}
72
+ currentId={currentId}
73
+ onSelect={onSelect}
74
+ t={t}
75
+ edgeLabel={
76
+ child.seedLength !== undefined ? t('forks.edge', { n: child.seedLength }) : undefined
77
+ }
78
+ />
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )
83
+ }
84
+
85
+ export function ForkTreeView({
86
+ entries,
87
+ currentId,
88
+ onSelect,
89
+ }: {
90
+ entries: SessionEntry[]
91
+ currentId?: string
92
+ onSelect: (id: string) => void
93
+ }): React.ReactElement {
94
+ const { t } = useI18n()
95
+ const forest = useMemo(() => buildForkForest(entries), [entries])
96
+
97
+ if (forest.roots.length === 0) {
98
+ return <MascotState mood="happy" text={t('forks.empty')} />
99
+ }
100
+
101
+ // Families (roots with descendants) span the full width and draw their
102
+ // tree; standalone sessions flow into a responsive card grid.
103
+ const families = forest.roots.filter(root => root.children.length > 0)
104
+ const singles = forest.roots.filter(root => root.children.length === 0)
105
+
106
+ return (
107
+ <div className="dshr-forks">
108
+ <div className="dshr-forks-hint">{t('forks.hint')}</div>
109
+ {families.map(root => (
110
+ <div key={root.id} className="dshr-family">
111
+ <FamilyBlock node={root} currentId={currentId} onSelect={onSelect} t={t} />
112
+ </div>
113
+ ))}
114
+ {singles.length > 0 && (
115
+ <div className="dshr-forkgrid">
116
+ {singles.map(root => (
117
+ <NodeCard key={root.id} node={root} currentId={currentId} onSelect={onSelect} t={t} />
118
+ ))}
119
+ </div>
120
+ )}
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,117 @@
1
+ import React from 'react'
2
+ import type { TimelineItem, TimelineModel } from '../../core/timeline.js'
3
+ import { formatClock, formatDuration, formatTokens } from '../api.js'
4
+ import { useI18n } from '../i18n.js'
5
+
6
+ const KIND_COLOR: Record<string, string> = {
7
+ user: 'var(--dshr-s-user)',
8
+ assistant: 'var(--dshr-s-assistant)',
9
+ tool: 'var(--dshr-s-tool)',
10
+ meta: 'var(--dshr-s-meta)',
11
+ }
12
+
13
+ export function ItemDetail({
14
+ item,
15
+ model,
16
+ }: {
17
+ item: TimelineItem
18
+ model: TimelineModel
19
+ }): React.ReactElement {
20
+ const { t } = useI18n()
21
+ const usage = item.usage
22
+ return (
23
+ <div>
24
+ <h3>
25
+ <span className="dshr-badge" style={{ background: KIND_COLOR[item.kind] }}>
26
+ {t(`kind.${item.kind}`)}
27
+ </span>
28
+ {item.kind === 'tool' ? item.toolCall?.name : item.type}
29
+ {item.shadowed && (
30
+ <span className="dshr-badge" style={{ background: 'var(--dshr-s-meta)' }}>
31
+ {t('detail.shadowed')}
32
+ </span>
33
+ )}
34
+ </h3>
35
+ <div className="dshr-detail-meta">
36
+ seq {item.seq} · t+{formatClock(item.time, model.startTime)}
37
+ {item.turn !== undefined && ` · turn ${item.turn}`}
38
+ {item.step !== undefined && ` · step ${item.step}`}
39
+ </div>
40
+
41
+ {item.text !== undefined && item.text !== '' && (
42
+ <>
43
+ <div className="dshr-section-label">{t('detail.message')}</div>
44
+ <pre className="dshr-pre">{item.text}</pre>
45
+ </>
46
+ )}
47
+
48
+ {item.toolCall && (
49
+ <>
50
+ <dl className="dshr-kv">
51
+ <dt>{t('detail.callId')}</dt>
52
+ <dd>{item.toolCall.callId}</dd>
53
+ <dt>{t('detail.duration')}</dt>
54
+ <dd>
55
+ {item.toolCall.durationMs !== undefined
56
+ ? formatDuration(item.toolCall.durationMs)
57
+ : t('detail.noResult')}
58
+ </dd>
59
+ {item.toolCall.error && (
60
+ <>
61
+ <dt>{t('detail.error')}</dt>
62
+ <dd>
63
+ {item.toolCall.error.name} ({item.toolCall.error.code})
64
+ </dd>
65
+ </>
66
+ )}
67
+ </dl>
68
+ <div className="dshr-section-label">{t('detail.arguments')}</div>
69
+ <pre className="dshr-pre">{item.toolCall.argumentsPretty ?? item.toolCall.argumentsRaw}</pre>
70
+ {item.toolCall.resultText !== undefined && item.toolCall.resultText !== '' && (
71
+ <>
72
+ <div className="dshr-section-label">{t('detail.result')}</div>
73
+ <pre className="dshr-pre">{item.toolCall.resultText}</pre>
74
+ </>
75
+ )}
76
+ </>
77
+ )}
78
+
79
+ {usage && (
80
+ <>
81
+ <div className="dshr-section-label">{t('detail.usage')}</div>
82
+ <dl className="dshr-kv">
83
+ <dt>{t('detail.inputUncached')}</dt>
84
+ <dd>{formatTokens(usage.inputTokens)}</dd>
85
+ {usage.cacheReadTokens !== undefined && (
86
+ <>
87
+ <dt>{t('detail.cacheRead')}</dt>
88
+ <dd>{formatTokens(usage.cacheReadTokens)}</dd>
89
+ </>
90
+ )}
91
+ {usage.cacheWriteTokens !== undefined && (
92
+ <>
93
+ <dt>{t('detail.cacheWrite')}</dt>
94
+ <dd>{formatTokens(usage.cacheWriteTokens)}</dd>
95
+ </>
96
+ )}
97
+ <dt>{t('detail.output')}</dt>
98
+ <dd>{formatTokens(usage.outputTokens)}</dd>
99
+ {usage.reasoningTokens !== undefined && (
100
+ <>
101
+ <dt>{t('detail.reasoning')}</dt>
102
+ <dd>{formatTokens(usage.reasoningTokens)}</dd>
103
+ </>
104
+ )}
105
+ </dl>
106
+ </>
107
+ )}
108
+
109
+ {item.kind === 'meta' && (
110
+ <>
111
+ <div className="dshr-section-label">{t('detail.eventData')}</div>
112
+ <pre className="dshr-pre">{JSON.stringify(item.event.data, null, 2)}</pre>
113
+ </>
114
+ )}
115
+ </div>
116
+ )
117
+ }
@@ -0,0 +1,258 @@
1
+ import React, { useMemo, useRef, useState } from 'react'
2
+ import type { AuditReport } from '../../core/audit.js'
3
+ import { estimateCost, formatUsd } from '../../core/cost.js'
4
+ import type { TimelineModel } from '../../core/timeline.js'
5
+ import { formatClock, formatDuration, formatTokens } from '../api.js'
6
+ import { useI18n } from '../i18n.js'
7
+
8
+ /** Cumulative token samples for the line chart. */
9
+ interface Point {
10
+ x: number // event time (ms)
11
+ seq: number
12
+ turn: number
13
+ step: number
14
+ cumInput: number
15
+ cumOutput: number
16
+ }
17
+
18
+ const W = 640
19
+ const H = 200
20
+ const PAD = { top: 12, right: 16, bottom: 24, left: 52 }
21
+
22
+ function niceTicks(max: number): number[] {
23
+ if (max <= 0) return [0]
24
+ const raw = max / 3
25
+ const mag = 10 ** Math.floor(Math.log10(raw))
26
+ const step = [1, 2, 5, 10].map(m => m * mag).find(s => s >= raw) ?? mag * 10
27
+ const ticks: number[] = []
28
+ for (let v = 0; v <= max; v += step) ticks.push(v)
29
+ return ticks
30
+ }
31
+
32
+ function CumulativeTokensChart({ model }: { model: TimelineModel }): React.ReactElement | null {
33
+ const { t } = useI18n()
34
+ const svgRef = useRef<SVGSVGElement>(null)
35
+ const [hover, setHover] = useState<number>() // point index
36
+
37
+ const points = useMemo<Point[]>(() => {
38
+ let cumInput = 0
39
+ let cumOutput = 0
40
+ const seqTime = new Map(model.items.map(i => [i.seq, i.time]))
41
+ return model.usageSamples.map(s => {
42
+ cumInput += s.usage.inputTokens + (s.usage.cacheReadTokens ?? 0) + (s.usage.cacheWriteTokens ?? 0)
43
+ cumOutput += s.usage.outputTokens
44
+ return {
45
+ x: seqTime.get(s.seq) ?? model.startTime,
46
+ seq: s.seq, turn: s.turn, step: s.step,
47
+ cumInput, cumOutput,
48
+ }
49
+ })
50
+ }, [model])
51
+
52
+ if (points.length === 0) return null
53
+
54
+ const x0 = model.startTime
55
+ const x1 = Math.max(model.endTime, points.at(-1)!.x)
56
+ const yMax = Math.max(points.at(-1)!.cumInput, points.at(-1)!.cumOutput, 1)
57
+ const plotW = W - PAD.left - PAD.right
58
+ const plotH = H - PAD.top - PAD.bottom
59
+ const sx = (x: number): number => PAD.left + ((x - x0) / Math.max(1, x1 - x0)) * plotW
60
+ const sy = (y: number): number => PAD.top + plotH - (y / yMax) * plotH
61
+
62
+ const path = (key: 'cumInput' | 'cumOutput'): string =>
63
+ points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${sx(p.x).toFixed(1)} ${sy(p[key]).toFixed(1)}`).join(' ')
64
+ const area = (key: 'cumInput' | 'cumOutput'): string =>
65
+ `${path(key)} L ${sx(points.at(-1)!.x).toFixed(1)} ${sy(0)} L ${sx(points[0].x).toFixed(1)} ${sy(0)} Z`
66
+
67
+ const ticks = niceTicks(yMax)
68
+ const hovered = hover !== undefined ? points[hover] : undefined
69
+
70
+ const onMove = (event: React.MouseEvent<SVGSVGElement>): void => {
71
+ const rect = svgRef.current?.getBoundingClientRect()
72
+ if (!rect) return
73
+ const mx = ((event.clientX - rect.left) / rect.width) * W
74
+ let best = 0
75
+ let bestDist = Number.POSITIVE_INFINITY
76
+ points.forEach((p, i) => {
77
+ const d = Math.abs(sx(p.x) - mx)
78
+ if (d < bestDist) {
79
+ bestDist = d
80
+ best = i
81
+ }
82
+ })
83
+ setHover(best)
84
+ }
85
+
86
+ return (
87
+ <div className="dshr-panel">
88
+ <div className="dshr-panel-head">
89
+ <span className="dshr-panel-title">{t('ov.tokensChart')}</span>
90
+ <span className="dshr-legend">
91
+ <span className="dshr-legend-item">
92
+ <span className="dshr-legend-swatch" style={{ background: 'var(--dshr-s-assistant)' }} />
93
+ {t('ts.input')}
94
+ </span>
95
+ <span className="dshr-legend-item">
96
+ <span className="dshr-legend-swatch" style={{ background: 'var(--dshr-s-tool)' }} />
97
+ {t('ts.output')}
98
+ </span>
99
+ </span>
100
+ </div>
101
+ <div className="dshr-chart-wrap">
102
+ <svg
103
+ ref={svgRef}
104
+ viewBox={`0 0 ${W} ${H}`}
105
+ className="dshr-chart"
106
+ role="img"
107
+ aria-label={t('ov.tokensChart')}
108
+ onMouseMove={onMove}
109
+ onMouseLeave={() => setHover(undefined)}
110
+ >
111
+ {ticks.map(v => (
112
+ <g key={v}>
113
+ <line className="dshr-grid" x1={PAD.left} x2={W - PAD.right} y1={sy(v)} y2={sy(v)} />
114
+ <text className="dshr-axis-label" x={PAD.left - 8} y={sy(v) + 3} textAnchor="end">
115
+ {formatTokens(v)}
116
+ </text>
117
+ </g>
118
+ ))}
119
+ <text className="dshr-axis-label" x={PAD.left} y={H - 6}>
120
+ {formatClock(x0, model.startTime)}
121
+ </text>
122
+ <text className="dshr-axis-label" x={W - PAD.right} y={H - 6} textAnchor="end">
123
+ {formatClock(x1, model.startTime)}
124
+ </text>
125
+ <path d={area('cumInput')} fill="var(--dshr-s-assistant)" opacity={0.1} />
126
+ <path d={path('cumInput')} className="dshr-line" stroke="var(--dshr-s-assistant)" />
127
+ <path d={path('cumOutput')} className="dshr-line" stroke="var(--dshr-s-tool)" />
128
+ {/* end markers + end labels */}
129
+ <circle cx={sx(points.at(-1)!.x)} cy={sy(points.at(-1)!.cumInput)} r={4}
130
+ fill="var(--dshr-s-assistant)" stroke="var(--dshr-bg)" strokeWidth={2} />
131
+ <circle cx={sx(points.at(-1)!.x)} cy={sy(points.at(-1)!.cumOutput)} r={4}
132
+ fill="var(--dshr-s-tool)" stroke="var(--dshr-bg)" strokeWidth={2} />
133
+ {hovered && (
134
+ <g>
135
+ <line className="dshr-crosshair" x1={sx(hovered.x)} x2={sx(hovered.x)} y1={PAD.top} y2={PAD.top + plotH} />
136
+ <circle cx={sx(hovered.x)} cy={sy(hovered.cumInput)} r={4}
137
+ fill="var(--dshr-s-assistant)" stroke="var(--dshr-bg)" strokeWidth={2} />
138
+ <circle cx={sx(hovered.x)} cy={sy(hovered.cumOutput)} r={4}
139
+ fill="var(--dshr-s-tool)" stroke="var(--dshr-bg)" strokeWidth={2} />
140
+ </g>
141
+ )}
142
+ </svg>
143
+ {hovered && (
144
+ <div
145
+ className="dshr-tooltip"
146
+ style={{
147
+ left: `${(sx(hovered.x) / W) * 100}%`,
148
+ transform: `translateX(${sx(hovered.x) > W * 0.7 ? '-100%' : '8px'})`,
149
+ }}
150
+ >
151
+ <div className="dshr-tooltip-title">
152
+ t+{formatClock(hovered.x, model.startTime)} · turn {hovered.turn} · step {hovered.step}
153
+ </div>
154
+ <div>
155
+ <span className="dshr-legend-swatch" style={{ background: 'var(--dshr-s-assistant)' }} />{' '}
156
+ {t('ts.input')} <b>{formatTokens(hovered.cumInput)}</b>
157
+ </div>
158
+ <div>
159
+ <span className="dshr-legend-swatch" style={{ background: 'var(--dshr-s-tool)' }} />{' '}
160
+ {t('ts.output')} <b>{formatTokens(hovered.cumOutput)}</b>
161
+ </div>
162
+ </div>
163
+ )}
164
+ </div>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ function ToolTimeBars({ model }: { model: TimelineModel }): React.ReactElement | null {
170
+ const { t } = useI18n()
171
+ const rows = useMemo(() => {
172
+ const byTool = new Map<string, { ms: number; calls: number }>()
173
+ for (const call of model.toolCalls) {
174
+ const entry = byTool.get(call.name) ?? { ms: 0, calls: 0 }
175
+ entry.ms += call.durationMs ?? 0
176
+ entry.calls += 1
177
+ byTool.set(call.name, entry)
178
+ }
179
+ return [...byTool.entries()]
180
+ .map(([name, v]) => ({ name, ...v }))
181
+ .sort((a, b) => b.ms - a.ms)
182
+ .slice(0, 8)
183
+ }, [model])
184
+
185
+ if (rows.length === 0) return null
186
+ const max = Math.max(...rows.map(r => r.ms), 1)
187
+
188
+ return (
189
+ <div className="dshr-panel">
190
+ <div className="dshr-panel-head">
191
+ <span className="dshr-panel-title">{t('ov.toolTime')}</span>
192
+ </div>
193
+ <div className="dshr-hbars">
194
+ {rows.map(row => (
195
+ <div key={row.name} className="dshr-hbar-row" title={`${row.name} — ${formatDuration(row.ms)} · ×${row.calls}`}>
196
+ <span className="dshr-hbar-name">{row.name}</span>
197
+ <span className="dshr-hbar-track">
198
+ <span className="dshr-hbar-fill" style={{ width: `${(row.ms / max) * 100}%` }} />
199
+ </span>
200
+ <span className="dshr-hbar-value">
201
+ {formatDuration(row.ms)} <span className="dshr-hbar-calls">×{row.calls}</span>
202
+ </span>
203
+ </div>
204
+ ))}
205
+ </div>
206
+ </div>
207
+ )
208
+ }
209
+
210
+ export function OverviewView({
211
+ model,
212
+ report,
213
+ onOpenAudit,
214
+ }: {
215
+ model: TimelineModel
216
+ report: AuditReport
217
+ onOpenAudit: () => void
218
+ }): React.ReactElement {
219
+ const { t } = useI18n()
220
+ const steps = model.turns.reduce((n, turn) => n + turn.steps.length, 0)
221
+ const alerts = report.counts.critical + report.counts.serious
222
+ const cost = estimateCost(model.totals, model.models)
223
+ const tiles: { value: string; label: string; alert?: boolean; onClick?: () => void }[] = [
224
+ { value: formatDuration(model.durationMs), label: t('cmp.duration') },
225
+ { value: String(model.turns.length), label: t('cmp.turns') },
226
+ { value: String(steps), label: t('cmp.steps') },
227
+ { value: String(model.toolCalls.length), label: t('cmp.toolCalls') },
228
+ { value: formatTokens(model.totals.billedInput), label: t('ov.inputTokens') },
229
+ { value: formatTokens(model.totals.output), label: t('ov.outputTokens') },
230
+ ...(cost !== undefined
231
+ ? [{ value: formatUsd(cost.usd), label: t('ov.cost', { label: cost.label }) }]
232
+ : []),
233
+ { value: String(report.findings.length), label: t('ov.findings'), alert: alerts > 0, onClick: onOpenAudit },
234
+ ]
235
+ return (
236
+ <div className="dshr-overview">
237
+ <div className="dshr-tiles">
238
+ {tiles.map(tile => (
239
+ <button
240
+ key={tile.label}
241
+ className="dshr-tile"
242
+ data-alert={tile.alert || undefined}
243
+ data-static={tile.onClick === undefined || undefined}
244
+ onClick={tile.onClick}
245
+ >
246
+ <span className="dshr-tile-value">
247
+ {tile.value}
248
+ {tile.alert ? <span className="dshr-tile-flag">⛔ {alerts}</span> : null}
249
+ </span>
250
+ <span className="dshr-tile-label">{tile.label}</span>
251
+ </button>
252
+ ))}
253
+ </div>
254
+ <CumulativeTokensChart model={model} />
255
+ <ToolTimeBars model={model} />
256
+ </div>
257
+ )
258
+ }