dsh-context 0.6.2 → 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,7 +18,18 @@ 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",
@@ -34,6 +45,9 @@ const DICT_ZH = {
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",
@@ -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,7 +87,18 @@ 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",
@@ -89,6 +114,9 @@ const DICT_EN = {
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",
@@ -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;
@@ -323,8 +685,8 @@ function makeView(ctx, t) {
323
685
  h(
324
686
  "div",
325
687
  { className: "lc-axis" },
326
- h("span", { className: "lc-axis-top" }, fmt(maxTotal)),
327
- 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))),
328
690
  h("span", { className: "lc-axis-bot" }, "0")
329
691
  ),
330
692
  h(
@@ -349,10 +711,12 @@ function makeView(ctx, t) {
349
711
  h("div", { className: "lc-grid lc-grid-top" }),
350
712
  h("div", { className: "lc-grid lc-grid-mid" }),
351
713
  requests.map((req, i) => {
714
+ const marker = markers[i];
715
+ const markerAt = marker !== void 0 ? eventAt(marker) : null;
352
716
  const selected = props.selectedSeq === req.seq;
353
717
  const hovered = props.hoveredSeq === req.seq;
354
718
  const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
355
- 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(" / ");
356
720
  return h(
357
721
  "div",
358
722
  {
@@ -369,7 +733,12 @@ function makeView(ctx, t) {
369
733
  props.onHover(req.seq);
370
734
  }
371
735
  },
372
- 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,
373
742
  h(
374
743
  "div",
375
744
  { className: "lc-bar-stack" },
@@ -408,105 +777,27 @@ function makeView(ctx, t) {
408
777
  }
409
778
  }, "T" + grp.turn);
410
779
  })
411
- ),
412
- edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
413
- )
414
- );
415
- }
416
- function RequestDetail(props) {
417
- const req = props.request;
418
- if (!req) return null;
419
- const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
420
- 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 });
421
- return h(
422
- "div",
423
- { className: "lc-detail" },
424
- h(
425
- "div",
426
- { className: "lc-detail-head" },
427
- h("b", null, head),
428
- isTurn ? h("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null,
429
- h("span", null, fmtTime(req.time)),
430
- h("span", null, tr("detail.estTotal", { n: fmt(req.total) })),
431
- req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt(req.prompt) })) : null,
432
- req.output !== void 0 ? h("span", null, tr("detail.output", { n: fmt(req.output) })) : null
780
+ )
433
781
  ),
