dsh-context 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -3
- package/lib/client.js +527 -278
- 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,22 +18,36 @@ const DICT_ZH = {
|
|
|
18
18
|
"overview.title": "\u5F53\u524D\u6784\u6210",
|
|
19
19
|
"overview.ofWindow": "tokens\uFF08\u7EA6 {p}%\uFF09",
|
|
20
20
|
"overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
|
|
21
|
-
"overview.
|
|
21
|
+
"overview.free": "\u5269\u4F59\u7A97\u53E3",
|
|
22
|
+
"stats.title": "\u4E0A\u4E0B\u6587\u7EDF\u8BA1",
|
|
23
|
+
"stats.hint": "\u7EDF\u8BA1\u5F53\u524D\u4FDD\u7559\u7684\u5386\u53F2\u7A97\u53E3\uFF08\u4E0E\u8D8B\u52BF\u56FE\u4E00\u81F4\uFF09",
|
|
24
|
+
"stats.turns": "\u8F6E\u6B21",
|
|
25
|
+
"stats.steps": "\u6B65\u6570",
|
|
26
|
+
"stats.recycled": "\u2702 \u56DE\u6536",
|
|
27
|
+
"stats.recycleSub": "\u538B\u7F29 {c} \u6B21 \xB7 \u526A\u679D {p} \u6B21",
|
|
28
|
+
"stats.injects": "\uFF0B \u6CE8\u5165",
|
|
29
|
+
"stats.switches": "\u21C4 \u6A21\u578B\u5207\u6362",
|
|
30
|
+
"stats.est": "\u2248 \u7D2F\u8BA1\u4F30\u7B97",
|
|
31
|
+
"stats.actualPrompt": "\u5B9E\u9645 prompt",
|
|
32
|
+
"stats.output": "\u8F93\u51FA",
|
|
22
33
|
"tools.top": "\u5DE5\u5177\u5B9A\u4E49 Top\uFF1A",
|
|
23
34
|
"tools.more": "\u7B49 {n} \u4E2A",
|
|
24
35
|
"trend.title": "\u5386\u53F2\u8D8B\u52BF",
|
|
25
36
|
"gran.step": "\u6B65\u9AA4",
|
|
26
37
|
"gran.turn": "\u8F6E\u6B21",
|
|
27
|
-
"trend.hint": "\
|
|
38
|
+
"trend.hint": "\u60AC\u505C\u67F1\u5B50\u67E5\u770B\u8BE6\u60C5\uFF08\u70B9\u51FB\u56FA\u5B9A\uFF09\uFF0C\u2702 \u8868\u793A\u538B\u7F29/\u526A\u679D\uFF1B\u5DE6\u53F3\u6ED1\u52A8\u53EF\u56DE\u770B\u66F4\u65E9\uFF1BStep/Turn \u5207\u6362\u7C92\u5EA6",
|
|
28
39
|
"trend.empty": "\u53D1\u8D77\u4E00\u8F6E\u5BF9\u8BDD\u540E\uFF0C\u8FD9\u91CC\u4F1A\u5C55\u793A\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u7684\u4E0A\u4E0B\u6587\u6784\u6210",
|
|
29
|
-
"detail.step": "
|
|
30
|
-
"detail.turn": "
|
|
40
|
+
"detail.step": "\u7B2C {t} \u8F6E \xB7 \u7B2C {s} \u6B65",
|
|
41
|
+
"detail.turn": "\u7B2C {t} \u8F6E \xB7 \u5171 {n} \u6B65",
|
|
31
42
|
"detail.lastStep": "\u672B\u6B65",
|
|
32
43
|
"detail.estTotal": "\u4F30\u7B97\u5408\u8BA1 \u2248 {n}",
|
|
33
44
|
"detail.actual": "\u5B9E\u9645 prompt {n}",
|
|
34
45
|
"detail.output": "\u8F93\u51FA {n}",
|
|
35
46
|
"events.title": "\u4E0A\u4E0B\u6587\u4E8B\u4EF6",
|
|
36
47
|
"events.empty": "\u6682\u65E0\u4E0A\u4E0B\u6587\u4E8B\u4EF6\uFF08\u538B\u7F29\u3001\u6CE8\u5165\u3001\u6A21\u578B\u5207\u6362\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\uFF09",
|
|
48
|
+
"events.at": "\u7B2C {t} \u8F6E \xB7 \u7B2C {s} \u6B65",
|
|
49
|
+
"events.range": "\u7B2C {t} \u8F6E \xB7 \u7B2C {a}\u2192{b} \u6B65",
|
|
50
|
+
"events.rangeTo": "\u7B2C {a} \u8F6E \xB7 \u7B2C {as} \u6B65 \u2192 \u7B2C {b} \u8F6E \xB7 \u7B2C {bs} \u6B65",
|
|
37
51
|
"nodes.title": "\u6D88\u606F\u6784\u6210",
|
|
38
52
|
"nodes.hint": "\u5F53\u524D\u6A21\u578B\u53EF\u89C1\u7684\u6D88\u606F\uFF0C\u6700\u65B0\u5728\u524D",
|
|
39
53
|
"nodes.more": "\u2026 \u66F4\u65E9\u7684 {n} \u6761\u6D88\u606F\u5DF2\u7701\u7565",
|
|
@@ -41,8 +55,8 @@ const DICT_ZH = {
|
|
|
41
55
|
"loading": "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u2026",
|
|
42
56
|
"error": "\u4E0A\u4E0B\u6587\u6570\u636E\u8BFB\u53D6\u5931\u8D25\uFF1A",
|
|
43
57
|
"footer": "\u4F30\u7B97\u53E3\u5F84\uFF1A\u4E0E dsh \u5185\u7F6E tokenMeter \u76F8\u540C\u7684\u56FA\u5B9A\u5BC6\u5EA6\u542F\u53D1\u5F0F\uFF08\u7EA6 4 \u5B57\u7B26 \u2248 1 token\uFF09\uFF1B\u300C\u5B9E\u9645\u300D\u4E3A\u4F9B\u5E94\u5546\u4E0A\u62A5\u7528\u91CF\u3002",
|
|
44
|
-
"tip.step": "
|
|
45
|
-
"tip.turn": "
|
|
58
|
+
"tip.step": "\u7B2C {t} \u8F6E \xB7 \u7B2C{s}\u6B65",
|
|
59
|
+
"tip.turn": "\u7B2C {t} \u8F6E \xB7 \u5171 {n} \u6B65",
|
|
46
60
|
"tip.total": "\u5408\u8BA1 \u2248 {n}",
|
|
47
61
|
"tip.actual": "\uFF08\u5B9E\u9645 {n}\uFF09",
|
|
48
62
|
"ev.compaction": "\u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u6458\u8981\u66FF\u6362 {n} \u6761\u6D88\u606F\uFF09",
|
|
@@ -62,7 +76,7 @@ const DICT_ZH = {
|
|
|
62
76
|
"node.nonText": "(\u975E\u6587\u672C\u6D88\u606F)",
|
|
63
77
|
"node.snapshot": "\u5FEB\u7167: "
|
|
64
78
|
};
|
|
65
|
-
|
|
79
|
+
var DICT_EN = {
|
|
66
80
|
"tab": "Context",
|
|
67
81
|
"cat.system": "System",
|
|
68
82
|
"cat.tools": "Tool schemas",
|
|
@@ -73,22 +87,36 @@ const DICT_EN = {
|
|
|
73
87
|
"overview.title": "Current composition",
|
|
74
88
|
"overview.ofWindow": "tokens (~{p}%)",
|
|
75
89
|
"overview.estimate": "tokens (estimated)",
|
|
76
|
-
"overview.
|
|
90
|
+
"overview.free": "Free window",
|
|
91
|
+
"stats.title": "Context stats",
|
|
92
|
+
"stats.hint": "Over the retained history window (same as the History chart)",
|
|
93
|
+
"stats.turns": "Turns",
|
|
94
|
+
"stats.steps": "Steps",
|
|
95
|
+
"stats.recycled": "\u2702 Recycled",
|
|
96
|
+
"stats.recycleSub": "{c} compactions \xB7 {p} prunes",
|
|
97
|
+
"stats.injects": "\uFF0B Injections",
|
|
98
|
+
"stats.switches": "\u21C4 Model switches",
|
|
99
|
+
"stats.est": "\u2248 Estimated total",
|
|
100
|
+
"stats.actualPrompt": "actual prompt",
|
|
101
|
+
"stats.output": "output",
|
|
77
102
|
"tools.top": "Top tool schemas:",
|
|
78
103
|
"tools.more": "of {n}",
|
|
79
104
|
"trend.title": "History",
|
|
80
105
|
"gran.step": "Step",
|
|
81
106
|
"gran.turn": "Turn",
|
|
82
|
-
"trend.hint": "
|
|
107
|
+
"trend.hint": "hover a bar for details (click to pin), \u2702 marks compaction/prune; scroll sideways to see earlier; Step/Turn switches granularity",
|
|
83
108
|
"trend.empty": "Send a message and each model request\u2019s context makeup shows up here",
|
|
84
|
-
"detail.step": "
|
|
85
|
-
"detail.turn": "
|
|
109
|
+
"detail.step": "Turn {t} \xB7 Step {s}",
|
|
110
|
+
"detail.turn": "Turn {t} \xB7 {n} steps",
|
|
86
111
|
"detail.lastStep": "last step",
|
|
87
112
|
"detail.estTotal": "estimated \u2248 {n}",
|
|
88
113
|
"detail.actual": "actual prompt {n}",
|
|
89
114
|
"detail.output": "output {n}",
|
|
90
115
|
"events.title": "Context events",
|
|
91
116
|
"events.empty": "No context events yet (compaction, injections, model switches appear here)",
|
|
117
|
+
"events.at": "Turn {t} \xB7 Step {s}",
|
|
118
|
+
"events.range": "Turn {t} \xB7 Step {a}\u2192{b}",
|
|
119
|
+
"events.rangeTo": "Turn {a} \xB7 Step {as} \u2192 Turn {b} \xB7 Step {bs}",
|
|
92
120
|
"nodes.title": "Messages",
|
|
93
121
|
"nodes.hint": "currently model-visible, newest first",
|
|
94
122
|
"nodes.more": "\u2026 {n} earlier messages omitted",
|
|
@@ -96,8 +124,8 @@ const DICT_EN = {
|
|
|
96
124
|
"loading": "Reading the session log\u2026",
|
|
97
125
|
"error": "Failed to read context data: ",
|
|
98
126
|
"footer": "Estimate: same fixed-density heuristic as dsh\u2019s built-in tokenMeter (~4 chars \u2248 1 token); \u201Cactual\u201D is provider-reported usage.",
|
|
99
|
-
"tip.step": "
|
|
100
|
-
"tip.turn": "
|
|
127
|
+
"tip.step": "Turn {t} \xB7 Step {s}",
|
|
128
|
+
"tip.turn": "Turn {t} \xB7 {n} steps",
|
|
101
129
|
"tip.total": "total \u2248 {n}",
|
|
102
130
|
"tip.actual": " (actual {n})",
|
|
103
131
|
"ev.compaction": "Context compacted (summary replaced {n} messages)",
|
|
@@ -117,9 +145,135 @@ const DICT_EN = {
|
|
|
117
145
|
"node.nonText": "(non-text message)",
|
|
118
146
|
"node.snapshot": "snapshot: "
|
|
119
147
|
};
|
|
120
|
-
|
|
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;
|
|
@@ -293,10 +655,14 @@ function makeView(ctx, t) {
|
|
|
293
655
|
const scrolledOnce = React.useRef(false);
|
|
294
656
|
const lastGranRef = React.useRef(props.granularity);
|
|
295
657
|
const [edges, setEdges] = React.useState({ left: false, right: false });
|
|
658
|
+
const edgesRef = React.useRef(edges);
|
|
296
659
|
const updateEdges = (el) => {
|
|
297
660
|
const left = el.scrollLeft > 4;
|
|
298
661
|
const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
|
|
299
|
-
|
|
662
|
+
const prev = edgesRef.current;
|
|
663
|
+
if (prev.left === left && prev.right === right) return;
|
|
664
|
+
edgesRef.current = { left, right };
|
|
665
|
+
setEdges({ left, right });
|
|
300
666
|
};
|
|
301
667
|
React.useLayoutEffect(() => {
|
|
302
668
|
const el = scrollRef.current;
|
|
@@ -319,8 +685,8 @@ function makeView(ctx, t) {
|
|
|
319
685
|
h(
|
|
320
686
|
"div",
|
|
321
687
|
{ className: "lc-axis" },
|
|
322
|
-
h("span", { className: "lc-axis-top" },
|
|
323
|
-
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))),
|
|
324
690
|
h("span", { className: "lc-axis-bot" }, "0")
|
|
325
691
|
),
|
|
326
692
|
h(
|
|
@@ -345,10 +711,12 @@ function makeView(ctx, t) {
|
|
|
345
711
|
h("div", { className: "lc-grid lc-grid-top" }),
|
|
346
712
|
h("div", { className: "lc-grid lc-grid-mid" }),
|
|
347
713
|
requests.map((req, i) => {
|
|
714
|
+
const marker = markers[i];
|
|
715
|
+
const markerAt = marker !== void 0 ? eventAt(marker) : null;
|
|
348
716
|
const selected = props.selectedSeq === req.seq;
|
|
349
717
|
const hovered = props.hoveredSeq === req.seq;
|
|
350
718
|
const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
|
|
351
|
-
const tip = (req.stepCount !== void 0 && req.stepCount > 1 ? tr("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 })) + " \xB7 " +
|
|
719
|
+
const tip = (req.stepCount !== void 0 && req.stepCount > 1 ? tr("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 })) + " \xB7 " + fmtTime2(req.time) + "\n" + tr("tip.total", { n: fmt3(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt3(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt3(req[c.key] || 0)).join(" / ");
|
|
352
720
|
return h(
|
|
353
721
|
"div",
|
|
354
722
|
{
|
|
@@ -365,7 +733,12 @@ function makeView(ctx, t) {
|
|
|
365
733
|
props.onHover(req.seq);
|
|
366
734
|
}
|
|
367
735
|
},
|
|
368
|
-
|
|
736
|
+
// The ✂ tooltip names the event AND where it happened: the
|
|
737
|
+
// gap between the request before and the request after.
|
|
738
|
+
marker !== void 0 ? h("span", {
|
|
739
|
+
className: "lc-bar-marker",
|
|
740
|
+
title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
|
|
741
|
+
}, "\u2702") : null,
|
|
369
742
|
h(
|
|
370
743
|
"div",
|
|
371
744
|
{ className: "lc-bar-stack" },
|
|
@@ -404,105 +777,27 @@ function makeView(ctx, t) {
|
|
|
404
777
|
}
|
|
405
778
|
}, "T" + grp.turn);
|
|
406
779
|
})
|
|
407
|
-
)
|
|
408
|
-
edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
|
|
409
|
-
)
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
function RequestDetail(props) {
|
|
413
|
-
const req = props.request;
|
|
414
|
-
if (!req) return null;
|
|
415
|
-
const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
|
|
416
|
-
const head = isTurn ? tr("detail.turn", { t: req.turn ?? 0, n: req.stepCount ?? 0 }) : tr("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 });
|
|
417
|
-
return h(
|
|
418
|
-
"div",
|
|
419
|
-
{ className: "lc-detail" },
|
|
420
|
-
h(
|
|
421
|
-
"div",
|
|
422
|
-
{ className: "lc-detail-head" },
|
|
423
|
-
h("b", null, head),
|
|
424
|
-
isTurn ? h("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null,
|
|
425
|
-
h("span", null, fmtTime(req.time)),
|
|
426
|
-
h("span", null, tr("detail.estTotal", { n: fmt(req.total) })),
|
|
427
|
-
req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt(req.prompt) })) : null,
|
|
428
|
-
req.output !== void 0 ? h("span", null, tr("detail.output", { n: fmt(req.output) })) : null
|
|
780
|
+
)
|
|
429
781
|
),
|
|
430
|
-
h(
|
|
431
|
-
h(
|
|
432
|
-
"div",
|
|
433
|
-
{ className: "lc-detail-rows" },
|
|
434
|
-
CATS.map((c) => {
|
|
435
|
-
const v = req[c.key] || 0;
|
|
436
|
-
return h(
|
|
437
|
-
"div",
|
|
438
|
-
{ key: c.key, className: "lc-detail-row" },
|
|
439
|
-
h("i", { style: { background: c.color } }),
|
|
440
|
-
h("span", { className: "lc-detail-label" }, catLabel(c.key)),
|
|
441
|
-
h(
|
|
442
|
-
"span",
|
|
443
|
-
{ className: "lc-bar-track" },
|
|
444
|
-
h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
|
|
445
|
-
),
|
|
446
|
-
h("span", { className: "lc-detail-num" }, fmt(v)),
|
|
447
|
-
h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
|
|
448
|
-
);
|
|
449
|
-
})
|
|
450
|
-
)
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
function EventList(props) {
|
|
454
|
-
if (props.events.length === 0) {
|
|
455
|
-
return h("div", { className: "lc-empty" }, t("events.empty"));
|
|
456
|
-
}
|
|
457
|
-
const sorted = props.events.slice().reverse();
|
|
458
|
-
return h(
|
|
459
|
-
"div",
|
|
460
|
-
{ className: "lc-events" },
|
|
461
|
-
sorted.map((ev, i) => {
|
|
462
|
-
const label = eventLabel(ev);
|
|
463
|
-
return h(
|
|
464
|
-
"div",
|
|
465
|
-
{ key: ev.seq + "-" + i, className: "lc-event" },
|
|
466
|
-
h("span", { className: "lc-event-icon lc-event-" + ev.kind }, EVENT_ICONS[ev.kind] || "\u2022"),
|
|
467
|
-
h("span", { className: "lc-event-label", title: label }, label),
|
|
468
|
-
ev.tokens ? h(
|
|
469
|
-
"span",
|
|
470
|
-
{ className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") },
|
|
471
|
-
(ev.kind === "inject" ? "+" : "\u2212") + fmt(ev.tokens)
|
|
472
|
-
) : null,
|
|
473
|
-
h("span", { className: "lc-event-time" }, fmtTime(ev.time))
|
|
474
|
-
);
|
|
475
|
-
})
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
function NodeList(props) {
|
|
479
|
-
if (props.nodes.length === 0) {
|
|
480
|
-
return h("div", { className: "lc-empty" }, t("nodes.empty"));
|
|
481
|
-
}
|
|
482
|
-
const catColor = {};
|
|
483
|
-
for (const c of CATS) catColor[c.key] = c.color;
|
|
484
|
-
const rows = props.nodes.slice().reverse();
|
|
485
|
-
return h(
|
|
486
|
-
"div",
|
|
487
|
-
{ className: "lc-nodes" },
|
|
488
|
-
props.dropped > 0 ? h("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null,
|
|
489
|
-
rows.map((n) => {
|
|
490
|
-
const text = nodeText(n);
|
|
491
|
-
return h(
|
|
492
|
-
"div",
|
|
493
|
-
{ key: n.seq, className: "lc-node" },
|
|
494
|
-
h("i", { style: { background: catColor[n.cat] || "#999" } }),
|
|
495
|
-
h("span", { className: "lc-node-preview", title: text }, text),
|
|
496
|
-
// Timestamp when the host event carried one.
|
|
497
|
-
typeof n.time === "number" ? h("span", { className: "lc-node-time" }, fmtTime(n.time)) : null,
|
|
498
|
-
h("span", { className: "lc-node-tokens" }, fmt(n.tokens))
|
|
499
|
-
);
|
|
500
|
-
})
|
|
782
|
+
edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
|
|
501
783
|
);
|
|
502
|
-
}
|
|
503
|
-
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/client/components/contextView.ts
|
|
788
|
+
function makeContextView(ctx, kit) {
|
|
789
|
+
const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
|
|
790
|
+
const StackedBar = makeStackedBar(kit);
|
|
791
|
+
const Legend = makeLegend(kit);
|
|
792
|
+
const TrendChart = makeTrendChart(kit);
|
|
793
|
+
const RequestDetail = makeRequestDetail(kit, StackedBar);
|
|
794
|
+
const EventList = makeEventList(kit);
|
|
795
|
+
const NodeList = makeNodeList(kit);
|
|
796
|
+
const StatsBoard = makeStatsBoard(kit);
|
|
797
|
+
return function ContextView(props) {
|
|
504
798
|
const sessionId = props.sessionId;
|
|
505
|
-
const
|
|
799
|
+
const initial = typeof sessionId === "string" && sessionId !== "" ? cacheGet(sessionId) ?? null : null;
|
|
800
|
+
const [data, setData] = React.useState(initial);
|
|
506
801
|
const [error, setError] = React.useState(null);
|
|
507
802
|
const [selectedSeq, setSelectedSeq] = React.useState(null);
|
|
508
803
|
const [hoveredSeq, setHoveredSeq] = React.useState(null);
|
|
@@ -510,6 +805,10 @@ function makeView(ctx, t) {
|
|
|
510
805
|
const [tick, setTick] = React.useState(0);
|
|
511
806
|
const [granularity, setGranularity] = React.useState("step");
|
|
512
807
|
const [hoverCat, setHoverCat] = React.useState(null);
|
|
808
|
+
const dataRef = React.useRef(initial);
|
|
809
|
+
React.useEffect(() => {
|
|
810
|
+
dataRef.current = data;
|
|
811
|
+
}, [data]);
|
|
513
812
|
React.useEffect(() => {
|
|
514
813
|
if (typeof sessionId !== "string" || sessionId === "") return void 0;
|
|
515
814
|
let alive = true;
|
|
@@ -517,18 +816,31 @@ function makeView(ctx, t) {
|
|
|
517
816
|
ctx.connection.rpc.call("/dsh-context", "snapshot", { sessionId }).then((res) => {
|
|
518
817
|
if (!alive) return;
|
|
519
818
|
if (res && res.ok) {
|
|
520
|
-
|
|
819
|
+
const snap = res.value;
|
|
820
|
+
cachePut(sessionId, snap);
|
|
821
|
+
setData(snap);
|
|
521
822
|
setError(null);
|
|
522
|
-
} else
|
|
823
|
+
} else if (dataRef.current === null) {
|
|
824
|
+
setError(res && res.error ? String(res.error.message || res.error.code) : "failed");
|
|
825
|
+
}
|
|
523
826
|
}, (err) => {
|
|
524
|
-
if (alive
|
|
827
|
+
if (alive && dataRef.current === null) {
|
|
828
|
+
setError(String(err instanceof Error ? err.message : err));
|
|
829
|
+
}
|
|
525
830
|
});
|
|
526
831
|
};
|
|
527
832
|
load();
|
|
528
|
-
const timerId = setInterval(
|
|
833
|
+
const timerId = setInterval(() => {
|
|
834
|
+
if (document.visibilityState !== "hidden") load();
|
|
835
|
+
}, 2e3);
|
|
836
|
+
const onVisible = () => {
|
|
837
|
+
if (document.visibilityState === "visible") load();
|
|
838
|
+
};
|
|
839
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
529
840
|
return () => {
|
|
530
841
|
alive = false;
|
|
531
842
|
clearInterval(timerId);
|
|
843
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
532
844
|
};
|
|
533
845
|
}, [sessionId]);
|
|
534
846
|
React.useEffect(() => {
|
|
@@ -548,6 +860,11 @@ function makeView(ctx, t) {
|
|
|
548
860
|
const events = data.events || [];
|
|
549
861
|
const nodes = data.nodes || [];
|
|
550
862
|
const displayRequests = granularity === "turn" ? aggregateByTurn(requests) : requests;
|
|
863
|
+
const markers = attachMarkers(displayRequests, events);
|
|
864
|
+
const markerOf = (req) => {
|
|
865
|
+
const i = displayRequests.indexOf(req);
|
|
866
|
+
return i >= 0 ? markers[i] : void 0;
|
|
867
|
+
};
|
|
551
868
|
let pinnedReq = null;
|
|
552
869
|
for (const req of displayRequests) if (req.seq === selectedSeq) pinnedReq = req;
|
|
553
870
|
let activeReq = null;
|
|
@@ -567,6 +884,8 @@ function makeView(ctx, t) {
|
|
|
567
884
|
return h(
|
|
568
885
|
"div",
|
|
569
886
|
{ className: "lc-root" },
|
|
887
|
+
// ---- session context stats (over the retained window) ----
|
|
888
|
+
h(StatsBoard, { requests, events }),
|
|
570
889
|
// ---- overview ----
|
|
571
890
|
h(
|
|
572
891
|
"div",
|
|
@@ -584,8 +903,8 @@ function makeView(ctx, t) {
|
|
|
584
903
|
h(
|
|
585
904
|
"div",
|
|
586
905
|
{ className: "lc-overview-num" },
|
|
587
|
-
h("b", null,
|
|
588
|
-
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")),
|
|
589
908
|
// The provider-reported prompt of the last request is the best
|
|
590
909
|
// ground truth for what the model actually received; the fixed
|
|
591
910
|
// density heuristic can undercount CJK-heavy content, so show the
|
|
@@ -593,7 +912,7 @@ function makeView(ctx, t) {
|
|
|
593
912
|
displayRequests.length > 0 && displayRequests[displayRequests.length - 1].prompt !== void 0 ? h(
|
|
594
913
|
"span",
|
|
595
914
|
{ className: "lc-actual" },
|
|
596
|
-
tr("overview.actual", { n:
|
|
915
|
+
tr("overview.actual", { n: fmt3(displayRequests[displayRequests.length - 1].prompt ?? 0) })
|
|
597
916
|
) : null
|
|
598
917
|
),
|
|
599
918
|
h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
|
|
@@ -603,7 +922,7 @@ function makeView(ctx, t) {
|
|
|
603
922
|
{ className: "lc-tools" },
|
|
604
923
|
t("tools.top"),
|
|
605
924
|
data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
|
|
606
|
-
return h("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " +
|
|
925
|
+
return h("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
|
|
607
926
|
}),
|
|
608
927
|
data.toolList.length > 5 ? h("span", { className: "lc-card-sub" }, " " + tr("tools.more", { n: data.toolList.length })) : null
|
|
609
928
|
) : null
|
|
@@ -644,7 +963,7 @@ function makeView(ctx, t) {
|
|
|
644
963
|
// The host caps the log at 160 requests; render them ALL so
|
|
645
964
|
// earlier turns/steps stay reachable via horizontal scroll.
|
|
646
965
|
requests: displayRequests,
|
|
647
|
-
|
|
966
|
+
markers,
|
|
648
967
|
selectedSeq: pinnedReq ? pinnedReq.seq : null,
|
|
649
968
|
hoveredSeq,
|
|
650
969
|
activeTurn,
|
|
@@ -653,7 +972,7 @@ function makeView(ctx, t) {
|
|
|
653
972
|
onHover: setHoveredSeq,
|
|
654
973
|
onHoverTurn: setHoverTurn
|
|
655
974
|
}),
|
|
656
|
-
h(RequestDetail, { request: activeReq })
|
|
975
|
+
h(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })
|
|
657
976
|
)
|
|
658
977
|
),
|
|
659
978
|
// ---- events + messages ----
|
|
@@ -680,94 +999,24 @@ function makeView(ctx, t) {
|
|
|
680
999
|
),
|
|
681
1000
|
h("div", { className: "lc-foot" }, t("footer"))
|
|
682
1001
|
);
|
|
683
|
-
}
|
|
684
|
-
return ContextView;
|
|
1002
|
+
};
|
|
685
1003
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
".lc-stacked-seg-on { filter: brightness(1.18); }",
|
|
703
|
-
".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
|
|
704
|
-
".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
|
|
705
|
-
".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
|
|
706
|
-
".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
|
|
707
|
-
".lc-chip-on { font-weight: 600; }",
|
|
708
|
-
".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
|
|
709
|
-
".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
|
|
710
|
-
".lc-tool-chip { background: var(--dsw-alias-bg-layer-2); border-radius: 4px; padding: 1px 7px; font-size: 12px; color: var(--dsw-alias-label-primary); }",
|
|
711
|
-
".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
|
|
712
|
-
".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
|
|
713
|
-
".lc-axis span { position: absolute; right: 0; line-height: 1; }",
|
|
714
|
-
".lc-axis-top { top: 13px; }",
|
|
715
|
-
".lc-axis-mid { top: 69px; }",
|
|
716
|
-
".lc-axis-bot { top: 125px; }",
|
|
717
|
-
".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
|
|
718
|
-
".lc-chart-scroll::-webkit-scrollbar { height: 8px; }",
|
|
719
|
-
".lc-chart-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 45%, transparent); border-radius: 4px; }",
|
|
720
|
-
".lc-chart-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 70%, transparent); }",
|
|
721
|
-
".lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }",
|
|
722
|
-
".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
|
|
723
|
-
".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
|
|
724
|
-
".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
|
|
725
|
-
".lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }",
|
|
726
|
-
".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
|
|
727
|
-
".lc-grid-top { top: 18px; }",
|
|
728
|
-
".lc-grid-mid { top: 74px; }",
|
|
729
|
-
".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }",
|
|
730
|
-
".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
731
|
-
".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
|
|
732
|
-
".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
|
|
733
|
-
".lc-bar-in-turn { background: rgba(99,102,241,0.14); }",
|
|
734
|
-
".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
|
|
735
|
-
".lc-bar-stack > div { width: 100%; }",
|
|
736
|
-
".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
|
|
737
|
-
".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
|
|
738
|
-
".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms; }",
|
|
739
|
-
".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
|
|
740
|
-
".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
|
|
741
|
-
".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
|
|
742
|
-
".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
|
|
743
|
-
".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
|
|
744
|
-
".lc-detail-tag { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); }",
|
|
745
|
-
".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
|
|
746
|
-
".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
|
|
747
|
-
".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
|
|
748
|
-
".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
|
|
749
|
-
".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
|
|
750
|
-
".lc-detail-num { width: 44px; text-align: right; }",
|
|
751
|
-
".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
|
|
752
|
-
".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; }",
|
|
753
|
-
".lc-col { flex: 1; min-width: 280px; }",
|
|
754
|
-
".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
|
|
755
|
-
".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
|
|
756
|
-
".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
|
|
757
|
-
".lc-event-icon.lc-event-inject { color: #a855f7; }",
|
|
758
|
-
".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
|
|
759
|
-
".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
|
|
760
|
-
".lc-event-tokens { color: var(--dsw-alias-state-success-primary); }",
|
|
761
|
-
".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
|
|
762
|
-
".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
|
|
763
|
-
".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
|
|
764
|
-
".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
|
|
765
|
-
".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
|
|
766
|
-
".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
|
|
767
|
-
".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
|
|
768
|
-
".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
|
|
769
|
-
".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }"
|
|
770
|
-
].join("\n");
|
|
1004
|
+
|
|
1005
|
+
// src/client/viewkit.ts
|
|
1006
|
+
function makeViewKit(t) {
|
|
1007
|
+
const { eventLabel, eventAt } = makeEventText(t, t);
|
|
1008
|
+
return {
|
|
1009
|
+
t,
|
|
1010
|
+
tr: t,
|
|
1011
|
+
fmt,
|
|
1012
|
+
fmtTime,
|
|
1013
|
+
catLabel: (key) => t("cat." + key),
|
|
1014
|
+
eventLabel,
|
|
1015
|
+
eventAt
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/client/index.ts
|
|
771
1020
|
function apply(ctx) {
|
|
772
1021
|
ctx.effect(() => {
|
|
773
1022
|
return ctx.locale.register("dsh-context", { zh: DICT_ZH, en: DICT_EN });
|
|
@@ -782,7 +1031,7 @@ function apply(ctx) {
|
|
|
782
1031
|
if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
|
|
783
1032
|
};
|
|
784
1033
|
}, "dsh-context: styles");
|
|
785
|
-
const ContextView =
|
|
1034
|
+
const ContextView = makeContextView(ctx, makeViewKit(t));
|
|
786
1035
|
ctx.slots.inject("conversation.view", () => {
|
|
787
1036
|
return ctx.slots.register(
|
|
788
1037
|
// order 20 renders right of Chat (0) and Trajectory (10).
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
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": {
|