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 +32 -3
- package/lib/client.js +512 -267
- package/lib/index.js +32 -8
- package/package.json +4 -3
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
|
|
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/
|
|
82
|
-
| `src/
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
-
|
|
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) + " " +
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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" },
|
|
327
|
-
h("span", { className: "lc-axis-mid" },
|
|
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 " +
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
819
|
+
const snap = res.value;
|
|
820
|
+
cachePut(sessionId, snap);
|
|
821
|
+
setData(snap);
|
|
525
822
|
setError(null);
|
|
526
|
-
} else
|
|
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
|
|
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(
|
|
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,
|
|
592
|
-
h("span", null, data.contextWindow ? " / " +
|
|
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:
|
|
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 + " " +
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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 =
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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": {
|