434
- h(StackedBar, { parts: partsOf(req), height: 10 }),
435
- h(
436
- "div",
437
- { className: "lc-detail-rows" },
438
- CATS.map((c) => {
439
- const v = req[c.key] || 0;
440
- return h(
441
- "div",
442
- { key: c.key, className: "lc-detail-row" },
443
- h("i", { style: { background: c.color } }),
444
- h("span", { className: "lc-detail-label" }, catLabel(c.key)),
445
- h(
446
- "span",
447
- { className: "lc-bar-track" },
448
- h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
449
- ),
450
- h("span", { className: "lc-detail-num" }, fmt(v)),
451
- h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
452
- );
453
- })
454
- )
455
- );
456
- }
457
- function EventList(props) {
458
- if (props.events.length === 0) {
459
- return h("div", { className: "lc-empty" }, t("events.empty"));
460
- }
461
- const sorted = props.events.slice().reverse();
462
- return h(
463
- "div",
464
- { className: "lc-events" },
465
- sorted.map((ev, i) => {
466
- const label = eventLabel(ev);
467
- return h(
468
- "div",
469
- { key: ev.seq + "-" + i, className: "lc-event" },
470
- h("span", { className: "lc-event-icon lc-event-" + ev.kind }, EVENT_ICONS[ev.kind] || "\u2022"),
471
- h("span", { className: "lc-event-label", title: label }, label),
472
- ev.tokens ? h(
473
- "span",
474
- { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") },
475
- (ev.kind === "inject" ? "+" : "\u2212") + fmt(ev.tokens)
476
- ) : null,
477
- h("span", { className: "lc-event-time" }, fmtTime(ev.time))
478
- );
479
- })
480
- );
481
- }
482
- function NodeList(props) {
483
- if (props.nodes.length === 0) {
484
- return h("div", { className: "lc-empty" }, t("nodes.empty"));
485
- }
486
- const catColor = {};
487
- for (const c of CATS) catColor[c.key] = c.color;
488
- const rows = props.nodes.slice().reverse();
489
- return h(
490
- "div",
491
- { className: "lc-nodes" },
492
- props.dropped > 0 ? h("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null,
493
- rows.map((n) => {
494
- const text = nodeText(n);
495
- return h(
496
- "div",
497
- { key: n.seq, className: "lc-node" },
498
- h("i", { style: { background: catColor[n.cat] || "#999" } }),
499
- h("span", { className: "lc-node-preview", title: text }, text),
500
- // Timestamp when the host event carried one.
501
- typeof n.time === "number" ? h("span", { className: "lc-node-time" }, fmtTime(n.time)) : null,
502
- h("span", { className: "lc-node-tokens" }, fmt(n.tokens))
503
- );
504
- })
782
+ edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
505
783
  );
506
- }
507
- 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) {
508
798
  const sessionId = props.sessionId;
509
- const [data, setData] = React.useState(null);
799
+ const initial = typeof sessionId === "string" && sessionId !== "" ? cacheGet(sessionId) ?? null : null;
800
+ const [data, setData] = React.useState(initial);
510
801
  const [error, setError] = React.useState(null);
511
802
  const [selectedSeq, setSelectedSeq] = React.useState(null);
512
803
  const [hoveredSeq, setHoveredSeq] = React.useState(null);
@@ -514,6 +805,10 @@ function makeView(ctx, t) {
514
805
  const [tick, setTick] = React.useState(0);
515
806
  const [granularity, setGranularity] = React.useState("step");
516
807
  const [hoverCat, setHoverCat] = React.useState(null);
808
+ const dataRef = React.useRef(initial);
809
+ React.useEffect(() => {
810
+ dataRef.current = data;
811
+ }, [data]);
517
812
  React.useEffect(() => {
518
813
  if (typeof sessionId !== "string" || sessionId === "") return void 0;
519
814
  let alive = true;
@@ -521,18 +816,31 @@ function makeView(ctx, t) {
521
816
  ctx.connection.rpc.call("/dsh-context", "snapshot", { sessionId }).then((res) => {
522
817
  if (!alive) return;
523
818
  if (res && res.ok) {
524
- setData(res.value);
819
+ const snap = res.value;
820
+ cachePut(sessionId, snap);
821
+ setData(snap);
525
822
  setError(null);
526
- } 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
+ }
527
826
  }, (err) => {
528
- 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
+ }
529
830
  });
530
831
  };
531
832
  load();
532
- 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);
533
840
  return () => {
534
841
  alive = false;
535
842
  clearInterval(timerId);
843
+ document.removeEventListener("visibilitychange", onVisible);
536
844
  };
537
845
  }, [sessionId]);
538
846
  React.useEffect(() => {
@@ -552,6 +860,11 @@ function makeView(ctx, t) {
552
860
  const events = data.events || [];
553
861
  const nodes = data.nodes || [];
554
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
+ };
555
868
  let pinnedReq = null;
556
869
  for (const req of displayRequests) if (req.seq === selectedSeq) pinnedReq = req;
557
870
  let activeReq = null;
@@ -571,6 +884,8 @@ function makeView(ctx, t) {
571
884
  return h(
572
885
  "div",
573
886
  { className: "lc-root" },
887
+ // ---- session context stats (over the retained window) ----
888
+ h(StatsBoard, { requests, events }),
574
889
  // ---- overview ----
575
890
  h(
576
891
  "div",
@@ -588,8 +903,8 @@ function makeView(ctx, t) {
588
903
  h(
589
904
  "div",
590
905
  { className: "lc-overview-num" },
591
- h("b", null, fmt(current.total)),
592
- 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")),
593
908
  // The provider-reported prompt of the last request is the best
594
909
  // ground truth for what the model actually received; the fixed
595
910
  // density heuristic can undercount CJK-heavy content, so show the
@@ -597,7 +912,7 @@ function makeView(ctx, t) {
597
912
  displayRequests.length > 0 && displayRequests[displayRequests.length - 1].prompt !== void 0 ? h(
598
913
  "span",
599
914
  { className: "lc-actual" },
600
- tr("overview.actual", { n: fmt(displayRequests[displayRequests.length - 1].prompt ?? 0) })
915
+ tr("overview.actual", { n: fmt3(displayRequests[displayRequests.length - 1].prompt ?? 0) })
601
916
  ) : null
602
917
  ),
603
918
  h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
@@ -607,7 +922,7 @@ function makeView(ctx, t) {
607
922
  { className: "lc-tools" },
608
923
  t("tools.top"),
609
924
  data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
610
- 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));
611
926
  }),
612
927
  data.toolList.length > 5 ? h("span", { className: "lc-card-sub" }, " " + tr("tools.more", { n: data.toolList.length })) : null
613
928
  ) : null
@@ -648,7 +963,7 @@ function makeView(ctx, t) {
648
963
  // The host caps the log at 160 requests; render them ALL so
649
964
  // earlier turns/steps stay reachable via horizontal scroll.
650
965
  requests: displayRequests,
651
- events,
966
+ markers,
652
967
  selectedSeq: pinnedReq ? pinnedReq.seq : null,
653
968
  hoveredSeq,
654
969
  activeTurn,
@@ -657,7 +972,7 @@ function makeView(ctx, t) {
657
972
  onHover: setHoveredSeq,
658
973
  onHoverTurn: setHoverTurn
659
974
  }),
660
- h(RequestDetail, { request: activeReq })
975
+ h(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })
661
976
  )
662
977
  ),
