dsh-context 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ Open any session and click **上下文 / Context** (to the right of Chat and Tra
57
57
  - **Transport**: host ↔ browser over a generic **Connection RPC channel** (`/dsh-context`, `ctx.connection.rpc` — the same channel mechanism the api gateway uses). The host half registers a `snapshot` endpoint; the client half calls it via `ctx.connection.rpc.call`.
58
58
  - **Incremental fold**: per-session fold state lives in the Host half, so each poll only processes newly appended events — reopening the tab is instant.
59
59
  - **Events decoded**: `request/header` (system prompt + tool schemas), surface events with `surfaceOp` (append/replace — compaction rewrites history in place), `compaction/summary|prune`, `assistant/message.usage` (real provider tokens), and message `source` metadata (`plugin` forms, `skill-invocation`) for injection events.
60
- - **Architecture**: `src/host.ts` is a plain ESM Cordis plugin (zero runtime dependencies) loaded by the `dsh-context` loader row; `src/client.cts` is the browser half, transpiled and wrapped at build time into the web boot's closure-factory bundle (`window.__ModuleLoader__.load`). The client renders with bare `React.createElement` — theme-native via dsh CSS variables, bilingual via the client `locale` service. Both halves are TypeScript with strict mode and local (drift-free) service contracts.
60
+ - **Architecture**: `src/host/` is a plain ESM Cordis plugin (zero runtime dependencies) loaded by the `dsh-context` loader row; `src/client/` is the browser half, bundled at build time into the web boot's closure-factory bundle (`window.__ModuleLoader__.load`). The client renders with bare `React.createElement` — theme-native via dsh CSS variables, bilingual via the client `locale` service. Both halves are strict TypeScript with local (drift-free) service contracts, sharing the wire model in `src/shared/types.ts`.
61
61
 
62
62
  ## Development
63
63
 
@@ -68,22 +68,51 @@ pnpm install # devDependencies only — the plugin itself stays depen
68
68
  pnpm run typecheck # tsc --noEmit (strict)
69
69
  pnpm run build # esbuild: lib/index.js (host) + lib/client.js (client bundle)
70
70
  pnpm test # typecheck + functional tests for both halves
71
+ pnpm run repro # real-React + jsdom render smoke (granularity-toggle regression)
71
72
  ```
72
73
 
73
74
  `build.mjs` also smoke-checks the outputs (both halves must parse; the host half must import with the `name`/`inject`/`apply` plugin shape).
74
75
 
75
76
  Commits are guarded by a [husky](https://typicode.github.io/husky/) pre-commit hook that runs `pnpm run typecheck` (`tsc --noEmit`, strict).
76
77
 
78
+ ## Project structure
79
+
80
+ ```
81
+ src/
82
+ shared/types.ts # Wire contract: Snapshot / RequestRecord / ContextEventRecord / SurfaceNode
83
+ host/ # Host half — folds the session log into the snapshot
84
+ index.ts # Plugin entry: name/inject/apply + /dsh-context RPC endpoint
85
+ services.ts # Local service contracts (sessions/sessionQuery/connection)
86
+ pricing.ts # dsh token-meter heuristic (estimateMessage/system/tools)
87
+ fold.ts # Incremental per-session fold + whole-turn retention
88
+ snapshot.ts # Snapshot building + event turn/step attribution + caching
89
+ client/ # Client half — the Context tab UI
90
+ index.ts # Plugin entry: dicts, styles, slot registration
91
+ view.ts # Composition root: ViewKit + component wiring
92
+ viewkit.ts # Shared component dependencies (t/tr/fmt/catLabel/event helpers)
93
+ i18n.ts / styles.ts # Bilingual dictionaries / theme-native styles
94
+ format.ts # Number (k/M) and time formatting
95
+ categories.ts # Category colors + partsOf breakdown projection
96
+ cache.ts # Per-session snapshot cache (stale-while-revalidate)
97
+ components/ # One module per component (TrendChart, RequestDetail, …)
98
+ scripts/ # build.mjs (esbuild) + publish.sh (npm release)
99
+ tests/ # host.test.mjs, client.test.mjs, repro-real-react.mjs
100
+ ```
101
+
102
+ First-screen notes: the tab seeds its initial render from the per-session snapshot cache (re-opening a session paints instantly, the poll refreshes behind it), pauses polling while the tab is hidden, and never blanks already-visible data on a transient fetch error. On the host, cold-session folding is a one-time ~10 ms pass over the durable log and is cached afterwards, so the `/dsh-context` poll stays cheap.
103
+
77
104
  ## Files
78
105
 
79
106
  | File | Role |
80
107
  | --- | --- |
81
- | `src/host.ts` | Host half (strict TS): incremental log fold, category accounting, `/dsh-context` snapshot RPC |
82
- | `src/client.cts` | Client half (strict TS, CJS-flavored for the browser bundle): tab registration, bilingual chart UI |
108
+ | `src/shared/types.ts` | Shared wire contract (type-only; both halves import it) |
109
+ | `src/host/*.ts` | Host half: fold, pricing, snapshot, RPC entry |
110
+ | `src/client/*.ts` | Client half: view composition, components, i18n, styles |
83
111
  | `tsconfig.json` | Strict typecheck config (noEmit; esbuild does the transpiling) |
84
112
  | `package.json` | `dsh.bundle` (patch layer) + `dsh.client` (web UI) manifests |
85
113
  | `cordis.patch.yml` | The bundle's patch layer: inserts the `dsh-context` row |
86
114
  | `scripts/build.mjs` | esbuild-based build of `lib/index.js` + `lib/client.js` |
115
+ | `tests/*.mjs` | Functional tests for both halves + the real-React repro |
87
116
  | `docs/screenshot.png` | The UI in action |
88
117
 
89
118
  ## License
package/lib/client.js CHANGED
@@ -1,13 +1,13 @@
1
- /* dsh-context client bundle — generated by scripts/build.mjs from src/client.ts */
1
+ /* dsh-context client bundle — generated by scripts/build.mjs from src/client/ */
2
2
  window.__ModuleLoader__.load({
3
3
  id: "dsh-context",
4
4
  factory: (require) => {
5
5
  var module = { exports: {} };
6
6
  var exports = module.exports;
7
7
  "use strict";
8
- const React = require("react");
9
- const h = React.createElement;
10
- const DICT_ZH = {
8
+
9
+ // src/client/i18n.ts
10
+ var DICT_ZH = {
11
11
  "tab": "\u4E0A\u4E0B\u6587",
12
12
  "cat.system": "\u7CFB\u7EDF\u63D0\u793A",
13
13
  "cat.tools": "\u5DE5\u5177\u5B9A\u4E49",
@@ -18,22 +18,36 @@ const DICT_ZH = {
18
18
  "overview.title": "\u5F53\u524D\u6784\u6210",
19
19
  "overview.ofWindow": "tokens\uFF08\u7EA6 {p}%\uFF09",
20
20
  "overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
21
- "overview.actual": "\xB7 \u4E0A\u6B21\u5B9E\u9645 prompt {n}",
21
+ "overview.free": "\u5269\u4F59\u7A97\u53E3",
22
+ "stats.title": "\u4E0A\u4E0B\u6587\u7EDF\u8BA1",
23
+ "stats.hint": "\u7EDF\u8BA1\u5F53\u524D\u4FDD\u7559\u7684\u5386\u53F2\u7A97\u53E3\uFF08\u4E0E\u8D8B\u52BF\u56FE\u4E00\u81F4\uFF09",
24
+ "stats.turns": "\u8F6E\u6B21",
25
+ "stats.steps": "\u6B65\u6570",
26
+ "stats.recycled": "\u2702 \u56DE\u6536",
27
+ "stats.recycleSub": "\u538B\u7F29 {c} \u6B21 \xB7 \u526A\u679D {p} \u6B21",
28
+ "stats.injects": "\uFF0B \u6CE8\u5165",
29
+ "stats.switches": "\u21C4 \u6A21\u578B\u5207\u6362",
30
+ "stats.est": "\u2248 \u7D2F\u8BA1\u4F30\u7B97",
31
+ "stats.actualPrompt": "\u5B9E\u9645 prompt",
32
+ "stats.output": "\u8F93\u51FA",
22
33
  "tools.top": "\u5DE5\u5177\u5B9A\u4E49 Top\uFF1A",
23
34
  "tools.more": "\u7B49 {n} \u4E2A",
24
35
  "trend.title": "\u5386\u53F2\u8D8B\u52BF",
25
36
  "gran.step": "\u6B65\u9AA4",
26
37
  "gran.turn": "\u8F6E\u6B21",
27
- "trend.hint": "\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u4E00\u6BB5\uFF1B\u70B9\u51FB\u67F1\u5B50\u67E5\u770B\u8BE6\u60C5\uFF0C\u2702 \u8868\u793A\u538B\u7F29/\u526A\u679D\uFF1B\u5DE6\u53F3\u6ED1\u52A8\u53EF\u56DE\u770B\u66F4\u65E9",
38
+ "trend.hint": "\u60AC\u505C\u67F1\u5B50\u67E5\u770B\u8BE6\u60C5\uFF08\u70B9\u51FB\u56FA\u5B9A\uFF09\uFF0C\u2702 \u8868\u793A\u538B\u7F29/\u526A\u679D\uFF1B\u5DE6\u53F3\u6ED1\u52A8\u53EF\u56DE\u770B\u66F4\u65E9\uFF1BStep/Turn \u5207\u6362\u7C92\u5EA6",
28
39
  "trend.empty": "\u53D1\u8D77\u4E00\u8F6E\u5BF9\u8BDD\u540E\uFF0C\u8FD9\u91CC\u4F1A\u5C55\u793A\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u7684\u4E0A\u4E0B\u6587\u6784\u6210",
29
- "detail.step": "T{t} \xB7 \u7B2C {s} \u6B65",
30
- "detail.turn": "T{t} \xB7 \u5171 {n} \u6B65",
40
+ "detail.step": "\u7B2C {t} \u8F6E \xB7 \u7B2C {s} \u6B65",
41
+ "detail.turn": "\u7B2C {t} \u8F6E \xB7 \u5171 {n} \u6B65",
31
42
  "detail.lastStep": "\u672B\u6B65",
32
43
  "detail.estTotal": "\u4F30\u7B97\u5408\u8BA1 \u2248 {n}",
33
44
  "detail.actual": "\u5B9E\u9645 prompt {n}",
34
45
  "detail.output": "\u8F93\u51FA {n}",
35
46
  "events.title": "\u4E0A\u4E0B\u6587\u4E8B\u4EF6",
36
47
  "events.empty": "\u6682\u65E0\u4E0A\u4E0B\u6587\u4E8B\u4EF6\uFF08\u538B\u7F29\u3001\u6CE8\u5165\u3001\u6A21\u578B\u5207\u6362\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\uFF09",
48
+ "events.at": "\u7B2C {t} \u8F6E \xB7 \u7B2C {s} \u6B65",
49
+ "events.range": "\u7B2C {t} \u8F6E \xB7 \u7B2C {a}\u2192{b} \u6B65",
50
+ "events.rangeTo": "\u7B2C {a} \u8F6E \xB7 \u7B2C {as} \u6B65 \u2192 \u7B2C {b} \u8F6E \xB7 \u7B2C {bs} \u6B65",
37
51
  "nodes.title": "\u6D88\u606F\u6784\u6210",
38
52
  "nodes.hint": "\u5F53\u524D\u6A21\u578B\u53EF\u89C1\u7684\u6D88\u606F\uFF0C\u6700\u65B0\u5728\u524D",
39
53
  "nodes.more": "\u2026 \u66F4\u65E9\u7684 {n} \u6761\u6D88\u606F\u5DF2\u7701\u7565",
@@ -41,8 +55,8 @@ const DICT_ZH = {
41
55
  "loading": "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u2026",
42
56
  "error": "\u4E0A\u4E0B\u6587\u6570\u636E\u8BFB\u53D6\u5931\u8D25\uFF1A",
43
57
  "footer": "\u4F30\u7B97\u53E3\u5F84\uFF1A\u4E0E dsh \u5185\u7F6E tokenMeter \u76F8\u540C\u7684\u56FA\u5B9A\u5BC6\u5EA6\u542F\u53D1\u5F0F\uFF08\u7EA6 4 \u5B57\u7B26 \u2248 1 token\uFF09\uFF1B\u300C\u5B9E\u9645\u300D\u4E3A\u4F9B\u5E94\u5546\u4E0A\u62A5\u7528\u91CF\u3002",
44
- "tip.step": "T{t} \xB7 \u7B2C{s}\u6B65",
45
- "tip.turn": "T{t} \xB7 \u5171 {n} \u6B65",
58
+ "tip.step": "\u7B2C {t} \u8F6E \xB7 \u7B2C{s}\u6B65",
59
+ "tip.turn": "\u7B2C {t} \u8F6E \xB7 \u5171 {n} \u6B65",
46
60
  "tip.total": "\u5408\u8BA1 \u2248 {n}",
47
61
  "tip.actual": "\uFF08\u5B9E\u9645 {n}\uFF09",
48
62
  "ev.compaction": "\u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u6458\u8981\u66FF\u6362 {n} \u6761\u6D88\u606F\uFF09",
@@ -62,7 +76,7 @@ const DICT_ZH = {
62
76
  "node.nonText": "(\u975E\u6587\u672C\u6D88\u606F)",
63
77
  "node.snapshot": "\u5FEB\u7167: "
64
78
  };
65
- const DICT_EN = {
79
+ var DICT_EN = {
66
80
  "tab": "Context",
67
81
  "cat.system": "System",
68
82
  "cat.tools": "Tool schemas",
@@ -73,22 +87,36 @@ const DICT_EN = {
73
87
  "overview.title": "Current composition",
74
88
  "overview.ofWindow": "tokens (~{p}%)",
75
89
  "overview.estimate": "tokens (estimated)",
76
- "overview.actual": "\xB7 last actual prompt {n}",
90
+ "overview.free": "Free window",
91
+ "stats.title": "Context stats",
92
+ "stats.hint": "Over the retained history window (same as the History chart)",
93
+ "stats.turns": "Turns",
94
+ "stats.steps": "Steps",
95
+ "stats.recycled": "\u2702 Recycled",
96
+ "stats.recycleSub": "{c} compactions \xB7 {p} prunes",
97
+ "stats.injects": "\uFF0B Injections",
98
+ "stats.switches": "\u21C4 Model switches",
99
+ "stats.est": "\u2248 Estimated total",
100
+ "stats.actualPrompt": "actual prompt",
101
+ "stats.output": "output",
77
102
  "tools.top": "Top tool schemas:",
78
103
  "tools.more": "of {n}",
79
104
  "trend.title": "History",
80
105
  "gran.step": "Step",
81
106
  "gran.turn": "Turn",
82
- "trend.hint": "one bar per model request; click a bar for details, \u2702 marks compaction/prune; scroll sideways to see earlier",
107
+ "trend.hint": "hover a bar for details (click to pin), \u2702 marks compaction/prune; scroll sideways to see earlier; Step/Turn switches granularity",
83
108
  "trend.empty": "Send a message and each model request\u2019s context makeup shows up here",
84
- "detail.step": "T{t} \xB7 step {s}",
85
- "detail.turn": "T{t} \xB7 {n} steps",
109
+ "detail.step": "Turn {t} \xB7 Step {s}",
110
+ "detail.turn": "Turn {t} \xB7 {n} steps",
86
111
  "detail.lastStep": "last step",
87
112
  "detail.estTotal": "estimated \u2248 {n}",
88
113
  "detail.actual": "actual prompt {n}",
89
114
  "detail.output": "output {n}",
90
115
  "events.title": "Context events",
91
116
  "events.empty": "No context events yet (compaction, injections, model switches appear here)",
117
+ "events.at": "Turn {t} \xB7 Step {s}",
118
+ "events.range": "Turn {t} \xB7 Step {a}\u2192{b}",
119
+ "events.rangeTo": "Turn {a} \xB7 Step {as} \u2192 Turn {b} \xB7 Step {bs}",
92
120
  "nodes.title": "Messages",
93
121
  "nodes.hint": "currently model-visible, newest first",
94
122
  "nodes.more": "\u2026 {n} earlier messages omitted",
@@ -96,8 +124,8 @@ const DICT_EN = {
96
124
  "loading": "Reading the session log\u2026",
97
125
  "error": "Failed to read context data: ",
98
126
  "footer": "Estimate: same fixed-density heuristic as dsh\u2019s built-in tokenMeter (~4 chars \u2248 1 token); \u201Cactual\u201D is provider-reported usage.",
99
- "tip.step": "T{t} \xB7 step {s}",
100
- "tip.turn": "T{t} \xB7 {n} steps",
127
+ "tip.step": "Turn {t} \xB7 Step {s}",
128
+ "tip.turn": "Turn {t} \xB7 {n} steps",
101
129
  "tip.total": "total \u2248 {n}",
102
130
  "tip.actual": " (actual {n})",
103
131
  "ev.compaction": "Context compacted (summary replaced {n} messages)",
@@ -117,9 +145,135 @@ const DICT_EN = {
117
145
  "node.nonText": "(non-text message)",
118
146
  "node.snapshot": "snapshot: "
119
147
  };
120
- const EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
148
+
149
+ // src/client/styles.ts
150
+ var STYLES = [
151
+ ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
152
+ ".lc-card { background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }",
153
+ ".lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }",
154
+ ".lc-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: 8px; }",
155
+ ".lc-stat { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }",
156
+ ".lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
157
+ ".lc-stat-value { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
158
+ ".lc-stat-sub { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
159
+ ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
160
+ ".lc-gran { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
161
+ ".lc-gran-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; }",
162
+ ".lc-gran-btn:hover { color: var(--dsw-alias-label-primary); }",
163
+ ".lc-gran-on, .lc-gran-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }",
164
+ ".lc-overview-num { margin-bottom: 8px; }",
165
+ ".lc-overview-num b { font-size: 20px; }",
166
+ ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
167
+ ".lc-stacked-wrap { position: relative; width: 100%; }",
168
+ ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }",
169
+ ".lc-bar-tip { position: absolute; bottom: calc(100% + 6px); transform: translateX(-50%); z-index: 5; white-space: nowrap; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; }",
170
+ ".lc-stacked > div { height: 100%; }",
171
+ ".lc-stacked-seg-on { filter: brightness(1.18); }",
172
+ ".lc-stacked-free-on { box-shadow: inset 0 0 0 1px var(--dsw-alias-label-secondary); border-radius: 3px; }",
173
+ ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
174
+ ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
175
+ ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
176
+ ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
177
+ ".lc-chip-on { font-weight: 600; }",
178
+ ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
179
+ ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
180
+ ".lc-tool-chip { background: var(--dsw-alias-bg-layer-2); border-radius: 4px; padding: 1px 7px; font-size: 12px; color: var(--dsw-alias-label-primary); }",
181
+ ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
182
+ ".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
183
+ ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
184
+ ".lc-axis-top { top: 13px; }",
185
+ ".lc-axis-mid { top: 69px; }",
186
+ ".lc-axis-bot { top: 125px; }",
187
+ ".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
188
+ ".lc-chart-scroll::-webkit-scrollbar { height: 8px; }",
189
+ ".lc-chart-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 45%, transparent); border-radius: 4px; }",
190
+ ".lc-chart-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 70%, transparent); }",
191
+ ".lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }",
192
+ ".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
193
+ ".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
194
+ ".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
195
+ ".lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }",
196
+ ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
197
+ ".lc-grid-top { top: 18px; }",
198
+ ".lc-grid-mid { top: 74px; }",
199
+ ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }",
200
+ ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
201
+ ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
202
+ ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
203
+ ".lc-bar-in-turn { background: rgba(99,102,241,0.14); }",
204
+ ".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
205
+ ".lc-bar-stack > div { width: 100%; }",
206
+ ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
207
+ ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
208
+ ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms; }",
209
+ ".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
210
+ ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
211
+ ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
212
+ ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
213
+ ".lc-detail-marker { color: var(--dsw-alias-state-warn-primary); font-size: 11px; background: var(--dsw-alias-bg-layer-2); border-radius: 6px; padding: 1px 7px; }",
214
+ ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
215
+ ".lc-detail-tag { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); }",
216
+ ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
217
+ ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
218
+ ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
219
+ ".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
220
+ ".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
221
+ ".lc-detail-num { width: 44px; text-align: right; }",
222
+ ".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
223
+ ".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; }",
224
+ ".lc-col { flex: 1; min-width: 280px; }",
225
+ ".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
226
+ ".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
227
+ ".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
228
+ ".lc-event-icon.lc-event-inject { color: #a855f7; }",
229
+ ".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
230
+ ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
231
+ ".lc-event-at { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }",
232
+ ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); }",
233
+ ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
234
+ ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
235
+ ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
236
+ ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
237
+ ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
238
+ ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
239
+ ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
240
+ ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
241
+ ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }"
242
+ ].join("\n");
243
+
244
+ // src/client/cache.ts
245
+ var MAX_CACHED_SESSIONS = 10;
246
+ var sessionCache = /* @__PURE__ */ new Map();
247
+ function cacheGet(sessionId) {
248
+ return sessionCache.get(sessionId);
249
+ }
250
+ function cachePut(sessionId, snapshot) {
251
+ sessionCache.set(sessionId, snapshot);
252
+ if (sessionCache.size > MAX_CACHED_SESSIONS) {
253
+ const oldest = sessionCache.keys().next().value;
254
+ if (oldest !== void 0) sessionCache.delete(oldest);
255
+ }
256
+ }
257
+
258
+ // src/client/categories.ts
259
+ var CATS = [
260
+ { key: "system", color: "#6366f1" },
261
+ { key: "tools", color: "#f59e0b" },
262
+ { key: "user", color: "#22c55e" },
263
+ { key: "inject", color: "#a855f7" },
264
+ { key: "assistant", color: "#3b82f6" },
265
+ { key: "tool", color: "#14b8a6" }
266
+ ];
267
+ function partsOf(breakdown) {
268
+ return CATS.map((c) => {
269
+ return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
270
+ });
271
+ }
272
+
273
+ // src/client/format.ts
121
274
  function fmt(n) {
122
275
  if (n === void 0 || n === null || isNaN(n)) return "\u2014";
276
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
123
277
  if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
124
278
  return String(Math.round(n));
125
279
  }
@@ -130,36 +284,14 @@ function fmtTime(t) {
130
284
  }
131
285
  return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
132
286
  }
133
- function aggregateByTurn(requests) {
134
- const out = [];
135
- let runSteps = 0;
136
- for (const req of requests) {
137
- const last = out.length > 0 ? out[out.length - 1] : null;
138
- if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
139
- runSteps++;
140
- out[out.length - 1] = { ...req, stepCount: runSteps };
141
- } else {
142
- runSteps = 1;
143
- out.push({ ...req, stepCount: 1 });
144
- }
145
- }
146
- return out;
147
- }
148
- function makeView(ctx, t) {
149
- function tr(key, vars) {
150
- return t(key, vars);
151
- }
152
- const CATS = [
153
- { key: "system", color: "#6366f1" },
154
- { key: "tools", color: "#f59e0b" },
155
- { key: "user", color: "#22c55e" },
156
- { key: "inject", color: "#a855f7" },
157
- { key: "assistant", color: "#3b82f6" },
158
- { key: "tool", color: "#14b8a6" }
159
- ];
160
- function catLabel(key) {
161
- return t("cat." + key);
162
- }
287
+
288
+ // src/client/react.ts
289
+ var React = require("react");
290
+ var h = React.createElement;
291
+
292
+ // src/client/components/events.ts
293
+ var EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
294
+ function makeEventText(t, tr) {
163
295
  function eventLabel(ev) {
164
296
  if (ev.kind === "compaction") return tr("ev.compaction", { n: ev.count || 0 });
165
297
  if (ev.kind === "prune") return t("ev.prune");
@@ -171,6 +303,58 @@ function makeView(ctx, t) {
171
303
  }
172
304
  return ev.kind;
173
305
  }
306
+ function eventAt(ev) {
307
+ if (ev.kind === "compaction" || ev.kind === "prune") {
308
+ if (typeof ev.turn === "number" && typeof ev.step === "number") {
309
+ if (typeof ev.fromTurn === "number" && typeof ev.fromStep === "number") {
310
+ if (ev.fromTurn === ev.turn) return t("events.range", { t: ev.turn, a: ev.fromStep, b: ev.step });
311
+ return t("events.rangeTo", { a: ev.fromTurn, as: ev.fromStep, b: ev.turn, bs: ev.step });
312
+ }
313
+ return t("events.at", { t: ev.turn, s: ev.step });
314
+ }
315
+ return null;
316
+ }
317
+ if (typeof ev.turn === "number" && typeof ev.step === "number") {
318
+ return t("events.at", { t: ev.turn, s: ev.step });
319
+ }
320
+ return null;
321
+ }
322
+ return { eventLabel, eventAt };
323
+ }
324
+ function makeEventList(kit) {
325
+ const { t, fmt: fmt3, fmtTime: fmtTime2, eventLabel, eventAt } = kit;
326
+ return function EventList(props) {
327
+ if (props.events.length === 0) {
328
+ return h("div", { className: "lc-empty" }, t("events.empty"));
329
+ }
330
+ const sorted = props.events.slice().reverse();
331
+ return h(
332
+ "div",
333
+ { className: "lc-events" },
334
+ sorted.map((ev, i) => {
335
+ const label = eventLabel(ev);
336
+ const at = eventAt(ev);
337
+ return h(
338
+ "div",
339
+ { key: ev.seq + "-" + i, className: "lc-event" },
340
+ h("span", { className: "lc-event-icon lc-event-" + ev.kind }, EVENT_ICONS[ev.kind] || "\u2022"),
341
+ h("span", { className: "lc-event-label", title: label }, label),
342
+ at !== null ? h("span", { className: "lc-event-at" }, at) : null,
343
+ ev.tokens ? h(
344
+ "span",
345
+ { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") },
346
+ (ev.kind === "inject" ? "+" : "\u2212") + fmt3(ev.tokens)
347
+ ) : null,
348
+ h("span", { className: "lc-event-time" }, fmtTime2(ev.time))
349
+ );
350
+ })
351
+ );
352
+ };
353
+ }
354
+
355
+ // src/client/components/nodes.ts
356
+ function makeNodeList(kit) {
357
+ const { t, tr, fmt: fmt3, fmtTime: fmtTime2 } = kit;
174
358
  function nodeText(n) {
175
359
  if (n.cat === "tool") {
176
360
  return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
@@ -182,23 +366,171 @@ function makeView(ctx, t) {
182
366
  if (n.cat === "inject") return t("form." + (n.form || "context"));
183
367
  return t("node.nonText");
184
368
  }
185
- function StackedBar(props) {
369
+ return function NodeList(props) {
370
+ if (props.nodes.length === 0) {
371
+ return h("div", { className: "lc-empty" }, t("nodes.empty"));
372
+ }
373
+ const catColor = {};
374
+ for (const c of CATS) catColor[c.key] = c.color;
375
+ const rows = props.nodes.slice().reverse();
376
+ return h(
377
+ "div",
378
+ { className: "lc-nodes" },
379
+ props.dropped > 0 ? h("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null,
380
+ rows.map((n) => {
381
+ const text = nodeText(n);
382
+ return h(
383
+ "div",
384
+ { key: n.seq, className: "lc-node" },
385
+ h("i", { style: { background: catColor[n.cat] || "#999" } }),
386
+ h("span", { className: "lc-node-preview", title: text }, text),
387
+ // Timestamp when the host event carried one.
388
+ typeof n.time === "number" ? h("span", { className: "lc-node-time" }, fmtTime2(n.time)) : null,
389
+ h("span", { className: "lc-node-tokens" }, fmt3(n.tokens))
390
+ );
391
+ })
392
+ );
393
+ };
394
+ }
395
+
396
+ // src/client/components/requestDetail.ts
397
+ function makeRequestDetail(kit, StackedBar) {
398
+ const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
399
+ return function RequestDetail(props) {
400
+ const req = props.request;
401
+ if (!req) return null;
402
+ const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
403
+ const head = isTurn ? tr("detail.turn", { t: req.turn ?? 0, n: req.stepCount ?? 0 }) : tr("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 });
404
+ const marker = props.marker ?? null;
405
+ const markerAt = marker !== null ? eventAt(marker) : null;
406
+ return h(
407
+ "div",
408
+ { className: "lc-detail" },
409
+ h(
410
+ "div",
411
+ { className: "lc-detail-head" },
412
+ h("b", null, head),
413
+ marker !== null && markerAt !== null ? h("span", { className: "lc-detail-marker", title: eventLabel(marker) }, "\u2702 " + markerAt) : null,
414
+ isTurn ? h("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null,
415
+ h("span", null, fmtTime2(req.time)),
416
+ h("span", null, tr("detail.estTotal", { n: fmt3(req.total) })),
417
+ req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt3(req.prompt) })) : null,
418
+ req.output !== void 0 ? h("span", null, tr("detail.output", { n: fmt3(req.output) })) : null
419
+ ),
420
+ h(StackedBar, { parts: partsOf(req), height: 10 }),
421
+ h(
422
+ "div",
423
+ { className: "lc-detail-rows" },
424
+ CATS.map((c) => {
425
+ const v = req[c.key] || 0;
426
+ return h(
427
+ "div",
428
+ { key: c.key, className: "lc-detail-row" },
429
+ h("i", { style: { background: c.color } }),
430
+ h("span", { className: "lc-detail-label" }, catLabel(c.key)),
431
+ h(
432
+ "span",
433
+ { className: "lc-bar-track" },
434
+ h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
435
+ ),
436
+ h("span", { className: "lc-detail-num" }, fmt3(v)),
437
+ h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
438
+ );
439
+ })
440
+ )
441
+ );
442
+ };
443
+ }
444
+
445
+ // src/client/components/statsBoard.ts
446
+ function makeStatsBoard(kit) {
447
+ const { t, tr, fmt: fmt3 } = kit;
448
+ return function StatsBoard(props) {
449
+ const turns = /* @__PURE__ */ new Set();
450
+ let steps = 0, compactions = 0, prunes = 0, injects = 0, switches = 0;
451
+ let recycled = 0, est = 0, prompt = 0, output = 0;
452
+ for (const req of props.requests) {
453
+ turns.add(req.turn ?? 0);
454
+ steps++;
455
+ est += req.total;
456
+ if (typeof req.prompt === "number") prompt += req.prompt;
457
+ if (typeof req.output === "number") output += req.output;
458
+ }
459
+ for (const ev of props.events) {
460
+ if (ev.kind === "compaction") {
461
+ compactions++;
462
+ recycled += ev.tokens || 0;
463
+ } else if (ev.kind === "prune") {
464
+ prunes++;
465
+ recycled += ev.tokens || 0;
466
+ } else if (ev.kind === "inject") injects++;
467
+ else if (ev.kind === "model") switches++;
468
+ }
469
+ const cell = (label, value, sub) => h(
470
+ "div",
471
+ { className: "lc-stat" },
472
+ h("span", { className: "lc-stat-label" }, label),
473
+ h("b", { className: "lc-stat-value" }, value),
474
+ sub !== void 0 ? h("span", { className: "lc-stat-sub" }, sub) : null
475
+ );
476
+ return h(
477
+ "div",
478
+ { className: "lc-card" },
479
+ h(
480
+ "div",
481
+ { className: "lc-card-title" },
482
+ t("stats.title"),
483
+ h("span", { className: "lc-card-sub" }, t("stats.hint"))
484
+ ),
485
+ h(
486
+ "div",
487
+ { className: "lc-stats" },
488
+ cell(t("stats.turns"), fmt3(turns.size)),
489
+ cell(t("stats.steps"), fmt3(steps)),
490
+ cell(
491
+ t("stats.recycled"),
492
+ recycled > 0 ? "\u2212" + fmt3(recycled) : "0",
493
+ tr("stats.recycleSub", { c: compactions, p: prunes })
494
+ ),
495
+ cell(t("stats.injects"), fmt3(injects)),
496
+ cell(t("stats.switches"), fmt3(switches)),
497
+ cell(t("stats.est"), "\u2248 " + fmt3(est)),
498
+ cell(t("stats.actualPrompt"), fmt3(prompt)),
499
+ cell(t("stats.output"), fmt3(output))
500
+ )
501
+ );
502
+ };
503
+ }
504
+
505
+ // src/client/components/stackedBar.ts
506
+ function makeStackedBar(kit) {
507
+ const { t, fmt: fmt3, catLabel } = kit;
508
+ return function StackedBar(props) {
186
509
  let total = 0;
187
510
  for (const p of props.parts) total += p.value;
188
511
  const scale = props.max !== void 0 && props.max > total ? props.max : total;
512
+ const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
189
513
  let tip = null;
190
514
  if (props.hoverKey !== null && props.hoverKey !== void 0) {
191
- let acc = 0;
192
- for (const p of props.parts) {
193
- const pct = scale > 0 ? p.value / scale * 100 : 0;
194
- if (p.key === props.hoverKey && p.value > 0) {
195
- tip = {
196
- text: catLabel(p.key) + " " + fmt(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
197
- leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
198
- };
199
- break;
515
+ if (props.hoverKey === "free" && free > 0) {
516
+ const pct = scale > 0 ? free / scale * 100 : 0;
517
+ tip = {
518
+ text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
519
+ leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
520
+ };
521
+ } else {
522
+ let acc = 0;
523
+ for (const p of props.parts) {
524
+ const pct = scale > 0 ? p.value / scale * 100 : 0;
525
+ if (p.key === props.hoverKey && p.value > 0) {
526
+ tip = {
527
+ text: catLabel(p.key) + " " + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
528
+ leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
529
+ };
530
+ break;
531
+ }
532
+ acc += pct;
200
533
  }
201
- acc += pct;
202
534
  }
203
535
  }
204
536
  return h(
@@ -213,7 +545,7 @@ function makeView(ctx, t) {
213
545
  if (props.onHoverKey !== void 0) props.onHoverKey(null);
214
546
  }
215
547
  },
216
- total <= 0 ? null : props.parts.map((p) => {
548
+ total > 0 ? props.parts.map((p) => {
217
549
  if (!p.value) return null;
218
550
  const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
219
551
  return h("div", {
@@ -224,12 +556,23 @@ function makeView(ctx, t) {
224
556
  if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
225
557
  }
226
558
  });
227
- })
559
+ }) : null,
560
+ free > 0 ? h("div", {
561
+ key: "free",
562
+ className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
563
+ style: { width: free / scale * 100 + "%" },
564
+ onMouseEnter: () => {
565
+ if (props.onHoverKey !== void 0) props.onHoverKey("free");
566
+ }
567
+ }) : null
228
568
  ),
229
569
  tip ? h("div", { className: "lc-bar-tip", style: { left: tip.leftPct + "%" } }, tip.text) : null
230
570
  );
231
- }
232
- function Legend(props) {
571
+ };
572
+ }
573
+ function makeLegend(kit) {
574
+ const { fmt: fmt3, catLabel } = kit;
575
+ return function Legend(props) {
233
576
  let total = 0;
234
577
  for (const p of props.parts) total += p.value;
235
578
  return h(
@@ -250,35 +593,54 @@ function makeView(ctx, t) {
250
593
  }
251
594
  },
252
595
  h("i", { style: { background: p.color } }),
253
- catLabel(p.key) + " " + fmt(p.value),
596
+ catLabel(p.key) + " " + fmt3(p.value),
254
597
  total > 0 ? h("em", null, Math.round(p.value / total * 100) + "%") : null
255
598
  );
256
599
  })
257
600
  );
601
+ };
602
+ }
603
+
604
+ // src/client/components/trendChart.ts
605
+ function aggregateByTurn(requests) {
606
+ const out = [];
607
+ let runSteps = 0;
608
+ for (const req of requests) {
609
+ const last = out.length > 0 ? out[out.length - 1] : null;
610
+ if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
611
+ runSteps++;
612
+ out[out.length - 1] = { ...req, stepCount: runSteps };
613
+ } else {
614
+ runSteps = 1;
615
+ out.push({ ...req, stepCount: 1 });
616
+ }
258
617
  }
259
- function partsOf(breakdown) {
260
- return CATS.map((c) => {
261
- return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
262
- });
618
+ return out;
619
+ }
620
+ function attachMarkers(requests, events) {
621
+ const markers = new Array(requests.length);
622
+ for (const ev of events) {
623
+ if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
624
+ for (let r = 0; r < requests.length; r++) {
625
+ if (requests[r].seq >= ev.seq) {
626
+ if (markers[r] === void 0) markers[r] = ev;
627
+ break;
628
+ }
629
+ }
263
630
  }
631
+ return markers;
632
+ }
633
+ function makeTrendChart(kit) {
634
+ const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
264
635
  const CHART_H = 112;
265
636
  const BAR_W = 14;
266
637
  const BAR_GAP = 2;
267
638
  const TURN_COLORS = ["#6366f1", "#f59e0b", "#22c55e", "#a855f7", "#3b82f6", "#14b8a6", "#ef4444", "#ec4899"];
268
- function TrendChart(props) {
639
+ return function TrendChart(props) {
269
640
  const requests = props.requests;
641
+ const markers = props.markers;
270
642
  let maxTotal = 1;
271
643
  for (const req of requests) if (req.total > maxTotal) maxTotal = req.total;
272
- const markers = {};
273
- for (const ev of props.events) {
274
- if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
275
- for (let r = 0; r < requests.length; r++) {
276
- if (requests[r].seq >= ev.seq) {
277
- if (!markers[r]) markers[r] = ev;
278
- break;
279
- }
280
- }
281
- }
282
644
  const groups = [];
283
645
  for (const req of requests) {
284
646
  let grp = groups.length > 0 ? groups[groups.length - 1] : null;
@@ -293,10 +655,14 @@ function makeView(ctx, t) {
293
655
  const scrolledOnce = React.useRef(false);
294
656
  const lastGranRef = React.useRef(props.granularity);
295
657
  const [edges, setEdges] = React.useState({ left: false, right: false });
658
+ const edgesRef = React.useRef(edges);
296
659
  const updateEdges = (el) => {
297
660
  const left = el.scrollLeft > 4;
298
661
  const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
299
- setEdges((prev) => prev.left === left && prev.right === right ? prev : { left, right });
662
+ const prev = edgesRef.current;
663
+ if (prev.left === left && prev.right === right) return;
664
+ edgesRef.current = { left, right };
665
+ setEdges({ left, right });
300
666
  };
301
667
  React.useLayoutEffect(() => {
302
668
  const el = scrollRef.current;
@@ -319,8 +685,8 @@ function makeView(ctx, t) {
319
685
  h(
320
686
  "div",
321
687
  { className: "lc-axis" },
322
- h("span", { className: "lc-axis-top" }, fmt(maxTotal)),
323
- h("span", { className: "lc-axis-mid" }, fmt(Math.round(maxTotal / 2))),
688
+ h("span", { className: "lc-axis-top" }, fmt3(maxTotal)),
689
+ h("span", { className: "lc-axis-mid" }, fmt3(Math.round(maxTotal / 2))),
324
690
  h("span", { className: "lc-axis-bot" }, "0")
325
691
  ),
326
692
  h(
@@ -345,10 +711,12 @@ function makeView(ctx, t) {
345
711
  h("div", { className: "lc-grid lc-grid-top" }),
346
712
  h("div", { className: "lc-grid lc-grid-mid" }),
347
713
  requests.map((req, i) => {
714
+ const marker = markers[i];
715
+ const markerAt = marker !== void 0 ? eventAt(marker) : null;
348
716
  const selected = props.selectedSeq === req.seq;
349
717
  const hovered = props.hoveredSeq === req.seq;
350
718
  const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
351
- const tip = (req.stepCount !== void 0 && req.stepCount > 1 ? tr("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 })) + " \xB7 " + fmtTime(req.time) + "\n" + tr("tip.total", { n: fmt(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt(req[c.key] || 0)).join(" / ");
719
+ const tip = (req.stepCount !== void 0 && req.stepCount > 1 ? tr("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 })) + " \xB7 " + fmtTime2(req.time) + "\n" + tr("tip.total", { n: fmt3(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt3(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt3(req[c.key] || 0)).join(" / ");
352
720
  return h(
353
721
  "div",
354
722
  {
@@ -365,7 +733,12 @@ function makeView(ctx, t) {
365
733
  props.onHover(req.seq);
366
734
  }
367
735
  },
368
- markers[i] ? h("span", { className: "lc-bar-marker", title: eventLabel(markers[i]) }, "\u2702") : null,
736
+ // The tooltip names the event AND where it happened: the
737
+ // gap between the request before and the request after.
738
+ marker !== void 0 ? h("span", {
739
+ className: "lc-bar-marker",
740
+ title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
741
+ }, "\u2702") : null,
369
742
  h(
370
743
  "div",
371
744
  { className: "lc-bar-stack" },
@@ -404,105 +777,27 @@ function makeView(ctx, t) {
404
777
  }
405
778
  }, "T" + grp.turn);
406
779
  })
407
- ),
408
- edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
409
- )
410
- );
411
- }
412
- function RequestDetail(props) {
413
- const req = props.request;
414
- if (!req) return null;
415
- const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
416
- const head = isTurn ? tr("detail.turn", { t: req.turn ?? 0, n: req.stepCount ?? 0 }) : tr("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 });
417
- return h(
418
- "div",
419
- { className: "lc-detail" },
420
- h(
421
- "div",
422
- { className: "lc-detail-head" },
423
- h("b", null, head),
424
- isTurn ? h("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null,
425
- h("span", null, fmtTime(req.time)),
426
- h("span", null, tr("detail.estTotal", { n: fmt(req.total) })),
427
- req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt(req.prompt) })) : null,
428
- req.output !== void 0 ? h("span", null, tr("detail.output", { n: fmt(req.output) })) : null
780
+ )
429
781
  ),
430
- h(StackedBar, { parts: partsOf(req), height: 10 }),
431
- h(
432
- "div",
433
- { className: "lc-detail-rows" },
434
- CATS.map((c) => {
435
- const v = req[c.key] || 0;
436
- return h(
437
- "div",
438
- { key: c.key, className: "lc-detail-row" },
439
- h("i", { style: { background: c.color } }),
440
- h("span", { className: "lc-detail-label" }, catLabel(c.key)),
441
- h(
442
- "span",
443
- { className: "lc-bar-track" },
444
- h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
445
- ),
446
- h("span", { className: "lc-detail-num" }, fmt(v)),
447
- h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
448
- );
449
- })
450
- )
451
- );
452
- }
453
- function EventList(props) {
454
- if (props.events.length === 0) {
455
- return h("div", { className: "lc-empty" }, t("events.empty"));
456
- }
457
- const sorted = props.events.slice().reverse();
458
- return h(
459
- "div",
460
- { className: "lc-events" },
461
- sorted.map((ev, i) => {
462
- const label = eventLabel(ev);
463
- return h(
464
- "div",
465
- { key: ev.seq + "-" + i, className: "lc-event" },
466
- h("span", { className: "lc-event-icon lc-event-" + ev.kind }, EVENT_ICONS[ev.kind] || "\u2022"),
467
- h("span", { className: "lc-event-label", title: label }, label),
468
- ev.tokens ? h(
469
- "span",
470
- { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") },
471
- (ev.kind === "inject" ? "+" : "\u2212") + fmt(ev.tokens)
472
- ) : null,
473
- h("span", { className: "lc-event-time" }, fmtTime(ev.time))
474
- );
475
- })
476
- );
477
- }
478
- function NodeList(props) {
479
- if (props.nodes.length === 0) {
480
- return h("div", { className: "lc-empty" }, t("nodes.empty"));
481
- }
482
- const catColor = {};
483
- for (const c of CATS) catColor[c.key] = c.color;
484
- const rows = props.nodes.slice().reverse();
485
- return h(
486
- "div",
487
- { className: "lc-nodes" },
488
- props.dropped > 0 ? h("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null,
489
- rows.map((n) => {
490
- const text = nodeText(n);
491
- return h(
492
- "div",
493
- { key: n.seq, className: "lc-node" },
494
- h("i", { style: { background: catColor[n.cat] || "#999" } }),
495
- h("span", { className: "lc-node-preview", title: text }, text),
496
- // Timestamp when the host event carried one.
497
- typeof n.time === "number" ? h("span", { className: "lc-node-time" }, fmtTime(n.time)) : null,
498
- h("span", { className: "lc-node-tokens" }, fmt(n.tokens))
499
- );
500
- })
782
+ edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
501
783
  );
502
- }
503
- function ContextView(props) {
784
+ };
785
+ }
786
+
787
+ // src/client/components/contextView.ts
788
+ function makeContextView(ctx, kit) {
789
+ const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
790
+ const StackedBar = makeStackedBar(kit);
791
+ const Legend = makeLegend(kit);
792
+ const TrendChart = makeTrendChart(kit);
793
+ const RequestDetail = makeRequestDetail(kit, StackedBar);
794
+ const EventList = makeEventList(kit);
795
+ const NodeList = makeNodeList(kit);
796
+ const StatsBoard = makeStatsBoard(kit);
797
+ return function ContextView(props) {
504
798
  const sessionId = props.sessionId;
505
- const [data, setData] = React.useState(null);
799
+ const initial = typeof sessionId === "string" && sessionId !== "" ? cacheGet(sessionId) ?? null : null;
800
+ const [data, setData] = React.useState(initial);
506
801
  const [error, setError] = React.useState(null);
507
802
  const [selectedSeq, setSelectedSeq] = React.useState(null);
508
803
  const [hoveredSeq, setHoveredSeq] = React.useState(null);
@@ -510,6 +805,10 @@ function makeView(ctx, t) {
510
805
  const [tick, setTick] = React.useState(0);
511
806
  const [granularity, setGranularity] = React.useState("step");
512
807
  const [hoverCat, setHoverCat] = React.useState(null);
808
+ const dataRef = React.useRef(initial);
809
+ React.useEffect(() => {
810
+ dataRef.current = data;
811
+ }, [data]);
513
812
  React.useEffect(() => {
514
813
  if (typeof sessionId !== "string" || sessionId === "") return void 0;
515
814
  let alive = true;
@@ -517,18 +816,31 @@ function makeView(ctx, t) {
517
816
  ctx.connection.rpc.call("/dsh-context", "snapshot", { sessionId }).then((res) => {
518
817
  if (!alive) return;
519
818
  if (res && res.ok) {
520
- setData(res.value);
819
+ const snap = res.value;
820
+ cachePut(sessionId, snap);
821
+ setData(snap);
521
822
  setError(null);
522
- } else setError(res && res.error ? String(res.error.message || res.error.code) : "failed");
823
+ } else if (dataRef.current === null) {
824
+ setError(res && res.error ? String(res.error.message || res.error.code) : "failed");
825
+ }
523
826
  }, (err) => {
524
- if (alive) setError(String(err instanceof Error ? err.message : err));
827
+ if (alive && dataRef.current === null) {
828
+ setError(String(err instanceof Error ? err.message : err));
829
+ }
525
830
  });
526
831
  };
527
832
  load();
528
- const timerId = setInterval(load, 2e3);
833
+ const timerId = setInterval(() => {
834
+ if (document.visibilityState !== "hidden") load();
835
+ }, 2e3);
836
+ const onVisible = () => {
837
+ if (document.visibilityState === "visible") load();
838
+ };
839
+ document.addEventListener("visibilitychange", onVisible);
529
840
  return () => {
530
841
  alive = false;
531
842
  clearInterval(timerId);
843
+ document.removeEventListener("visibilitychange", onVisible);
532
844
  };
533
845
  }, [sessionId]);
534
846
  React.useEffect(() => {
@@ -548,6 +860,11 @@ function makeView(ctx, t) {
548
860
  const events = data.events || [];
549
861
  const nodes = data.nodes || [];
550
862
  const displayRequests = granularity === "turn" ? aggregateByTurn(requests) : requests;
863
+ const markers = attachMarkers(displayRequests, events);
864
+ const markerOf = (req) => {
865
+ const i = displayRequests.indexOf(req);
866
+ return i >= 0 ? markers[i] : void 0;
867
+ };
551
868
  let pinnedReq = null;
552
869
  for (const req of displayRequests) if (req.seq === selectedSeq) pinnedReq = req;
553
870
  let activeReq = null;
@@ -567,6 +884,8 @@ function makeView(ctx, t) {
567
884
  return h(
568
885
  "div",
569
886
  { className: "lc-root" },
887
+ // ---- session context stats (over the retained window) ----
888
+ h(StatsBoard, { requests, events }),
570
889
  // ---- overview ----
571
890
  h(
572
891
  "div",
@@ -584,8 +903,8 @@ function makeView(ctx, t) {
584
903
  h(
585
904
  "div",
586
905
  { className: "lc-overview-num" },
587
- h("b", null, fmt(current.total)),
588
- h("span", null, data.contextWindow ? " / " + fmt(data.contextWindow) + " " + tr("overview.ofWindow", { p: windowPct ?? 0 }) : " " + t("overview.estimate")),
906
+ h("b", null, fmt3(current.total)),
907
+ h("span", null, data.contextWindow ? " / " + fmt3(data.contextWindow) + " " + tr("overview.ofWindow", { p: windowPct ?? 0 }) : " " + t("overview.estimate")),
589
908
  // The provider-reported prompt of the last request is the best
590
909
  // ground truth for what the model actually received; the fixed
591
910
  // density heuristic can undercount CJK-heavy content, so show the
@@ -593,7 +912,7 @@ function makeView(ctx, t) {
593
912
  displayRequests.length > 0 && displayRequests[displayRequests.length - 1].prompt !== void 0 ? h(
594
913
  "span",
595
914
  { className: "lc-actual" },
596
- tr("overview.actual", { n: fmt(displayRequests[displayRequests.length - 1].prompt ?? 0) })
915
+ tr("overview.actual", { n: fmt3(displayRequests[displayRequests.length - 1].prompt ?? 0) })
597
916
  ) : null
598
917
  ),
599
918
  h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
@@ -603,7 +922,7 @@ function makeView(ctx, t) {
603
922
  { className: "lc-tools" },
604
923
  t("tools.top"),
605
924
  data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
606
- return h("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt(tool.tokens));
925
+ return h("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
607
926
  }),
608
927
  data.toolList.length > 5 ? h("span", { className: "lc-card-sub" }, " " + tr("tools.more", { n: data.toolList.length })) : null
609
928
  ) : null
@@ -644,7 +963,7 @@ function makeView(ctx, t) {
644
963
  // The host caps the log at 160 requests; render them ALL so
645
964
  // earlier turns/steps stay reachable via horizontal scroll.
646
965
  requests: displayRequests,
647
- events,
966
+ markers,
648
967
  selectedSeq: pinnedReq ? pinnedReq.seq : null,
649
968
  hoveredSeq,
650
969
  activeTurn,
@@ -653,7 +972,7 @@ function makeView(ctx, t) {
653
972
  onHover: setHoveredSeq,
654
973
  onHoverTurn: setHoverTurn
655
974
  }),
656
- h(RequestDetail, { request: activeReq })
975
+ h(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })
657
976
  )
658
977
  ),
659
978
  // ---- events + messages ----
@@ -680,94 +999,24 @@ function makeView(ctx, t) {
680
999
  ),
681
1000
  h("div", { className: "lc-foot" }, t("footer"))
682
1001
  );
683
- }
684
- return ContextView;
1002
+ };
685
1003
  }
686
- const STYLES = [
687
- ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
688
- ".lc-card { background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }",
689
- ".lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }",
690
- ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
691
- ".lc-gran { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
692
- ".lc-gran-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; }",
693
- ".lc-gran-btn:hover { color: var(--dsw-alias-label-primary); }",
694
- ".lc-gran-on, .lc-gran-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }",
695
- ".lc-overview-num { margin-bottom: 8px; }",
696
- ".lc-overview-num b { font-size: 20px; }",
697
- ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
698
- ".lc-stacked-wrap { position: relative; width: 100%; }",
699
- ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }",
700
- ".lc-bar-tip { position: absolute; bottom: calc(100% + 6px); transform: translateX(-50%); z-index: 5; white-space: nowrap; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; }",
701
- ".lc-stacked > div { height: 100%; }",
702
- ".lc-stacked-seg-on { filter: brightness(1.18); }",
703
- ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
704
- ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
705
- ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
706
- ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
707
- ".lc-chip-on { font-weight: 600; }",
708
- ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
709
- ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
710
- ".lc-tool-chip { background: var(--dsw-alias-bg-layer-2); border-radius: 4px; padding: 1px 7px; font-size: 12px; color: var(--dsw-alias-label-primary); }",
711
- ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
712
- ".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
713
- ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
714
- ".lc-axis-top { top: 13px; }",
715
- ".lc-axis-mid { top: 69px; }",
716
- ".lc-axis-bot { top: 125px; }",
717
- ".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
718
- ".lc-chart-scroll::-webkit-scrollbar { height: 8px; }",
719
- ".lc-chart-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 45%, transparent); border-radius: 4px; }",
720
- ".lc-chart-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 70%, transparent); }",
721
- ".lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }",
722
- ".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
723
- ".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
724
- ".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
725
- ".lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }",
726
- ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
727
- ".lc-grid-top { top: 18px; }",
728
- ".lc-grid-mid { top: 74px; }",
729
- ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }",
730
- ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
731
- ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
732
- ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
733
- ".lc-bar-in-turn { background: rgba(99,102,241,0.14); }",
734
- ".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
735
- ".lc-bar-stack > div { width: 100%; }",
736
- ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
737
- ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
738
- ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms; }",
739
- ".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
740
- ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
741
- ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
742
- ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
743
- ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
744
- ".lc-detail-tag { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); }",
745
- ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
746
- ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
747
- ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
748
- ".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
749
- ".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
750
- ".lc-detail-num { width: 44px; text-align: right; }",
751
- ".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
752
- ".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; }",
753
- ".lc-col { flex: 1; min-width: 280px; }",
754
- ".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
755
- ".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
756
- ".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
757
- ".lc-event-icon.lc-event-inject { color: #a855f7; }",
758
- ".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
759
- ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
760
- ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); }",
761
- ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
762
- ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
763
- ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
764
- ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
765
- ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
766
- ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
767
- ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
768
- ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
769
- ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }"
770
- ].join("\n");
1004
+
1005
+ // src/client/viewkit.ts
1006
+ function makeViewKit(t) {
1007
+ const { eventLabel, eventAt } = makeEventText(t, t);
1008
+ return {
1009
+ t,
1010
+ tr: t,
1011
+ fmt,
1012
+ fmtTime,
1013
+ catLabel: (key) => t("cat." + key),
1014
+ eventLabel,
1015
+ eventAt
1016
+ };
1017
+ }
1018
+
1019
+ // src/client/index.ts
771
1020
  function apply(ctx) {
772
1021
  ctx.effect(() => {
773
1022
  return ctx.locale.register("dsh-context", { zh: DICT_ZH, en: DICT_EN });
@@ -782,7 +1031,7 @@ function apply(ctx) {
782
1031
  if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
783
1032
  };
784
1033
  }, "dsh-context: styles");
785
- const ContextView = makeView(ctx, t);
1034
+ const ContextView = makeContextView(ctx, makeViewKit(t));
786
1035
  ctx.slots.inject("conversation.view", () => {
787
1036
  return ctx.slots.register(
788
1037
  // order 20 renders right of Chat (0) and Trajectory (10).
package/lib/index.js CHANGED
@@ -1,8 +1,10 @@
1
- const name = "dsh-context";
2
- const inject = ["connection"];
3
- const CHARS_PER_TOKEN = 4;
4
- const BLOCK_OVERHEAD = 4;
5
- const ROLE_OVERHEAD = 4;
1
+ // src/host/pricing.ts
2
+ var CHARS_PER_TOKEN = 4;
3
+ var BLOCK_OVERHEAD = 4;
4
+ var ROLE_OVERHEAD = 4;
5
+ function estimateToolsTotal(tools) {
6
+ return tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD : 0;
7
+ }
6
8
  function estimateBlocks(blocks) {
7
9
  let tokens = 0;
8
10
  if (!Array.isArray(blocks)) return 0;
@@ -58,8 +60,10 @@ function toolCallNames(blocks) {
58
60
  function isInjection(source) {
59
61
  return source !== null && typeof source === "object" && (source.kind === "plugin" || source.kind === "skill-invocation" || typeof source.form === "string");
60
62
  }
61
- const MAX_REQUEST_STEPS = 1500;
62
- const MAX_KEPT_TURNS = 300;
63
+
64
+ // src/host/fold.ts
65
+ var MAX_REQUEST_STEPS = 1500;
66
+ var MAX_KEPT_TURNS = 300;
63
67
  function trimToLastTurns(requests, maxTurns) {
64
68
  let runs = 0;
65
69
  let start = requests.length;
@@ -178,7 +182,7 @@ function foldInto(st, events) {
178
182
  name: typeof t.name === "string" ? t.name : "?",
179
183
  tokens: estimateToolSchema(t)
180
184
  }));
181
- st.toolsTokens = tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD : 0;
185
+ st.toolsTokens = estimateToolsTotal(tools);
182
186
  st.systemTokens = estimateSystem(header.system);
183
187
  if (header.config && typeof header.config.model === "string") st.model = header.config.model;
184
188
  if (header.config && typeof header.config.provider === "string") st.provider = header.config.provider;
@@ -276,6 +280,8 @@ function foldInto(st, events) {
276
280
  }
277
281
  if (st.events.length > 400) st.events = st.events.slice(-400);
278
282
  }
283
+
284
+ // src/host/snapshot.ts
279
285
  function buildResult(st) {
280
286
  const surfaceTotal = st.sums.user + st.sums.inject + st.sums.assistant + st.sums.tool;
281
287
  const result = {
@@ -301,6 +307,20 @@ function buildResult(st) {
301
307
  const MAX_NODES = 200;
302
308
  result.droppedNodes = Math.max(0, st.surface.length - MAX_NODES);
303
309
  result.nodes = st.surface.slice(-MAX_NODES);
310
+ let ri = 0;
311
+ for (const ev of result.events) {
312
+ while (ri < result.requests.length && result.requests[ri].seq <= ev.seq) ri++;
313
+ const next = result.requests[ri];
314
+ const prev = ri > 0 ? result.requests[ri - 1] : void 0;
315
+ if (next !== void 0 && typeof next.turn === "number" && typeof next.step === "number") {
316
+ ev.turn = next.turn;
317
+ ev.step = next.step;
318
+ }
319
+ if (prev !== void 0 && typeof prev.turn === "number" && typeof prev.step === "number") {
320
+ ev.fromTurn = prev.turn;
321
+ ev.fromStep = prev.step;
322
+ }
323
+ }
304
324
  return result;
305
325
  }
306
326
  async function computeSnapshot(ctx, states, sessionId) {
@@ -331,6 +351,10 @@ async function computeSnapshot(ctx, states, sessionId) {
331
351
  st.result = buildResult(st.fold);
332
352
  return st.result;
333
353
  }
354
+
355
+ // src/host/index.ts
356
+ var name = "dsh-context";
357
+ var inject = ["connection"];
334
358
  function apply(ctx) {
335
359
  const states = /* @__PURE__ */ new Map();
336
360
  ctx.effect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Context insight panel for DeepSeek Harness: a Context tab (beside Chat/Trajectory) showing what the model's context window is made of and how it evolves — composition, per-request history, compactions, injections, and model switches.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {
@@ -27,9 +27,10 @@
27
27
  "scripts": {
28
28
  "build": "node scripts/build.mjs",
29
29
  "typecheck": "tsc --noEmit",
30
- "test": "npm run typecheck && node scripts/test-host.mjs && node scripts/test-client.mjs",
30
+ "test": "npm run typecheck && node tests/host.test.mjs && node tests/client.test.mjs",
31
31
  "release": "bash scripts/publish.sh",
32
- "prepare": "husky"
32
+ "prepare": "husky",
33
+ "repro": "node tests/repro-real-react.mjs"
33
34
  },
34
35
  "dsh": {
35
36
  "bundle": {