dsh-context 0.6.2 → 0.7.1

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
@@ -1,90 +1,56 @@
1
1
  # dsh-context
2
2
 
3
- A **Context insight panel** for [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness) (dsh): a plugin that adds a **Context** tab to the web UI — right beside **Chat** and **Trajectory** — so you can see what the model's context window is actually made of, and how it evolves across the conversation.
3
+ [![npm version](https://img.shields.io/npm/v/dsh-context)](https://www.npmjs.com/package/dsh-context)
4
+ [![GitHub stars](https://img.shields.io/github/stars/bowenliang123/dsh-context?style=social)](https://github.com/bowenliang123/dsh-context)
4
5
 
5
- ![dsh-context screenshot](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/screenshot.png)
6
+ **See what your DeepSeek Harness agent's context window is actually made of and how it evolves.**
6
7
 
7
- ## Quick install
8
+ `dsh-context` is a [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness) plugin that adds a **Context Insight** panel to the web UI: a live, visual answer to *"what is the model carrying right now, and how did it get there?"* — context composition, per-request history, compactions, and injections, all in one place.
8
9
 
9
- One command, from any [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness) installation (the `web` profile is the one `dsh web` boots; it is created automatically on first use):
10
+ ![Context panel overview](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-overview.png)
10
11
 
11
- ```sh
12
- dsh plugin --profile web add dsh-context
13
- ```
12
+ ## Install
14
13
 
15
- then start the web UI and open any session the **上下文 / Context** tab appears right beside **Chat** and **Trajectory**:
14
+ One command, from any DeepSeek Harness installation:
16
15
 
17
16
  ```sh
18
- dsh web
17
+ dsh plugin --profile web add dsh-context
19
18
  ```
20
19
 
21
- > Running dsh from a source checkout instead of the installed CLI? Prefix the commands with `pnpm` (`pnpm dsh plugin --profile web add dsh-context`).
22
-
23
- ## Why
24
-
25
- Every model request packs the same window from six sources: the system prompt, tool schemas, your messages, injected context (skills, AGENTS.md, runtime snapshots), assistant replies, and tool results. When a conversation degrades or gets compacted, *which part ate the budget* is usually invisible. dsh-context makes it observable:
20
+ Then start the web UI with `dsh web`, open any session, and click the **上下文 / Context** tab. No build step, no restart.
26
21
 
27
- - **Current composition** — a stacked bar of the six categories, scaled against the model's context window (the gray track is your remaining headroom), plus the top-5 most expensive tool schemas.
28
- - **History** — one stacked bar per model request (finer than per-turn), with Y-axis ticks and gridlines. Click any bar for its full breakdown, including the **provider-reported** prompt/output tokens next to the estimate. ✂ marks where compaction/pruning happened — watch the bars drop.
29
- - **Context events** — compactions, tool-output prunes, skill injections (`Skill injected (code-review)`), plugin context injections, model switches — each with its token delta and timestamp.
30
- - **Messages** — the currently model-visible surface, message by message, with per-message token costs.
22
+ ## What you'll see
31
23
 
32
- The UI is bilingual (中文/English) and follows the dsh locale automatically.
24
+ ### 📊 Context stats the session at a glance
33
25
 
34
- ## Install
35
-
36
- dsh-context ships as a **dsh bundle**: an npm package with a `dsh.bundle` manifest (a `cordis.patch.yml` layer that inserts the plugin row) and a `dsh.client` manifest (the web UI half). No build step, no restart — the one-liner above installs it into the `web` profile (or any other profile you boot with `dsh --profile <name>`). The `dsh-context` loader row activates the host half, and the web app picks up the package's `./client` bundle and adds the **上下文 / Context** tab to every session view.
26
+ Turns, steps, how much context has been recycled by compactions and prunes, how many injections happened, model switches, and the estimated total tokens sent — next to the provider-reported actuals, so you can see how the estimate holds up.
37
27
 
38
- To install from this checkout instead (for development), from the repo root:
28
+ ### 🧱 Current composition what's in the window right now
39
29
 
40
- ```sh
41
- dsh plugin --profile <name> add .
42
- ```
30
+ A six-color stacked bar scaled against the model's full context window (the gray track is your remaining headroom): system prompt, tool schemas, your messages, injected context, assistant replies, and tool results — plus the top-5 most expensive tool schemas. When a conversation starts degrading, this is where you find out *which part ate the budget*.
43
31
 
44
- If dsh is run from a source checkout, prefix the commands with `pnpm` (`pnpm dsh plugin ...`).
32
+ ### 📈 History watch the window grow (and get compacted)
45
33
 
46
- ## Usage
34
+ One stacked bar per model request, finer than per-message. Toggle between **Turn** and **Step** granularity, scroll sideways through the session, hover any bar for a quick tooltip, and click to pin the full breakdown — including provider-reported actual prompt/output tokens next to the estimate. **✂ marks where compaction or pruning happened** — watch the bars drop:
47
35
 
48
- Open any session and click **上下文 / Context** (to the right of Chat and Trajectory). Data refreshes every 2 seconds while the tab is open; switching sessions switches the view to that session's log — including historical, persisted sessions.
36
+ ![History chart with a pinned request](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/history-detail.png)
49
37
 
50
- - **Hover** a history bar for a quick tooltip; **click** it to pin the breakdown below the chart.
51
- - The overview bar is scaled to the model's context window, so ~13% full means ~13% of the window is spoken for.
52
- - Numbers are estimates using the *same fixed-density heuristic as dsh's built-in tokenMeter* (~4 chars ≈ 1 token), so they match the harness's own stats. Wherever the provider reported real usage, it's shown alongside as "actual".
38
+ Above: a real session that grew to ~563k tokens across 48 turns, then compaction (✂) recycled −535.5k in one step, and the conversation continued from a fresh, small window.
53
39
 
54
- ## How it works
40
+ ### Context events — when and why the window changed
55
41
 
56
- - **Data source**: the session's durable event log. Live sessions are folded straight from the in-memory log (`sessions.get(id).events` no clone, no disk parse); persisted sessions fall back to `sessionQuery.readSession`.
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
- - **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
- - **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.
42
+ Every compaction, tool-output prune, skill or plugin context injection, and model switch each with its token delta, turn/step attribution, and timestamp:
61
43
 
62
- ## Development
44
+ ![Context events and messages](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-events.png)
63
45
 
64
- The project is a pnpm workspace root (`pnpm-workspace.yaml`), so `pnpm install` never walks up into a parent workspace.
65
-
66
- ```sh
67
- pnpm install # devDependencies only — the plugin itself stays dependency-free
68
- pnpm run typecheck # tsc --noEmit (strict)
69
- pnpm run build # esbuild: lib/index.js (host) + lib/client.js (client bundle)
70
- pnpm test # typecheck + functional tests for both halves
71
- ```
46
+ ### 💬 Messages the currently model-visible surface
72
47
 
73
- `build.mjs` also smoke-checks the outputs (both halves must parse; the host half must import with the `name`/`inject`/`apply` plugin shape).
48
+ The exact message list the model sees right now, newest first, with a per-message token cost.
74
49
 
75
- Commits are guarded by a [husky](https://typicode.github.io/husky/) pre-commit hook that runs `pnpm run typecheck` (`tsc --noEmit`, strict).
76
50
 
77
- ## Files
51
+ ## Like it?
78
52
 
79
- | File | Role |
80
- | --- | --- |
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 |
83
- | `tsconfig.json` | Strict typecheck config (noEmit; esbuild does the transpiling) |
84
- | `package.json` | `dsh.bundle` (patch layer) + `dsh.client` (web UI) manifests |
85
- | `cordis.patch.yml` | The bundle's patch layer: inserts the `dsh-context` row |
86
- | `scripts/build.mjs` | esbuild-based build of `lib/index.js` + `lib/client.js` |
87
- | `docs/screenshot.png` | The UI in action |
53
+ If `dsh-context` helped you understand what your agent is carrying around, a ⭐ on [GitHub](https://github.com/bowenliang123/dsh-context) is much appreciated — and issues/PRs are welcome!
88
54
 
89
55
  ## License
90
56
 
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,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.6.2",
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.",
3
+ "version": "0.7.1",
4
+ "description": "Context insight panel for DeepSeek Harness: see 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": {
7
7
  "type": "git",
@@ -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": {