663
978
  // ---- events + messages ----
@@ -684,94 +999,24 @@ function makeView(ctx, t) {
684
999
  ),
685
1000
  h("div", { className: "lc-foot" }, t("footer"))
686
1001
  );
687
- }
688
- return ContextView;
1002
+ };
689
1003
  }
690
- const STYLES = [
691
- ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
692
- ".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; }",
693
- ".lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }",
694
- ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
695
- ".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; }",
696
- ".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; }",
697
- ".lc-gran-btn:hover { color: var(--dsw-alias-label-primary); }",
698
- ".lc-gran-on, .lc-gran-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }",
699
- ".lc-overview-num { margin-bottom: 8px; }",
700
- ".lc-overview-num b { font-size: 20px; }",
701
- ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
702
- ".lc-stacked-wrap { position: relative; width: 100%; }",
703
- ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }",
704
- ".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; }",
705
- ".lc-stacked > div { height: 100%; }",
706
- ".lc-stacked-seg-on { filter: brightness(1.18); }",
707
- ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
708
- ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
709
- ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
710
- ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
711
- ".lc-chip-on { font-weight: 600; }",
712
- ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
713
- ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
714
- ".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); }",
715
- ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
716
- ".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
717
- ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
718
- ".lc-axis-top { top: 13px; }",
719
- ".lc-axis-mid { top: 69px; }",
720
- ".lc-axis-bot { top: 125px; }",
721
- ".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
722
- ".lc-chart-scroll::-webkit-scrollbar { height: 8px; }",
723
- ".lc-chart-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 45%, transparent); border-radius: 4px; }",
724
- ".lc-chart-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 70%, transparent); }",
725
- ".lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }",
726
- ".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
727
- ".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
728
- ".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
729
- ".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%; }",
730
- ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
731
- ".lc-grid-top { top: 18px; }",
732
- ".lc-grid-mid { top: 74px; }",
733
- ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }",
734
- ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
735
- ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
736
- ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
737
- ".lc-bar-in-turn { background: rgba(99,102,241,0.14); }",
738
- ".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
739
- ".lc-bar-stack > div { width: 100%; }",
740
- ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
741
- ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
742
- ".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; }",
743
- ".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
744
- ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
745
- ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
746
- ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
747
- ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
748
- ".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); }",
749
- ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
750
- ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
751
- ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
752
- ".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
753
- ".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
754
- ".lc-detail-num { width: 44px; text-align: right; }",
755
- ".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
756
- ".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; }",
757
- ".lc-col { flex: 1; min-width: 280px; }",
758
- ".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
759
- ".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
760
- ".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
761
- ".lc-event-icon.lc-event-inject { color: #a855f7; }",
762
- ".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
763
- ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
764
- ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); }",
765
- ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
766
- ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
767
- ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
768
- ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
769
- ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
770
- ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
771
- ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
772
- ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
773
- ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }"
774
- ].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
775
1020
  function apply(ctx) {
776
1021
  ctx.effect(() => {
777
1022
  return ctx.locale.register("dsh-context", { zh: DICT_ZH, en: DICT_EN });
@@ -786,7 +1031,7 @@ function apply(ctx) {
786
1031
  if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
787
1032
  };
788
1033
  }, "dsh-context: styles");
789
- const ContextView = makeView(ctx, t);
1034
+ const ContextView = makeContextView(ctx, makeViewKit(t));
790
1035
  ctx.slots.inject("conversation.view", () => {
791
1036
  return ctx.slots.register(
792
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.2",
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": {