chiral-pulse 0.1.0 → 1.2.4
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 +16 -1
- package/lib/client.js +233 -69
- package/lib/client.js.map +1 -1
- package/package.json +68 -68
package/README.md
CHANGED
|
@@ -9,6 +9,21 @@
|
|
|
9
9
|
|
|
10
10
|
`dsh-plugin` · `deepseek-harness` · `ui-plugin` · `death-stranding` · `bb-pod` · `ecg` · `theme`
|
|
11
11
|
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 版本
|
|
17
|
+
|
|
18
|
+
| 版本 | npm | GitHub Release | 亮点 |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| **1.2.3** | ✅ [chiral-pulse@1.2.3](https://www.npmjs.com/package/chiral-pulse) | [v1.2.3](https://github.com/MoonShadow1976/chiral-pulse/releases/tag/v1.2.3) | 真·erase-bar 扫掠(冻结像素缓存,扫描线就地刷新);重试卡顿→心跳停跳;英雄页/徽章修复 |
|
|
21
|
+
| **1.2.2** | — | [v1.2.2](https://github.com/MoonShadow1976/chiral-pulse/releases/tag/v1.2.2) | sweep 模式过渡版本(被 1.2.3 取代) |
|
|
22
|
+
| **1.2.0** | — | [v1.2.0](https://github.com/MoonShadow1976/chiral-pulse/releases/tag/v1.2.0) | 恒定走纸、切角语言、全局暗蓝皮肤定稿 |
|
|
23
|
+
| **0.1.0** | ✅ 已发布 | [v0.1.0](https://github.com/MoonShadow1976/chiral-pulse/releases/tag/v0.1.0) | 首个版本 |
|
|
24
|
+
|
|
25
|
+
> 发版流程:打 tag(`v*.*.*`)→ GitHub Actions 自动构建并发布 npm(见 `.github/workflows/release.yml`)。
|
|
26
|
+
|
|
12
27
|
---
|
|
13
28
|
|
|
14
29
|
## 这是什么
|
|
@@ -69,7 +84,7 @@ bundle 层,无需手写配置。
|
|
|
69
84
|
```
|
|
70
85
|
|
|
71
86
|
```powershell
|
|
72
|
-
New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\web\node_modules
|
|
87
|
+
New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\chiral-pulse" -Target "D:\path\to\chiral-pulse"
|
|
73
88
|
```
|
|
74
89
|
|
|
75
90
|
用户 patch 层是热加载的:无需重启 `dsh web`。之后**刷新浏览器页面**即可
|
package/lib/client.js
CHANGED
|
@@ -28,7 +28,7 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
* @returns the waveform amplitude at that phase.
|
|
29
29
|
*/
|
|
30
30
|
function ecgValue(phase) {
|
|
31
|
-
return bump(phase, .14, .03, .16) - bump(phase, .3, .011, .26) + bump(phase, .335, .
|
|
31
|
+
return bump(phase, .14, .03, .16) - bump(phase, .3, .011, .26) + bump(phase, .335, .016, 1) - bump(phase, .375, .011, .34) + bump(phase, .52, .048, .26) + bump(phase, .8, .012, .05);
|
|
32
32
|
}
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region src/client/HeartLine.tsx
|
|
@@ -53,8 +53,14 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
*/
|
|
54
54
|
/** Monitor view height, CSS px. */
|
|
55
55
|
const ECG_HEIGHT = 22;
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* FIXED paper speed in px/second — the real hospital-monitor invariant.
|
|
58
|
+
* The trace scrolls at this absolute rate no matter the strip width; the
|
|
59
|
+
* width only decides how much history fits on screen. A rate change (42→90)
|
|
60
|
+
* therefore only densifies the beats — it never speeds the paper up, and
|
|
61
|
+
* resizing the window cannot make the trace run faster either.
|
|
62
|
+
*/
|
|
63
|
+
const PAPER_SPEED_PX_PER_SECOND = 30;
|
|
58
64
|
/** Activity window for the step-rate base, ms. */
|
|
59
65
|
const ACTIVITY_WINDOW_MS = 1e4;
|
|
60
66
|
/** Rotating status lines (locale keys), one every STATUS_ROTATE_S ticks. */
|
|
@@ -77,12 +83,20 @@ window.__ModuleLoader__.load({
|
|
|
77
83
|
/** BPM floor (a resting BB) and ceiling. */
|
|
78
84
|
const BPM_FLOOR = 42;
|
|
79
85
|
const BPM_CEIL = 150;
|
|
86
|
+
/**
|
|
87
|
+
* How fast the displayed heart rate ramps toward its target, in BPM/second.
|
|
88
|
+
* A hospital monitor updates its HR figure on a ~2-3s rolling average and
|
|
89
|
+
* the trace follows gradually — the rate change reads as a slow ramp, not a
|
|
90
|
+
* snap: 42 → 90 takes (90-42)/6 = 8 seconds of visible densification.
|
|
91
|
+
*/
|
|
92
|
+
const BPM_RAMP_PER_SECOND = 6;
|
|
80
93
|
/** Trace color by activity mode: idle amber, thinking cyan, tool orange, run warm. */
|
|
81
94
|
const MODE_COLOR = {
|
|
82
95
|
idle: "#ffb454",
|
|
83
96
|
think: "#6fdbe2",
|
|
84
97
|
tool: "#ff7a4d",
|
|
85
|
-
run: "#ffc46b"
|
|
98
|
+
run: "#ffc46b",
|
|
99
|
+
flat: "#c0483c"
|
|
86
100
|
};
|
|
87
101
|
/** Tail of the model's in-flight output: last non-empty text/reasoning block, whitespace-flattened. */
|
|
88
102
|
function streamingTail(blocks) {
|
|
@@ -99,13 +113,21 @@ window.__ModuleLoader__.load({
|
|
|
99
113
|
*/
|
|
100
114
|
function HeartLine({ useSession, useProjection, t }) {
|
|
101
115
|
const stats = useProjection("sessionStats");
|
|
102
|
-
const live =
|
|
103
|
-
partial: s.partial !== null,
|
|
104
|
-
partialText: s.partial === null ? "" : streamingTail(s.partial.blocks),
|
|
105
|
-
toolName: s.runningCalls[0]?.name ?? null,
|
|
106
|
-
running: s.running,
|
|
107
|
-
error: s.lastAgentError
|
|
108
|
-
|
|
116
|
+
const live = {
|
|
117
|
+
partial: useSession((s) => s.partial !== null),
|
|
118
|
+
partialText: useSession((s) => s.partial === null ? "" : streamingTail(s.partial.blocks)),
|
|
119
|
+
toolName: useSession((s) => s.runningCalls[0]?.name ?? null),
|
|
120
|
+
running: useSession((s) => s.running),
|
|
121
|
+
error: useSession((s) => s.lastAgentError),
|
|
122
|
+
retrying: useSession((s) => {
|
|
123
|
+
const nodes = s.chat.legacy.nodes;
|
|
124
|
+
for (let i = nodes.length - 1; i >= 0; i -= 1) {
|
|
125
|
+
const n = nodes[i];
|
|
126
|
+
if (n.kind === "model-retry") return n.retryState === "scheduled" && n.time > Date.now() - 12e4;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
})
|
|
130
|
+
};
|
|
109
131
|
const steps = stats?.steps ?? 0;
|
|
110
132
|
const bpmRef = (0, react.useRef)(BPM_FLOOR);
|
|
111
133
|
const targetRef = (0, react.useRef)(BPM_FLOOR);
|
|
@@ -139,9 +161,8 @@ window.__ModuleLoader__.load({
|
|
|
139
161
|
const perMinute = span > 0 ? delta / span * 6e4 : 0;
|
|
140
162
|
const base = Math.min(BPM_CEIL, Math.max(BPM_FLOOR, 42 + perMinute * 6));
|
|
141
163
|
const act = liveRef.current;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const mode = act.toolName !== null ? "tool" : act.partial ? "think" : act.running ? "run" : "idle";
|
|
164
|
+
targetRef.current = act.retrying ? 0 : Math.min(BPM_CEIL, Math.max(BPM_FLOOR, base + (act.toolName !== null ? BOOST_TOOL : 0) + (act.partial ? BOOST_THINKING : 0) + (act.running ? BOOST_RUNNING : 0)));
|
|
165
|
+
const mode = act.retrying ? "flat" : act.toolName !== null ? "tool" : act.partial ? "think" : act.running ? "run" : "idle";
|
|
145
166
|
modeRef.current = mode;
|
|
146
167
|
setUi((current) => ({
|
|
147
168
|
bpm: Math.round(bpmRef.current),
|
|
@@ -163,6 +184,7 @@ window.__ModuleLoader__.load({
|
|
|
163
184
|
const ctx = canvas.getContext("2d");
|
|
164
185
|
if (ctx === null) return;
|
|
165
186
|
ctxRef.current = ctx;
|
|
187
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
166
188
|
const applySize = () => {
|
|
167
189
|
const dpr = window.devicePixelRatio || 1;
|
|
168
190
|
dprRef.current = dpr;
|
|
@@ -175,44 +197,89 @@ window.__ModuleLoader__.load({
|
|
|
175
197
|
if (width !== widthRef.current) {
|
|
176
198
|
widthRef.current = width;
|
|
177
199
|
applySize();
|
|
200
|
+
if (reduced) paint(performance.now());
|
|
178
201
|
}
|
|
179
202
|
});
|
|
180
203
|
observer.observe(canvas);
|
|
181
|
-
let
|
|
204
|
+
let displayNow = 0;
|
|
205
|
+
let lastPaintReal = 0;
|
|
206
|
+
let framePeriodMs = 16.7;
|
|
207
|
+
let traceCache = [];
|
|
208
|
+
let lastScanX = -1;
|
|
182
209
|
const paint = (now) => {
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
if (lastPaintReal === 0) {
|
|
211
|
+
lastPaintReal = now;
|
|
212
|
+
displayNow = now;
|
|
213
|
+
}
|
|
214
|
+
const realDt = Math.max(0, (now - lastPaintReal) / 1e3);
|
|
215
|
+
lastPaintReal = now;
|
|
216
|
+
if (realDt > 0 && realDt < .05) framePeriodMs = realDt * 1e3;
|
|
217
|
+
const dt = framePeriodMs / 1e3;
|
|
218
|
+
displayNow += dt * 1e3;
|
|
219
|
+
const diff = targetRef.current - bpmRef.current;
|
|
220
|
+
const step = BPM_RAMP_PER_SECOND * dt;
|
|
221
|
+
if (diff > step) bpmRef.current += step;
|
|
222
|
+
else if (diff < -step) bpmRef.current -= step;
|
|
223
|
+
else bpmRef.current = targetRef.current;
|
|
187
224
|
const c = canvasRef.current;
|
|
188
225
|
const g = ctxRef.current;
|
|
189
226
|
if (c === null || g === null) return;
|
|
227
|
+
const dpr = dprRef.current;
|
|
190
228
|
const w = widthRef.current;
|
|
191
229
|
const h = ECG_HEIGHT;
|
|
192
|
-
const
|
|
230
|
+
const wantW = Math.round(w * dpr);
|
|
231
|
+
const wantH = Math.round(h * dpr);
|
|
232
|
+
if (c.width !== wantW || c.height !== wantH) {
|
|
233
|
+
c.width = wantW;
|
|
234
|
+
c.height = wantH;
|
|
235
|
+
}
|
|
193
236
|
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
194
237
|
g.clearRect(0, 0, w, h);
|
|
195
|
-
const tNow =
|
|
196
|
-
const secondsPerPixel =
|
|
238
|
+
const tNow = displayNow / 1e3;
|
|
239
|
+
const secondsPerPixel = 1 / PAPER_SPEED_PX_PER_SECOND;
|
|
197
240
|
const mid = h / 2;
|
|
198
|
-
const amp = h * .
|
|
241
|
+
const amp = h * .5;
|
|
199
242
|
const wander = .05 * Math.sin(tNow * .6) + .035 * Math.sin(tNow * 1.7 + 1.3);
|
|
200
243
|
const bpm = bpmRef.current;
|
|
201
|
-
const
|
|
202
|
-
|
|
244
|
+
const flatline = targetRef.current === 0;
|
|
245
|
+
const sweepPeriod = w / PAPER_SPEED_PX_PER_SECOND;
|
|
246
|
+
const tInSweep = (tNow % sweepPeriod + sweepPeriod) % sweepPeriod;
|
|
247
|
+
const scanX = w - tInSweep * PAPER_SPEED_PX_PER_SECOND;
|
|
248
|
+
const scanXInt = Math.round(scanX);
|
|
249
|
+
const yNow = (x) => {
|
|
250
|
+
if (flatline) return mid;
|
|
251
|
+
let v = -Infinity;
|
|
252
|
+
for (let i = 0; i < 4; i += 1) {
|
|
253
|
+
const s = ecgValue(((tNow - tInSweep - (w - (x + i * .25)) * secondsPerPixel) * (bpm / 60) % 1 + 1) % 1);
|
|
254
|
+
if (s > v) v = s;
|
|
255
|
+
}
|
|
256
|
+
return mid - (v + wander) * amp;
|
|
203
257
|
};
|
|
258
|
+
if (traceCache.length !== w + 1) {
|
|
259
|
+
traceCache = new Array(w + 1);
|
|
260
|
+
for (let x = 0; x <= w; x += 1) traceCache[x] = yNow(x);
|
|
261
|
+
lastScanX = scanXInt;
|
|
262
|
+
} else if (lastScanX > scanXInt) {
|
|
263
|
+
for (let x = scanXInt; x <= lastScanX && x <= w; x += 1) traceCache[x] = yNow(x);
|
|
264
|
+
lastScanX = scanXInt;
|
|
265
|
+
} else if (lastScanX < scanXInt) {
|
|
266
|
+
traceCache[scanXInt] = yNow(scanXInt);
|
|
267
|
+
lastScanX = scanXInt;
|
|
268
|
+
} else lastScanX = scanXInt;
|
|
204
269
|
g.beginPath();
|
|
205
270
|
for (let x = 0; x <= w; x += 1) {
|
|
206
|
-
const y =
|
|
271
|
+
const y = traceCache[x];
|
|
207
272
|
if (x === 0) g.moveTo(x + 3, y);
|
|
208
273
|
else g.lineTo(x + 3, y);
|
|
209
274
|
}
|
|
210
|
-
g.
|
|
275
|
+
g.globalAlpha = .1;
|
|
276
|
+
g.strokeStyle = "rgba(111, 219, 226, 1)";
|
|
211
277
|
g.lineWidth = 1;
|
|
212
278
|
g.stroke();
|
|
279
|
+
g.globalAlpha = 1;
|
|
213
280
|
g.beginPath();
|
|
214
281
|
for (let x = 0; x <= w; x += 1) {
|
|
215
|
-
const y =
|
|
282
|
+
const y = traceCache[x];
|
|
216
283
|
if (x === 0) g.moveTo(x, y);
|
|
217
284
|
else g.lineTo(x, y);
|
|
218
285
|
}
|
|
@@ -221,18 +288,12 @@ window.__ModuleLoader__.load({
|
|
|
221
288
|
g.lineJoin = "round";
|
|
222
289
|
g.lineCap = "round";
|
|
223
290
|
g.stroke();
|
|
224
|
-
|
|
225
|
-
g.
|
|
226
|
-
g.
|
|
227
|
-
g.
|
|
228
|
-
g.arc(w - 2, headY, 5, 0, Math.PI * 2);
|
|
229
|
-
g.fill();
|
|
230
|
-
g.globalAlpha = 1;
|
|
231
|
-
g.beginPath();
|
|
232
|
-
g.arc(w - 2, headY, 1.6, 0, Math.PI * 2);
|
|
233
|
-
g.fill();
|
|
291
|
+
g.fillStyle = "rgba(255, 180, 84, 0.16)";
|
|
292
|
+
g.fillRect(scanX - 5, 0, 10, h);
|
|
293
|
+
g.fillStyle = "rgba(255, 224, 190, 0.95)";
|
|
294
|
+
g.fillRect(scanX - 1, 0, 2, h);
|
|
234
295
|
};
|
|
235
|
-
if (
|
|
296
|
+
if (reduced) {
|
|
236
297
|
paint(performance.now());
|
|
237
298
|
return () => {
|
|
238
299
|
observer.disconnect();
|
|
@@ -250,25 +311,26 @@ window.__ModuleLoader__.load({
|
|
|
250
311
|
};
|
|
251
312
|
}, []);
|
|
252
313
|
const flavor = STATUS_KEYS[Math.floor(ui.elapsed / STATUS_ROTATE_S) % STATUS_KEYS.length];
|
|
253
|
-
const status = live.error !== null ? `⚠ ${live.error.slice(0, 16)}` : live.toolName !== null ? `EXEC · ${live.toolName}` : live.partialText !== "" ? `⇢ ${live.partialText.slice(-18)}` : t(flavor);
|
|
314
|
+
const status = live.error !== null ? `⚠ ${live.error.slice(0, 16)}` : live.retrying ? t("status.flatline") : live.toolName !== null ? `EXEC · ${live.toolName}` : live.partialText !== "" ? `⇢ ${live.partialText.slice(-18)}` : t(flavor);
|
|
254
315
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
255
316
|
className: "cp-line",
|
|
256
317
|
role: "group",
|
|
257
318
|
"aria-label": t("line.aria"),
|
|
258
319
|
"data-chiral-pulse": true,
|
|
259
320
|
"data-mode": ui.mode,
|
|
321
|
+
"data-rev": "20",
|
|
260
322
|
children: [
|
|
261
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
262
324
|
className: "cp-lineBpm",
|
|
263
|
-
children:
|
|
264
|
-
className: "cp-lineBpmUnit",
|
|
265
|
-
children: t("bpm.unit")
|
|
266
|
-
})]
|
|
325
|
+
children: ui.bpm
|
|
267
326
|
}),
|
|
268
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
328
|
+
className: "cp-lineEcgWrap",
|
|
329
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
330
|
+
ref: canvasRef,
|
|
331
|
+
className: "cp-lineEcg",
|
|
332
|
+
"aria-hidden": true
|
|
333
|
+
})
|
|
272
334
|
}),
|
|
273
335
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
274
336
|
className: "cp-lineReadout",
|
|
@@ -302,7 +364,8 @@ window.__ModuleLoader__.load({
|
|
|
302
364
|
"status.doom": "DOOMS LEVEL: 0",
|
|
303
365
|
"status.keep": "KEEP ON KEEPING ON",
|
|
304
366
|
"status.voidout": "NO VOIDOUT DETECTED",
|
|
305
|
-
"status.odradek": "ODRADEK SYNC: OK"
|
|
367
|
+
"status.odradek": "ODRADEK SYNC: OK",
|
|
368
|
+
"status.flatline": "♥ FLATLINE"
|
|
306
369
|
};
|
|
307
370
|
/** Chinese dictionary. */
|
|
308
371
|
const zh = {
|
|
@@ -314,7 +377,8 @@ window.__ModuleLoader__.load({
|
|
|
314
377
|
"status.doom": "DOOMS 等级:0",
|
|
315
378
|
"status.keep": "继续前进 · KEEP ON KEEPING ON",
|
|
316
379
|
"status.voidout": "未检测到虚爆",
|
|
317
|
-
"status.odradek": "奥卓克同步:正常"
|
|
380
|
+
"status.odradek": "奥卓克同步:正常",
|
|
381
|
+
"status.flatline": "♥ 心脏停跳"
|
|
318
382
|
};
|
|
319
383
|
//#endregion
|
|
320
384
|
//#region src/client/style.ts
|
|
@@ -358,6 +422,12 @@ body:not([data-ds-dark-theme]) {
|
|
|
358
422
|
--dsw-alias-bg-mask-3: rgba(0, 0, 0, 0.5);
|
|
359
423
|
--dsw-alias-bg-mask-drop: rgba(10, 14, 19, 0.72);
|
|
360
424
|
--dsw-alias-bg-skeleton: rgba(150, 170, 200, 0.07);
|
|
425
|
+
/* Setting rows / select chips (language, agent preset, model, permissions…)
|
|
426
|
+
paint from these; without an override they stay the LIGHT theme's
|
|
427
|
+
near-white and produce white-on-white text. */
|
|
428
|
+
--dsw-alias-bg-module-platform: rgb(15, 20, 27);
|
|
429
|
+
--dsw-alias-bg-multi-select: rgb(18, 24, 32);
|
|
430
|
+
--dsw-alias-fill-tsp-secondary: rgb(18, 24, 32);
|
|
361
431
|
|
|
362
432
|
/* text: cold blue-grey */
|
|
363
433
|
--dsw-alias-label-primary: rgb(235, 240, 246);
|
|
@@ -366,10 +436,12 @@ body:not([data-ds-dark-theme]) {
|
|
|
366
436
|
--dsw-alias-label-caption: rgb(108, 122, 141);
|
|
367
437
|
--dsw-alias-label-dimmed: rgb(94, 108, 127);
|
|
368
438
|
--dsw-alias-label-primary-dimmed: rgb(205, 213, 222);
|
|
369
|
-
--dsw-alias-label-primary-inverted: rgb(
|
|
439
|
+
--dsw-alias-label-primary-inverted: rgb(20, 26, 35);
|
|
370
440
|
--dsw-alias-label-primary-foreground: rgb(13, 17, 23);
|
|
441
|
+
--dsw-alias-label-primary-bluish: rgb(200, 214, 232);
|
|
371
442
|
--dsw-alias-brand-text: rgb(235, 240, 246);
|
|
372
443
|
--dsw-alias-brand-primary: rgb(103, 158, 254);
|
|
444
|
+
--dsw-alias-brand-primary-invert: rgb(235, 240, 246);
|
|
373
445
|
|
|
374
446
|
/* hairlines: cold blue, readable against the body */
|
|
375
447
|
--dsw-alias-border-l1: rgba(140, 170, 215, 0.15);
|
|
@@ -396,6 +468,9 @@ body:not([data-ds-dark-theme]) {
|
|
|
396
468
|
--dsw-alias-button-floating-fill: rgb(19, 25, 33);
|
|
397
469
|
--dsw-alias-button-floating-hover: rgb(24, 31, 41);
|
|
398
470
|
--dsw-alias-button-elevated-fill: rgb(22, 29, 38);
|
|
471
|
+
/* Contrast fill (attachment rail): pale case + DARK inverted ink — the
|
|
472
|
+
wordmark badge and toasts also ride label-primary-inverted, so the pair
|
|
473
|
+
(pale fill, dark ink) stays readable everywhere. */
|
|
399
474
|
--dsw-alias-button-contrast-fill: rgb(205, 213, 222);
|
|
400
475
|
--dsw-alias-button-tool-bar-fill: rgba(140, 170, 215, 0.24);
|
|
401
476
|
--dsw-alias-button-tool-bar-fill-invisible: rgba(140, 170, 215, 0.13);
|
|
@@ -438,6 +513,9 @@ body:not([data-ds-dark-theme]) {
|
|
|
438
513
|
--dsw-alias-state-success-primary: rgb(52, 205, 168);
|
|
439
514
|
--dsw-alias-state-success-secondary: rgb(94, 222, 189);
|
|
440
515
|
--dsw-alias-state-success-tertiary: rgb(12, 28, 24);
|
|
516
|
+
/* Business tint (hero "preview" badge et al.): dark case so the pale
|
|
517
|
+
primary-bluish ink stays readable — the light-theme default is near-white. */
|
|
518
|
+
--dsw-alias-state-business-tertiary: rgb(26, 34, 46);
|
|
441
519
|
--dsw-static-green-400: rgb(94, 222, 189);
|
|
442
520
|
--dsw-static-green-500: rgb(52, 205, 168);
|
|
443
521
|
}
|
|
@@ -491,10 +569,14 @@ body {
|
|
|
491
569
|
--cp-amber-bright: #ffd9a0;
|
|
492
570
|
--cp-cyan: #6fdbe2;
|
|
493
571
|
--cp-dim: #64727f;
|
|
572
|
+
/* flex: none — the hero (blank-session) composer column squeezes its
|
|
573
|
+
children on short viewports; the monitor strip must never shrink. */
|
|
574
|
+
flex: none;
|
|
494
575
|
display: flex;
|
|
495
576
|
align-items: center;
|
|
496
577
|
gap: 12px;
|
|
497
578
|
height: 26px;
|
|
579
|
+
min-height: 26px;
|
|
498
580
|
margin: 3px 0 4px;
|
|
499
581
|
padding: 0 10px;
|
|
500
582
|
border: 1px solid rgba(140, 170, 215, 0.26);
|
|
@@ -519,9 +601,13 @@ body {
|
|
|
519
601
|
}
|
|
520
602
|
|
|
521
603
|
.cp-lineBpm {
|
|
604
|
+
/* Fixed width: a 3-digit readout (42 → 150) must not widen the block and
|
|
605
|
+
squeeze the paper area — that would shrink the trace window and pull the
|
|
606
|
+
left edge rightward as the rate climbs. */
|
|
522
607
|
flex: none;
|
|
523
|
-
|
|
524
|
-
|
|
608
|
+
width: 48px;
|
|
609
|
+
text-align: center;
|
|
610
|
+
font-size: 16px;
|
|
525
611
|
line-height: 1;
|
|
526
612
|
letter-spacing: 0.5px;
|
|
527
613
|
color: var(--cp-amber-bright);
|
|
@@ -529,18 +615,15 @@ body {
|
|
|
529
615
|
font-variant-numeric: tabular-nums;
|
|
530
616
|
white-space: nowrap;
|
|
531
617
|
}
|
|
532
|
-
.cp-lineBpmUnit {
|
|
533
|
-
font-size: 7px;
|
|
534
|
-
letter-spacing: 1px;
|
|
535
|
-
color: var(--cp-dim);
|
|
536
|
-
margin-left: 2px;
|
|
537
|
-
}
|
|
538
618
|
|
|
539
|
-
.cp-
|
|
540
|
-
|
|
619
|
+
.cp-lineEcgWrap {
|
|
620
|
+
/* basis 0 + grow: the paper area takes exactly the flex-allocated width;
|
|
621
|
+
never shrink (a squeezed strip would shrink the canvas bitmap and make
|
|
622
|
+
the trace speed depend on the window width). */
|
|
623
|
+
flex: 1 1 0;
|
|
541
624
|
min-width: 100px;
|
|
542
625
|
height: 22px;
|
|
543
|
-
|
|
626
|
+
position: relative;
|
|
544
627
|
border-radius: 0;
|
|
545
628
|
border: 1px solid rgba(140, 170, 215, 0.16);
|
|
546
629
|
/* Static paper grid lives in CSS; the canvas above it only paints the trace. */
|
|
@@ -549,6 +632,17 @@ body {
|
|
|
549
632
|
repeating-linear-gradient(90deg, rgba(140, 170, 215, 0.06) 0 1px, transparent 1px 11px),
|
|
550
633
|
rgba(7, 10, 15, 0.55);
|
|
551
634
|
}
|
|
635
|
+
/* The canvas fills its wrapper exactly (absolute), so its intrinsic size can
|
|
636
|
+
never distort the flex layout or the trace during remounts. */
|
|
637
|
+
.cp-lineEcg {
|
|
638
|
+
position: absolute;
|
|
639
|
+
inset: 0;
|
|
640
|
+
width: 100%;
|
|
641
|
+
height: 100%;
|
|
642
|
+
display: block;
|
|
643
|
+
border: 0;
|
|
644
|
+
background: transparent;
|
|
645
|
+
}
|
|
552
646
|
|
|
553
647
|
.cp-lineReadout {
|
|
554
648
|
flex: none;
|
|
@@ -745,16 +839,13 @@ body {
|
|
|
745
839
|
──────────────────────────────────────────────────────────────────────── */
|
|
746
840
|
[data-composer-seat] textarea {
|
|
747
841
|
caret-color: #ffb454;
|
|
748
|
-
letter-spacing: 0.01em;
|
|
749
842
|
}
|
|
750
843
|
[data-composer-seat] textarea:focus {
|
|
751
844
|
caret-color: #ffd9a0;
|
|
752
845
|
}
|
|
846
|
+
/* Composer seat: squared, no extra frame — a visible outline on the big hero
|
|
847
|
+
card read as a jarring border. */
|
|
753
848
|
[data-composer-seat] {
|
|
754
|
-
box-shadow:
|
|
755
|
-
inset 0 0 0 1px rgba(140, 170, 215, 0.22),
|
|
756
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.06),
|
|
757
|
-
inset 0 -12px 30px rgba(0, 0, 0, 0.25);
|
|
758
849
|
border-radius: 0;
|
|
759
850
|
}
|
|
760
851
|
|
|
@@ -762,7 +853,10 @@ body {
|
|
|
762
853
|
Dialogs, menus, tooltips, toasts — the floating DS surfaces
|
|
763
854
|
──────────────────────────────────────────────────────────────────────── */
|
|
764
855
|
[role="dialog"] {
|
|
765
|
-
border: 1px solid rgba(255, 180, 84, 0.
|
|
856
|
+
border: 1px solid rgba(255, 180, 84, 0.35) !important;
|
|
857
|
+
/* Inner hairline frame — the DS double-cased panel. */
|
|
858
|
+
outline: 1px solid rgba(140, 170, 215, 0.22);
|
|
859
|
+
outline-offset: -6px;
|
|
766
860
|
border-radius: 0 !important;
|
|
767
861
|
background: linear-gradient(180deg, rgba(15, 20, 27, 0.98), rgba(10, 14, 19, 0.99)) !important;
|
|
768
862
|
box-shadow:
|
|
@@ -796,6 +890,76 @@ body {
|
|
|
796
890
|
);
|
|
797
891
|
}
|
|
798
892
|
|
|
893
|
+
/* Toast (the only alert portaled straight onto body): DS gold badge —
|
|
894
|
+
amber case, dark ink, chamfered. Inline error rows keep the dark case
|
|
895
|
+
above; this rule wins for the fixed top-center banner. */
|
|
896
|
+
body > [role="alert"] {
|
|
897
|
+
border: 1px solid rgba(255, 196, 120, 0.7) !important;
|
|
898
|
+
border-radius: 0 !important;
|
|
899
|
+
background: linear-gradient(180deg, #ffbe6b, #e09a3c) !important;
|
|
900
|
+
color: rgb(28, 18, 6) !important;
|
|
901
|
+
box-shadow:
|
|
902
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.4),
|
|
903
|
+
0 10px 32px rgba(0, 0, 0, 0.5) !important;
|
|
904
|
+
clip-path: polygon(
|
|
905
|
+
10px 0,
|
|
906
|
+
100% 0,
|
|
907
|
+
100% calc(100% - 10px),
|
|
908
|
+
calc(100% - 10px) 100%,
|
|
909
|
+
0 100%,
|
|
910
|
+
0 10px
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/* Session-header action + utility buttons (Session log, jobs…):
|
|
915
|
+
DS chamfered buttons. Session log lives in .utilities, jobs in .actions. */
|
|
916
|
+
[data-slot="conversation.session.header.actions"] button,
|
|
917
|
+
[data-slot="conversation.session.header.utilities"] button {
|
|
918
|
+
border-radius: 0 !important;
|
|
919
|
+
clip-path: polygon(
|
|
920
|
+
6px 0,
|
|
921
|
+
100% 0,
|
|
922
|
+
100% calc(100% - 6px),
|
|
923
|
+
calc(100% - 6px) 100%,
|
|
924
|
+
0 100%,
|
|
925
|
+
0 6px
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* ────────────────────────────────────────────────────────────────────────
|
|
930
|
+
Sidebar: DS hairline on the conversation-history column
|
|
931
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
932
|
+
[data-sidebar-collapsed] > div:first-child {
|
|
933
|
+
border-right: 1px solid rgba(140, 170, 215, 0.22);
|
|
934
|
+
box-shadow: inset -1px 0 0 rgba(255, 180, 84, 0.06);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* Sidebar buttons (New Session etc.): DS chamfered corners. */
|
|
938
|
+
[data-slot="sidebar"] button {
|
|
939
|
+
border-radius: 0 !important;
|
|
940
|
+
clip-path: polygon(
|
|
941
|
+
6px 0,
|
|
942
|
+
100% 0,
|
|
943
|
+
100% calc(100% - 6px),
|
|
944
|
+
calc(100% - 6px) 100%,
|
|
945
|
+
0 100%,
|
|
946
|
+
0 6px
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/* Workspace rows (workspaces, sessions, groups): DS chamfered entries. */
|
|
951
|
+
[role="treeitem"] {
|
|
952
|
+
border-radius: 0 !important;
|
|
953
|
+
clip-path: polygon(
|
|
954
|
+
5px 0,
|
|
955
|
+
100% 0,
|
|
956
|
+
100% calc(100% - 5px),
|
|
957
|
+
calc(100% - 5px) 100%,
|
|
958
|
+
0 100%,
|
|
959
|
+
0 5px
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
799
963
|
/* ────────────────────────────────────────────────────────────────────────
|
|
800
964
|
DS chamfer everywhere else: kill the round-corner language
|
|
801
965
|
──────────────────────────────────────────────────────────────────────── */
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client/ecg.ts","../src/client/HeartLine.tsx","../src/client/locales.ts","../src/client/style.ts","../src/client/index.ts"],"sourcesContent":["/**\n * CHIRAL PULSE — ECG waveform synthesis.\n *\n * A cardiac cycle is a pure function of beat phase in [0,1): the classic\n * P-QRS-T complex as a sum of wrapped gaussian bumps. The monitor line is a\n * scrolling window over the time axis: the right edge shows the current\n * instant, the window spans `cycles` beats of history. BPM is the phase\n * clock speed, so the whole rhythm accelerates and slows with activity.\n */\n\n/** One wrapped gaussian bump: peak at `center` with `width`, amplitude `amp`. */\nfunction bump(phase: number, center: number, width: number, amp: number): number {\n let d = phase - center\n d -= Math.round(d)\n return amp * Math.exp(-(d * d) / (2 * width * width))\n}\n\n/**\n * Sample one cardiac cycle at beat phase in [0,1). Output range ≈ [-0.35, 1].\n * @param phase - beat phase, any real value (wrapping is internal).\n * @returns the waveform amplitude at that phase.\n */\nexport function ecgValue(phase: number): number {\n return (\n bump(phase, 0.14, 0.030, 0.16) // P wave\n - bump(phase, 0.30, 0.011, 0.26) // Q dip\n + bump(phase, 0.335, 0.012, 1.0) // R spike (wide enough to survive sampling)\n - bump(phase, 0.375, 0.011, 0.34) // S dip\n + bump(phase, 0.52, 0.048, 0.26) // T wave\n + bump(phase, 0.80, 0.012, 0.05) // U ripple\n )\n}\n\n/** One rendered frame of the scrolling monitor line. */\nexport interface EcgFrame {\n /** `x,y` pairs for an SVG polyline `points` attribute, rightmost = now. */\n points: string\n /** The y of the leading (rightmost) sample, in view units. */\n headY: number\n}\n\n/**\n * Build the polyline points for the scrolling window.\n *\n * Hospital monitor semantics: the paper moves at a FIXED speed — every pixel\n * represents a fixed amount of wall-clock time, so the trace scrolls left at\n * a constant rate regardless of heart rate. What changes with BPM is the\n * density of QRS complexes across that fixed window: a fast heart packs more\n * beats onto the screen, a resting one spaces them out.\n *\n * @param nowMs - wall-clock sample time (drives the scan).\n * @param bpm - current heart rate (beat density only; never the scan speed).\n * @param width - view width in user units (x spans 0..width).\n * @param height - view height in user units.\n * @param windowSeconds - how many seconds of signal the window shows.\n * @param step - x sampling step in user units.\n * @returns the frame.\n */\nexport function buildEcgFrame(\n nowMs: number,\n bpm: number,\n width: number,\n height: number,\n windowSeconds: number,\n step = 2,\n): EcgFrame {\n const mid = height / 2\n const amp = height * 0.44\n // Fixed paper speed: seconds per user-unit pixel.\n const secondsPerPixel = windowSeconds / width\n const tNow = nowMs / 1_000\n // Slow baseline wander, like a real monitor: two incommensurate sines keep\n // the resting trace from freezing into a straight flatline.\n const wander = 0.05 * Math.sin(tNow * 0.6)\n + 0.035 * Math.sin(tNow * 1.7 + 1.3)\n const points: string[] = []\n let headY = mid\n for (let x = 0; x <= width; x += step) {\n // x → absolute time: the right edge is \"now\", leftward is the past at a\n // constant rate. Sub-step peak guard: also sample the midpoints so a\n // narrow R spike between two samples still paints at full height.\n const tX = tNow - (width - x) * secondsPerPixel\n const phase = ((tX * (bpm / 60)) % 1 + 1) % 1\n let v = ecgValue(phase)\n if (step > 1) {\n const tMid = tX - secondsPerPixel * step * 0.5\n const phaseMid = ((tMid * (bpm / 60)) % 1 + 1) % 1\n v = Math.max(v, ecgValue(phaseMid))\n }\n const y = mid - (v + wander) * amp\n points.push(`${x.toFixed(1)},${y.toFixed(1)}`)\n if (x === width - step) headY = y\n }\n return { points: points.join(' '), headY }\n}\n\n/**\n * Steady-state BPM for a measured activity rate.\n * @param stepsPerMinute - measured step cadence (steps / minute over a window).\n * @returns target BPM within [42, 150] — a resting BB sleeps at 42, full\n * sprint peaks at 150.\n */\nexport function bpmForActivity(stepsPerMinute: number): number {\n return Math.min(150, Math.max(42, 42 + stepsPerMinute * 6))\n}\n","/**\n * HeartLine — the CHIRAL PULSE monitor strip, docked above the composer\n * (`conversation.input.dock`). A 26px \"monitor paper feed\": the scrolling\n * ECG waveform is the hero, flanked by the BPM read and the status word.\n * No duplicated figures — StatsLine already shows turns/tokens.\n *\n * The pulse is LIVE, not decorative:\n * - `partial` non-null → the model is thinking/generating → +38 BPM\n * - `runningCalls` non-empty → a tool is executing → +52 BPM\n * - `running` (session turn in flight) → +10 BPM\n * - otherwise the 10s step-window activity rate sets the base (~42 idle)\n * The BPM target is smoothed with a lerp; the paper speed stays FIXED and\n * only the beat density changes — hospital monitor semantics.\n *\n * Rendering: a single <canvas> redrawn per rAF at full frame rate. Fixed\n * memory (one canvas the size of the strip), no DOM attribute churn, no\n * string building — the trace is ~width straight segments per frame, which\n * is far cheaper than SVG polyline swaps and cannot stutter from throttling.\n */\nimport { useEffect, useRef, useState } from 'react'\nimport type {\n PropsLocale, PropsRuntime,\n} from '@deepseek-ai/dsh-client-ui-slots'\nimport type { SessionProjectionMap } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: merges the sessionStats key into SessionProjectionMap.\nimport type {} from '@deepseek-ai/dsh-session-stats/client'\nimport { ecgValue } from './ecg.ts'\nimport type { ChiralKey } from './locales.ts'\nimport { NS } from './locales.ts'\n\n/** Full props: the input-dock runtime seat plus the locale seat. */\nexport type HeartLineProps = PropsRuntime<'conversation.input.dock'> & PropsLocale<typeof NS>\n\n/** Monitor view height, CSS px. */\nconst ECG_HEIGHT = 22\n/** Seconds of signal shown across the window (fixed paper speed). */\nconst ECG_WINDOW = 5\n/** Activity window for the step-rate base, ms. */\nconst ACTIVITY_WINDOW_MS = 10_000\n/** Rotating status lines (locale keys), one every STATUS_ROTATE_S ticks. */\nconst STATUS_KEYS: readonly ChiralKey[] = [\n 'status.stable', 'status.bonded', 'status.chiral', 'status.doom',\n 'status.keep', 'status.voidout', 'status.odradek',\n]\nconst STATUS_ROTATE_S = 4\n/** BPM boost while the model is streaming a partial (thinking/generating). */\nconst BOOST_THINKING = 38\n/** BPM boost while a tool call is running. */\nconst BOOST_TOOL = 52\n/** BPM boost while the session turn is simply in flight. */\nconst BOOST_RUNNING = 10\n/** BPM floor (a resting BB) and ceiling. */\nconst BPM_FLOOR = 42\nconst BPM_CEIL = 150\n\n/** Trace color by activity mode: idle amber, thinking cyan, tool orange, run warm. */\nconst MODE_COLOR = {\n idle: '#ffb454',\n think: '#6fdbe2',\n tool: '#ff7a4d',\n run: '#ffc46b',\n} as const\ntype Mode = keyof typeof MODE_COLOR\n\n/** One activity sample: (time, steps) at a projection update. */\ninterface StepSample {\n t: number\n steps: number\n}\n\n/** Tail of the model's in-flight output: last non-empty text/reasoning block, whitespace-flattened. */\nfunction streamingTail(blocks: readonly { kind: string; text?: string }[]): string {\n for (let i = blocks.length - 1; i >= 0; i -= 1) {\n const block = blocks[i]\n const text = block.text\n if (text !== undefined && text.trim() !== '') {\n return text.replace(/\\s+/g, ' ').trim()\n }\n }\n return ''\n}\n\n/**\n * The CHIRAL PULSE dock entry.\n * @param props - runtime seat (useSession, useProjection) plus the locale seat.\n * @returns the monitor strip.\n */\nexport function HeartLine({ useSession, useProjection, t }: HeartLineProps) {\n const stats = useProjection('sessionStats') as SessionProjectionMap['sessionStats'] | undefined\n // Live activity + real model state: streaming output tail, running tool\n // name, turn in flight, and the last agent error.\n const live = useSession(s => ({\n partial: s.partial !== null,\n partialText: s.partial === null ? '' : streamingTail(s.partial.blocks),\n toolName: s.runningCalls[0]?.name ?? null,\n running: s.running,\n error: s.lastAgentError,\n }))\n\n const steps = stats?.steps ?? 0\n\n // ── BPM engine: step-window base + live activity boost ────────────────\n // targetRef updates once per second (activity readout); bpmRef eases toward\n // it EVERY FRAME inside paint, so the trace phase never jumps — a stepped\n // BPM would snap the whole waveform sideways at every tick.\n const bpmRef = useRef(BPM_FLOOR)\n const targetRef = useRef(BPM_FLOOR)\n const samplesRef = useRef<StepSample[]>([])\n const lastStepsRef = useRef(steps)\n const liveRef = useRef(live)\n liveRef.current = live\n const modeRef = useRef<Mode>('idle')\n const [ui, setUi] = useState({ bpm: BPM_FLOOR, elapsed: 0, mode: 'idle' as Mode })\n useEffect(() => {\n if (steps !== lastStepsRef.current) {\n lastStepsRef.current = steps\n samplesRef.current.push({ t: performance.now(), steps })\n }\n }, [steps])\n\n useEffect(() => {\n const id = window.setInterval(() => {\n const now = performance.now()\n const samples = samplesRef.current\n while (samples.length > 0 && now - samples[0].t > ACTIVITY_WINDOW_MS) samples.shift()\n const first = samples[0]\n const span = first === undefined ? 0 : now - first.t\n const delta = first === undefined ? 0 : lastStepsRef.current - first.steps\n const perMinute = span > 0 ? (delta / span) * 60_000 : 0\n const base = Math.min(BPM_CEIL, Math.max(BPM_FLOOR, 42 + perMinute * 6))\n const act = liveRef.current\n const boost = (act.toolName !== null ? BOOST_TOOL : 0)\n + (act.partial ? BOOST_THINKING : 0)\n + (act.running ? BOOST_RUNNING : 0)\n targetRef.current = Math.min(BPM_CEIL, Math.max(BPM_FLOOR, base + boost))\n const mode: Mode = act.toolName !== null ? 'tool' : act.partial ? 'think' : act.running ? 'run' : 'idle'\n modeRef.current = mode\n setUi(current => ({\n bpm: Math.round(bpmRef.current),\n elapsed: current.elapsed + 1,\n mode,\n }))\n }, 1_000)\n return () => { window.clearInterval(id) }\n }, [])\n\n // ── Canvas trace: one fixed-size canvas, redrawn per rAF ──────────────\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const ctxRef = useRef<CanvasRenderingContext2D | null>(null)\n const widthRef = useRef(640)\n const dprRef = useRef(1)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (canvas === null) return\n const ctx = canvas.getContext('2d')\n if (ctx === null) return\n ctxRef.current = ctx\n\n const applySize = (): void => {\n const dpr = window.devicePixelRatio || 1\n dprRef.current = dpr\n canvas.width = Math.max(120, Math.round(widthRef.current * dpr))\n canvas.height = Math.round(ECG_HEIGHT * dpr)\n }\n applySize()\n const observer = new ResizeObserver((entries) => {\n const width = Math.max(120, Math.round(entries[0]?.contentRect.width ?? 640))\n if (width !== widthRef.current) {\n widthRef.current = width\n applySize()\n }\n })\n observer.observe(canvas)\n\n let lastBpmFrame = 0\n const paint = (now: number): void => {\n // Frame-rate BPM easing: the target updates once per second, but the\n // trace phase must move continuously or the whole waveform snaps.\n if (lastBpmFrame === 0) lastBpmFrame = now\n const dt = (now - lastBpmFrame) / 1_000\n lastBpmFrame = now\n bpmRef.current += (targetRef.current - bpmRef.current) * Math.min(1, dt * 0.8)\n const c = canvasRef.current\n const g = ctxRef.current\n if (c === null || g === null) return\n const w = widthRef.current\n const h = ECG_HEIGHT\n const dpr = dprRef.current\n g.setTransform(dpr, 0, 0, dpr, 0, 0)\n g.clearRect(0, 0, w, h)\n\n const tNow = now / 1_000\n const secondsPerPixel = ECG_WINDOW / w\n const mid = h / 2\n const amp = h * 0.44\n const wander = 0.05 * Math.sin(tNow * 0.6) + 0.035 * Math.sin(tNow * 1.7 + 1.3)\n const bpm = bpmRef.current\n const yAt = (x: number): number => {\n const tX = tNow - (w - x) * secondsPerPixel\n const phase = ((tX * (bpm / 60)) % 1 + 1) % 1\n return mid - (ecgValue(phase) + wander) * amp\n }\n\n // Chiral ghost: the same trace offset by 3px, faint cyan.\n g.beginPath()\n for (let x = 0; x <= w; x += 1) {\n const y = yAt(x)\n if (x === 0) g.moveTo(x + 3, y)\n else g.lineTo(x + 3, y)\n }\n g.strokeStyle = 'rgba(111, 219, 226, 0.18)'\n g.lineWidth = 1\n g.stroke()\n\n // Main trace.\n g.beginPath()\n for (let x = 0; x <= w; x += 1) {\n const y = yAt(x)\n if (x === 0) g.moveTo(x, y)\n else g.lineTo(x, y)\n }\n g.strokeStyle = MODE_COLOR[modeRef.current]\n g.lineWidth = 1.4\n g.lineJoin = 'round'\n g.lineCap = 'round'\n g.stroke()\n\n // Scan head: halo + dot at the leading edge.\n const headY = yAt(w)\n g.fillStyle = MODE_COLOR[modeRef.current]\n g.globalAlpha = 0.22\n g.beginPath()\n g.arc(w - 2, headY, 5, 0, Math.PI * 2)\n g.fill()\n g.globalAlpha = 1\n g.beginPath()\n g.arc(w - 2, headY, 1.6, 0, Math.PI * 2)\n g.fill()\n }\n\n if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n paint(performance.now()) // one static frame\n return () => { observer.disconnect() }\n }\n let raf = 0\n const loop = (now: number): void => {\n raf = requestAnimationFrame(loop)\n paint(now)\n }\n raf = requestAnimationFrame(loop)\n return () => {\n observer.disconnect()\n cancelAnimationFrame(raf)\n }\n }, [])\n\n // Status word: real model state wins; the flavor rotation only plays while idle.\n const flavor = STATUS_KEYS[Math.floor(ui.elapsed / STATUS_ROTATE_S) % STATUS_KEYS.length]\n const status = live.error !== null\n ? `⚠ ${live.error.slice(0, 16)}`\n : live.toolName !== null\n ? `EXEC · ${live.toolName}`\n : live.partialText !== ''\n ? `⇢ ${live.partialText.slice(-18)}`\n : t(flavor)\n\n return (\n <div className=\"cp-line\" role=\"group\" aria-label={t('line.aria')} data-chiral-pulse data-mode={ui.mode}>\n <div className=\"cp-lineBpm\">\n {ui.bpm}\n <span className=\"cp-lineBpmUnit\">{t('bpm.unit')}</span>\n </div>\n\n <canvas ref={canvasRef} className=\"cp-lineEcg\" aria-hidden />\n\n <div className=\"cp-lineReadout\">\n <div className=\"cp-lineStatus\" title={status}>{status}</div>\n </div>\n </div>\n )\n}\n","/**\n * CHIRAL PULSE — dictionary namespace.\n *\n * The DS monitor idiom stays English in both locales (it is part of the\n * aesthetic: \"LINK STABLE\", \"TIME TO COMPLETION\"); the zh side translates\n * the labels a user actually reads.\n */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'chiral'\n\n/** Dictionary keys of the `chiral` namespace (string-literal union). */\nexport type ChiralKey =\n | 'line.aria'\n | 'bpm.unit'\n | 'status.stable'\n | 'status.bonded'\n | 'status.chiral'\n | 'status.doom'\n | 'status.keep'\n | 'status.voidout'\n | 'status.odradek'\n\n/** English dictionary. */\nexport const en: Record<ChiralKey, string> = {\n 'line.aria': 'BB vital-signs strip — CHIRAL PULSE',\n 'bpm.unit': 'BPM',\n 'status.stable': 'LINK STABLE',\n 'status.bonded': 'BB BONDED',\n 'status.chiral': 'CHIRAL DENSITY: NOMINAL',\n 'status.doom': 'DOOMS LEVEL: 0',\n 'status.keep': 'KEEP ON KEEPING ON',\n 'status.voidout': 'NO VOIDOUT DETECTED',\n 'status.odradek': 'ODRADEK SYNC: OK',\n}\n\n/** Chinese dictionary. */\nexport const zh: Record<ChiralKey, string> = {\n 'line.aria': 'BB 生命体征走纸 — CHIRAL PULSE 手性脉冲',\n 'bpm.unit': '次/分',\n 'status.stable': '链路稳定',\n 'status.bonded': 'BB 连接完成',\n 'status.chiral': '手性密度:正常',\n 'status.doom': 'DOOMS 等级:0',\n 'status.keep': '继续前进 · KEEP ON KEEPING ON',\n 'status.voidout': '未检测到虚爆',\n 'status.odradek': '奥卓克同步:正常',\n}\n","/**\n * CHIRAL PULSE — the Death Stranding sheet, two layers.\n *\n * LAYER 1 — the global skin. The whole app paints from `--dsw-*` variables\n * (ui-theme's design platform: alias tokens reference static tokens, so\n * remapping the palette re-skins every component without touching its\n * structure). This sheet FORCES the DS look under BOTH theme modes: a deep\n * blue-black machine body, cold blue-grey hairlines, amber reserved for\n * emphasis (the heartbeat waveform, hover blooms) — and the deepseek brand\n * blues are left untouched, so the whale mark stays DeepSeek blue.\n *\n * LAYER 2 — the atmosphere. A fixed full-viewport CRT scanline weave, a\n * faint chiral lattice, and a vignette, all pointer-transparent. Plus the\n * BB vital-signs strip that docks under the composer stats: a 26px monitor\n * paper feed whose scrolling ECG is the hero, with the BPM and status read.\n *\n * Every rule is scoped under `.cp-*` (except the token remap, which must\n * target `body`), rides one owned <style data-plugin> tag, and the loader\n * removes it on unload.\n */\n\nexport const CHIRAL_CSS = `\n/* ────────────────────────────────────────────────────────────────────────\n LAYER 1 · global DS skin — dark blue-black, both theme modes\n ──────────────────────────────────────────────────────────────────────── */\n\n/* Alias-level remap: independent of the static scale's role flip between\n themes, so the DS look is identical under light and dark settings. */\nbody[data-ds-dark-theme],\nbody:not([data-ds-dark-theme]) {\n /* machine body — blue-grey with air, not a black void */\n --dsw-alias-bg-base: rgb(13, 17, 23);\n --dsw-alias-bg-layer-1: rgb(17, 22, 29);\n --dsw-alias-bg-layer-2: rgb(21, 27, 35);\n --dsw-alias-bg-layer-3: rgb(26, 33, 42);\n --dsw-alias-bg-overlay: rgb(31, 40, 51);\n --dsw-alias-bg-mask-1: rgba(0, 0, 0, 0.5);\n --dsw-alias-bg-mask-2: rgba(0, 0, 0, 0.22);\n --dsw-alias-bg-mask-3: rgba(0, 0, 0, 0.5);\n --dsw-alias-bg-mask-drop: rgba(10, 14, 19, 0.72);\n --dsw-alias-bg-skeleton: rgba(150, 170, 200, 0.07);\n\n /* text: cold blue-grey */\n --dsw-alias-label-primary: rgb(235, 240, 246);\n --dsw-alias-label-secondary: rgb(178, 190, 205);\n --dsw-alias-label-tertiary: rgb(140, 153, 170);\n --dsw-alias-label-caption: rgb(108, 122, 141);\n --dsw-alias-label-dimmed: rgb(94, 108, 127);\n --dsw-alias-label-primary-dimmed: rgb(205, 213, 222);\n --dsw-alias-label-primary-inverted: rgb(235, 240, 246);\n --dsw-alias-label-primary-foreground: rgb(13, 17, 23);\n --dsw-alias-brand-text: rgb(235, 240, 246);\n --dsw-alias-brand-primary: rgb(103, 158, 254);\n\n /* hairlines: cold blue, readable against the body */\n --dsw-alias-border-l1: rgba(140, 170, 215, 0.15);\n --dsw-alias-border-l2: rgba(140, 170, 215, 0.24);\n --dsw-alias-border-l2-darkmode-thin: rgba(140, 170, 215, 0.19);\n --dsw-alias-border-l3: rgba(140, 170, 215, 0.34);\n --dsw-alias-border-l4: rgba(140, 170, 215, 0.46);\n --dsw-alias-border-inverted: rgba(255, 255, 255, 0.09);\n --dsw-alias-border-inverted2: rgba(255, 255, 255, 0.11);\n\n /* hovers: amber bloom, kept subtle */\n --dsw-alias-interactive-bg-hover: rgba(255, 180, 84, 0.08);\n --dsw-alias-interactive-bg-active: rgba(255, 180, 84, 0.12);\n --dsw-alias-interactive-bg-hover-accent: rgba(255, 180, 84, 0.14);\n --dsw-alias-interactive-bg-hover-solid: rgb(22, 29, 38);\n --dsw-alias-interactive-bg-hover-danger: rgba(242, 90, 90, 0.14);\n\n /* buttons: brand blue stays the primary action */\n --dsw-alias-button-primary-dimmed: rgb(30, 40, 53);\n --dsw-alias-button-primary-hover: rgb(124, 172, 255);\n --dsw-alias-button-ghost-active-fill: rgb(22, 29, 38);\n --dsw-alias-button-ghost-active-hover: rgb(27, 35, 46);\n --dsw-alias-button-ghost-active-border: rgb(140, 170, 215);\n --dsw-alias-button-floating-fill: rgb(19, 25, 33);\n --dsw-alias-button-floating-hover: rgb(24, 31, 41);\n --dsw-alias-button-elevated-fill: rgb(22, 29, 38);\n --dsw-alias-button-contrast-fill: rgb(205, 213, 222);\n --dsw-alias-button-tool-bar-fill: rgba(140, 170, 215, 0.24);\n --dsw-alias-button-tool-bar-fill-invisible: rgba(140, 170, 215, 0.13);\n --dsw-alias-button-tool-bar-hover: rgba(140, 170, 215, 0.32);\n\n /* surfaces */\n --dsw-specific-sidebar-fill: rgb(9, 12, 17);\n --dsw-specific-sidebar-nav-item-active: rgb(20, 27, 36);\n --dsw-specific-sidebar-nav-item-active-accent: rgb(27, 36, 48);\n --dsw-specific-sidebar-nav-item-hover: rgb(15, 20, 28);\n --dsw-specific-bubble: rgb(18, 24, 32);\n --dsw-specific-bubble-highlight: rgb(24, 32, 42);\n --dsw-specific-input-major: rgb(15, 20, 27);\n --dsw-specific-login-input: rgb(12, 16, 22);\n --dsw-specific-menu: rgb(21, 27, 35);\n --dsw-specific-selector: rgb(20, 26, 34);\n --dsw-specific-tip: rgb(16, 21, 28);\n --dsw-alias-markdown-code-block: rgb(10, 14, 19);\n --dsw-alias-markdown-code-block-banner: rgb(13, 17, 23);\n --dsw-alias-markdown-inline-code: rgb(18, 24, 32);\n --dsw-alias-markdown-code-segment-selected: rgb(16, 21, 28);\n --dsw-alias-markdown-code-segment-unselected: rgb(12, 16, 22);\n --dsw-alias-markdown-placeholder: rgb(16, 21, 28);\n --dsw-alias-markdown-tag: rgb(18, 24, 32);\n --dsw-alias-markdown-citation: rgb(22, 29, 38);\n\n /* floats */\n --dsw-alias-toast-bg: rgb(22, 29, 38);\n --dsw-alias-tooltip-bg: rgb(20, 26, 34);\n --dsw-alias-scrollbar-bg-l1: rgb(13, 18, 25);\n --dsw-alias-scrollbar-bg-l2: rgb(17, 23, 31);\n --dsw-alias-scrollbar-hover-l1: rgb(34, 44, 58);\n --dsw-alias-scrollbar-hover-l2: rgb(42, 54, 70);\n\n /* status: amber stays the warn/emphasis hue; success leans chiral cyan */\n --dsw-alias-state-warn-primary: rgb(245, 158, 11);\n --dsw-alias-state-warn-secondary: rgb(247, 173, 49);\n --dsw-alias-state-warn-label: rgb(221, 134, 41);\n --dsw-alias-state-warn-tertiary: rgb(39, 36, 31);\n --dsw-alias-state-success-primary: rgb(52, 205, 168);\n --dsw-alias-state-success-secondary: rgb(94, 222, 189);\n --dsw-alias-state-success-tertiary: rgb(12, 28, 24);\n --dsw-static-green-400: rgb(94, 222, 189);\n --dsw-static-green-500: rgb(52, 205, 168);\n}\n\n/* Focus ring: amber, the DS highlight color. */\n:focus-visible {\n outline: 1px solid rgba(255, 180, 84, 0.65) !important;\n outline-offset: 2px;\n}\n\n/* Ambient bloom behind the app. */\nbody {\n background-image:\n radial-gradient(1100px 620px at 12% -8%, rgba(111, 219, 226, 0.04), transparent 60%),\n radial-gradient(900px 560px at 108% 112%, rgba(103, 158, 254, 0.05), transparent 60%);\n background-attachment: fixed;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n LAYER 2 · atmosphere overlays (injected as fixed elements)\n ──────────────────────────────────────────────────────────────────────── */\n.cp-atmo {\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 2147483000;\n}\n.cp-atmo-scanlines {\n background: repeating-linear-gradient(\n 0deg,\n rgba(255, 255, 255, 0.024) 0 1px,\n transparent 1px 3px\n );\n mix-blend-mode: overlay;\n}\n.cp-atmo-lattice {\n opacity: 0.55;\n background:\n repeating-linear-gradient(60deg, transparent 0 17px, rgba(103, 158, 254, 0.03) 17px 18px),\n repeating-linear-gradient(120deg, transparent 0 17px, rgba(111, 219, 226, 0.028) 17px 18px);\n}\n.cp-atmo-vignette {\n background: radial-gradient(120% 100% at 50% 40%, transparent 55%, rgba(0, 0, 0, 0.24) 100%);\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n BB vital-signs strip · the heartbeat paper feed\n ──────────────────────────────────────────────────────────────────────── */\n.cp-line {\n --cp-amber: #ffb454;\n --cp-amber-bright: #ffd9a0;\n --cp-cyan: #6fdbe2;\n --cp-dim: #64727f;\n display: flex;\n align-items: center;\n gap: 12px;\n height: 26px;\n margin: 3px 0 4px;\n padding: 0 10px;\n border: 1px solid rgba(140, 170, 215, 0.26);\n border-radius: 0;\n background:\n linear-gradient(115deg, rgba(140, 190, 255, 0.06) 0%, transparent 30%),\n linear-gradient(180deg, rgba(14, 19, 26, 0.92), rgba(9, 13, 18, 0.94));\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.05),\n inset 0 0 16px rgba(103, 158, 254, 0.06);\n color: #c9d3dc;\n font-family: ui-monospace, \"Cascadia Mono\", \"JetBrains Mono\", Consolas, \"Courier New\", monospace;\n overflow: hidden;\n clip-path: polygon(\n 7px 0,\n 100% 0,\n 100% calc(100% - 7px),\n calc(100% - 7px) 100%,\n 0 100%,\n 0 7px\n );\n}\n\n.cp-lineBpm {\n flex: none;\n min-width: 42px;\n font-size: 11px;\n line-height: 1;\n letter-spacing: 0.5px;\n color: var(--cp-amber-bright);\n text-shadow: 0 0 10px rgba(255, 180, 84, 0.5);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n.cp-lineBpmUnit {\n font-size: 7px;\n letter-spacing: 1px;\n color: var(--cp-dim);\n margin-left: 2px;\n}\n\n.cp-lineEcg {\n flex: 1 1 auto;\n min-width: 100px;\n height: 22px;\n display: block;\n border-radius: 0;\n border: 1px solid rgba(140, 170, 215, 0.16);\n /* Static paper grid lives in CSS; the canvas above it only paints the trace. */\n background:\n repeating-linear-gradient(0deg, rgba(140, 170, 215, 0.07) 0 1px, transparent 1px 11px),\n repeating-linear-gradient(90deg, rgba(140, 170, 215, 0.06) 0 1px, transparent 1px 11px),\n rgba(7, 10, 15, 0.55);\n}\n\n.cp-lineReadout {\n flex: none;\n width: 132px;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n overflow: hidden;\n}\n.cp-lineStatus {\n font-size: 8px;\n letter-spacing: 1.8px;\n text-transform: uppercase;\n color: var(--cp-cyan);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 100%;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .cp-lineEcg {\n opacity: 0.9;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n ECG paper grid + activity-mode color coupling\n ──────────────────────────────────────────────────────────────────────── */\n.cp-line[data-mode=\"think\"] .cp-lineBpm {\n color: #7fe3e8;\n text-shadow: 0 0 10px rgba(111, 219, 226, 0.55);\n}\n.cp-line[data-mode=\"tool\"] .cp-lineBpm {\n color: #ff9b7a;\n text-shadow: 0 0 10px rgba(255, 122, 77, 0.6);\n}\n.cp-line[data-mode=\"run\"] .cp-lineBpm {\n color: #ffd9a0;\n}\n.cp-line[data-mode=\"think\"] .cp-lineStatus,\n.cp-line[data-mode=\"tool\"] .cp-lineStatus {\n color: #9fe8ec;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Message-flow dressing — DS glyphs on each node kind\n ──────────────────────────────────────────────────────────────────────── */\n[data-chat-flow-kind] {\n position: relative;\n}\n[data-chat-flow-kind=\"assistant\"] {\n padding-left: 18px;\n}\n[data-chat-flow-kind=\"assistant\"]::before {\n content: \"✦\";\n position: absolute;\n left: 4px;\n top: 12px;\n color: rgba(255, 180, 84, 0.85);\n font-size: 11px;\n line-height: 1;\n text-shadow: 0 0 8px rgba(255, 180, 84, 0.6);\n}\n[data-chat-flow-kind=\"assistant\"]::after {\n content: \"\";\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 1px;\n background: linear-gradient(180deg, transparent, rgba(255, 180, 84, 0.35), transparent);\n}\n[data-chat-flow-kind=\"user\"],\n[data-chat-flow-kind=\"steering\"] {\n padding-right: 18px;\n}\n[data-chat-flow-kind=\"user\"]::before,\n[data-chat-flow-kind=\"steering\"]::before {\n content: \"▸▸\";\n position: absolute;\n right: 2px;\n top: 4px;\n color: rgba(120, 150, 195, 0.75);\n font-size: 10px;\n line-height: 1;\n letter-spacing: -1px;\n}\n[data-chat-flow-kind=\"context\"] {\n padding-left: 16px;\n}\n[data-chat-flow-kind=\"context\"]::before {\n content: \"⇢\";\n position: absolute;\n left: 2px;\n top: 12px;\n color: rgba(111, 219, 226, 0.7);\n font-size: 11px;\n line-height: 1;\n}\n[data-variant=\"think\"] {\n border-left: 2px solid rgba(111, 219, 226, 0.35);\n padding-left: 10px;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Tool-card chassis — the ui-primitives block family gets a DS case\n ──────────────────────────────────────────────────────────────────────── */\n[data-tool],\n[data-search],\n[data-read],\n[data-web],\n[data-diff],\n[data-terminal],\n[data-context-injection-body] {\n border: 1px solid rgba(140, 170, 215, 0.24) !important;\n border-radius: 0 !important;\n background:\n linear-gradient(115deg, rgba(140, 190, 255, 0.06) 0%, transparent 30%),\n linear-gradient(180deg, rgba(15, 20, 27, 0.88), rgba(9, 13, 18, 0.92)) !important;\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.05),\n inset 0 0 18px rgba(103, 158, 254, 0.05);\n position: relative;\n clip-path: polygon(\n 7px 0,\n 100% 0,\n 100% calc(100% - 7px),\n calc(100% - 7px) 100%,\n 0 100%,\n 0 7px\n );\n}\n[data-terminal]::before {\n content: \"❯_\";\n position: absolute;\n right: 8px;\n top: 6px;\n color: rgba(111, 219, 226, 0.35);\n font-size: 10px;\n font-family: ui-monospace, Consolas, monospace;\n}\n[data-read]::before {\n content: \"▤\";\n position: absolute;\n right: 8px;\n top: 6px;\n color: rgba(120, 150, 195, 0.4);\n font-size: 11px;\n}\n[data-search]::before {\n content: \"⌕\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(255, 180, 84, 0.4);\n font-size: 13px;\n}\n[data-web]::before {\n content: \"⌖\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(111, 219, 226, 0.4);\n font-size: 12px;\n}\n[data-diff]::before {\n content: \"⇄\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(120, 150, 195, 0.4);\n font-size: 12px;\n}\n[data-tool]::before {\n content: \"⚙\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(255, 180, 84, 0.4);\n font-size: 11px;\n}\n[data-context-injection-body]::before {\n content: \"⇢\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(111, 219, 226, 0.4);\n font-size: 11px;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Composer details\n ──────────────────────────────────────────────────────────────────────── */\n[data-composer-seat] textarea {\n caret-color: #ffb454;\n letter-spacing: 0.01em;\n}\n[data-composer-seat] textarea:focus {\n caret-color: #ffd9a0;\n}\n[data-composer-seat] {\n box-shadow:\n inset 0 0 0 1px rgba(140, 170, 215, 0.22),\n inset 0 1px 0 rgba(255, 255, 255, 0.06),\n inset 0 -12px 30px rgba(0, 0, 0, 0.25);\n border-radius: 0;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Dialogs, menus, tooltips, toasts — the floating DS surfaces\n ──────────────────────────────────────────────────────────────────────── */\n[role=\"dialog\"] {\n border: 1px solid rgba(255, 180, 84, 0.32) !important;\n border-radius: 0 !important;\n background: linear-gradient(180deg, rgba(15, 20, 27, 0.98), rgba(10, 14, 19, 0.99)) !important;\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.07),\n inset 0 0 26px rgba(103, 158, 254, 0.06) !important;\n}\n[role=\"menu\"] {\n border: 1px solid rgba(140, 170, 215, 0.3) !important;\n border-radius: 0 !important;\n background: rgba(13, 18, 25, 0.97) !important;\n}\n[role=\"menuitem\"]:hover {\n background: rgba(255, 180, 84, 0.08) !important;\n}\n[role=\"tooltip\"] {\n border: 1px solid rgba(140, 170, 215, 0.32) !important;\n border-radius: 0 !important;\n background: rgba(15, 20, 27, 0.97) !important;\n}\n[role=\"alert\"] {\n border: 1px solid rgba(255, 180, 84, 0.38) !important;\n border-radius: 0 !important;\n background: rgba(15, 20, 27, 0.97) !important;\n clip-path: polygon(\n 8px 0,\n 100% 0,\n 100% calc(100% - 8px),\n calc(100% - 8px) 100%,\n 0 100%,\n 0 8px\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n DS chamfer everywhere else: kill the round-corner language\n ──────────────────────────────────────────────────────────────────────── */\nbutton,\ninput,\ntextarea,\nselect,\n[role=\"tab\"],\n[role=\"menuitem\"],\n[role=\"treeitem\"] {\n border-radius: 0 !important;\n}\n\n`\n","/**\n * CHIRAL PULSE, browser half: the Death Stranding skin plus the BB\n * vital-signs strip under the composer stats.\n *\n * Two contributions:\n * 1. The global DS skin 鈥?a `--dsw-*` token remap (blue-black machine body,\n * amber hairlines, sand-paper light variant), the DeepSeek whale mark's\n * brand blues untouched, plus three pointer-transparent atmosphere\n * overlays (CRT scanlines, chiral lattice, vignette).\n * 2. The heartbeat strip on `conversation.composer.dock` 鈥?a 26px monitor\n * paper feed whose scrolling ECG is the hero and whose BPM follows the\n * session's live activity (model streaming, tools executing).\n *\n * The plugin owns no state of its own beyond the component's local beat\n * engine; every figure arrives through the session standard kit. All styles\n * ride one owned <style data-plugin> tag so the loader removes them on\n * unload/reload.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-conversation SlotMap merge (the composer.dock entry).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { HeartLine } from './HeartLine.tsx'\nimport { en, NS, zh, type ChiralKey } from './locales.ts'\nimport { CHIRAL_CSS } from './style.ts'\n\nexport { HeartLine } from './HeartLine.tsx'\nexport type { HeartLineProps } from './HeartLine.tsx'\nexport type { ChiralKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The BB monitor strip's copy. */\n chiral: ChiralKey\n }\n}\n\n/** Required services: the slot registry and the locale service. */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: register dictionaries, inject the DS sheet and the\n * atmosphere overlays, and dock the heartbeat strip under the composer.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'chiral-pulse: dictionaries')\n\n ctx.effect(() => {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'chiral-pulse'\n tag.textContent = CHIRAL_CSS\n document.head.appendChild(tag)\n return () => { tag.remove() }\n }, 'chiral-pulse: styles')\n\n // Atmosphere overlays: scanlines + chiral lattice + vignette, all\n // pointer-transparent, riding the top of the stacking order.\n ctx.effect(() => {\n const layers = [\n { className: 'cp-atmo cp-atmo-scanlines', label: 'scanlines' },\n { className: 'cp-atmo cp-atmo-lattice', label: 'lattice' },\n { className: 'cp-atmo cp-atmo-vignette', label: 'vignette' },\n ]\n const nodes = layers.map(({ className }) => {\n const el = document.createElement('div')\n el.className = className\n el.setAttribute('aria-hidden', 'true')\n document.body.appendChild(el)\n return el\n })\n return () => {\n for (const el of nodes) el.remove()\n }\n }, 'chiral-pulse: atmosphere')\n\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\n name: 'conversation.input.dock',\n id: 'chiral-pulse',\n // Above the composer card, under the goal strip: the pulse feed rides\n // with the input it monitors.\n order: 20,\n locale: NS,\n }, HeartLine))\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;EAWA,SAAS,KAAK,OAAe,QAAgB,OAAe,KAAqB;GAC/E,IAAI,IAAI,QAAQ;GAChB,KAAK,KAAK,MAAM,CAAC;GACjB,OAAO,MAAM,KAAK,IAAI,EAAE,IAAI,MAAM,IAAI,QAAQ,MAAM;EACtD;;;;;;EAOA,SAAgB,SAAS,OAAuB;GAC9C,OACE,KAAK,OAAO,KAAM,KAAO,GAAI,IAC3B,KAAK,OAAO,IAAM,MAAO,GAAI,IAC7B,KAAK,OAAO,MAAO,MAAO,CAAG,IAC7B,KAAK,OAAO,MAAO,MAAO,GAAI,IAC9B,KAAK,OAAO,KAAM,MAAO,GAAI,IAC7B,KAAK,OAAO,IAAM,MAAO,GAAI;EAEnC;;;;;;;;;;;;;;;;;;;;;;;ECGA,MAAM,aAAa;;EAEnB,MAAM,aAAa;;EAEnB,MAAM,qBAAqB;;EAE3B,MAAM,cAAoC;GACxC;GAAiB;GAAiB;GAAiB;GACnD;GAAe;GAAkB;EACnC;EACA,MAAM,kBAAkB;;EAExB,MAAM,iBAAiB;;EAEvB,MAAM,aAAa;;EAEnB,MAAM,gBAAgB;;EAEtB,MAAM,YAAY;EAClB,MAAM,WAAW;;EAGjB,MAAM,aAAa;GACjB,MAAM;GACN,OAAO;GACP,MAAM;GACN,KAAK;EACP;;EAUA,SAAS,cAAc,QAA4D;GACjF,KAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;IAE9C,MAAM,OADQ,OAAO,EACH,CAAC;IACnB,IAAI,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,IACxC,OAAO,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;GAE1C;GACA,OAAO;EACT;;;;;;EAOA,SAAgB,UAAU,EAAE,YAAY,eAAe,KAAqB;GAC1E,MAAM,QAAQ,cAAc,cAAc;GAG1C,MAAM,OAAO,YAAW,OAAM;IAC5B,SAAS,EAAE,YAAY;IACvB,aAAa,EAAE,YAAY,OAAO,KAAK,cAAc,EAAE,QAAQ,MAAM;IACrE,UAAU,EAAE,aAAa,EAAE,EAAE,QAAQ;IACrC,SAAS,EAAE;IACX,OAAO,EAAE;GACX,EAAE;GAEF,MAAM,QAAQ,OAAO,SAAS;GAM9B,MAAM,UAAA,GAAA,MAAA,OAAA,CAAgB,SAAS;GAC/B,MAAM,aAAA,GAAA,MAAA,OAAA,CAAmB,SAAS;GAClC,MAAM,cAAA,GAAA,MAAA,OAAA,CAAkC,CAAC,CAAC;GAC1C,MAAM,gBAAA,GAAA,MAAA,OAAA,CAAsB,KAAK;GACjC,MAAM,WAAA,GAAA,MAAA,OAAA,CAAiB,IAAI;GAC3B,QAAQ,UAAU;GAClB,MAAM,WAAA,GAAA,MAAA,OAAA,CAAuB,MAAM;GACnC,MAAM,CAAC,IAAI,UAAA,GAAA,MAAA,SAAA,CAAkB;IAAE,KAAK;IAAW,SAAS;IAAG,MAAM;GAAe,CAAC;GACjF,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,UAAU,aAAa,SAAS;KAClC,aAAa,UAAU;KACvB,WAAW,QAAQ,KAAK;MAAE,GAAG,YAAY,IAAI;MAAG;KAAM,CAAC;IACzD;GACF,GAAG,CAAC,KAAK,CAAC;GAEV,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,KAAK,OAAO,kBAAkB;KAClC,MAAM,MAAM,YAAY,IAAI;KAC5B,MAAM,UAAU,WAAW;KAC3B,OAAO,QAAQ,SAAS,KAAK,MAAM,QAAQ,EAAE,CAAC,IAAI,oBAAoB,QAAQ,MAAM;KACpF,MAAM,QAAQ,QAAQ;KACtB,MAAM,OAAO,UAAU,KAAA,IAAY,IAAI,MAAM,MAAM;KACnD,MAAM,QAAQ,UAAU,KAAA,IAAY,IAAI,aAAa,UAAU,MAAM;KACrE,MAAM,YAAY,OAAO,IAAK,QAAQ,OAAQ,MAAS;KACvD,MAAM,OAAO,KAAK,IAAI,UAAU,KAAK,IAAI,WAAW,KAAK,YAAY,CAAC,CAAC;KACvE,MAAM,MAAM,QAAQ;KACpB,MAAM,SAAS,IAAI,aAAa,OAAO,aAAa,MAC/C,IAAI,UAAU,iBAAiB,MAC/B,IAAI,UAAU,gBAAgB;KACnC,UAAU,UAAU,KAAK,IAAI,UAAU,KAAK,IAAI,WAAW,OAAO,KAAK,CAAC;KACxE,MAAM,OAAa,IAAI,aAAa,OAAO,SAAS,IAAI,UAAU,UAAU,IAAI,UAAU,QAAQ;KAClG,QAAQ,UAAU;KAClB,OAAM,aAAY;MAChB,KAAK,KAAK,MAAM,OAAO,OAAO;MAC9B,SAAS,QAAQ,UAAU;MAC3B;KACF,EAAE;IACJ,GAAG,GAAK;IACR,aAAa;KAAE,OAAO,cAAc,EAAE;IAAE;GAC1C,GAAG,CAAC,CAAC;GAGL,MAAM,aAAA,GAAA,MAAA,OAAA,CAAsC,IAAI;GAChD,MAAM,UAAA,GAAA,MAAA,OAAA,CAAiD,IAAI;GAC3D,MAAM,YAAA,GAAA,MAAA,OAAA,CAAkB,GAAG;GAC3B,MAAM,UAAA,GAAA,MAAA,OAAA,CAAgB,CAAC;GAEvB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,SAAS,UAAU;IACzB,IAAI,WAAW,MAAM;IACrB,MAAM,MAAM,OAAO,WAAW,IAAI;IAClC,IAAI,QAAQ,MAAM;IAClB,OAAO,UAAU;IAEjB,MAAM,kBAAwB;KAC5B,MAAM,MAAM,OAAO,oBAAoB;KACvC,OAAO,UAAU;KACjB,OAAO,QAAQ,KAAK,IAAI,KAAK,KAAK,MAAM,SAAS,UAAU,GAAG,CAAC;KAC/D,OAAO,SAAS,KAAK,MAAM,aAAa,GAAG;IAC7C;IACA,UAAU;IACV,MAAM,WAAW,IAAI,gBAAgB,YAAY;KAC/C,MAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,MAAM,QAAQ,EAAE,EAAE,YAAY,SAAS,GAAG,CAAC;KAC5E,IAAI,UAAU,SAAS,SAAS;MAC9B,SAAS,UAAU;MACnB,UAAU;KACZ;IACF,CAAC;IACD,SAAS,QAAQ,MAAM;IAEvB,IAAI,eAAe;IACnB,MAAM,SAAS,QAAsB;KAGnC,IAAI,iBAAiB,GAAG,eAAe;KACvC,MAAM,MAAM,MAAM,gBAAgB;KAClC,eAAe;KACf,OAAO,YAAY,UAAU,UAAU,OAAO,WAAW,KAAK,IAAI,GAAG,KAAK,EAAG;KAC7E,MAAM,IAAI,UAAU;KACpB,MAAM,IAAI,OAAO;KACjB,IAAI,MAAM,QAAQ,MAAM,MAAM;KAC9B,MAAM,IAAI,SAAS;KACnB,MAAM,IAAI;KACV,MAAM,MAAM,OAAO;KACnB,EAAE,aAAa,KAAK,GAAG,GAAG,KAAK,GAAG,CAAC;KACnC,EAAE,UAAU,GAAG,GAAG,GAAG,CAAC;KAEtB,MAAM,OAAO,MAAM;KACnB,MAAM,kBAAkB,aAAa;KACrC,MAAM,MAAM,IAAI;KAChB,MAAM,MAAM,IAAI;KAChB,MAAM,SAAS,MAAO,KAAK,IAAI,OAAO,EAAG,IAAI,OAAQ,KAAK,IAAI,OAAO,MAAM,GAAG;KAC9E,MAAM,MAAM,OAAO;KACnB,MAAM,OAAO,MAAsB;MAGjC,OAAO,OAAO,WAFH,QAAQ,IAAI,KAAK,oBACN,MAAM,MAAO,IAAI,KAAK,CAChB,IAAI,UAAU;KAC5C;KAGA,EAAE,UAAU;KACZ,KAAK,IAAI,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG;MAC9B,MAAM,IAAI,IAAI,CAAC;MACf,IAAI,MAAM,GAAG,EAAE,OAAO,IAAI,GAAG,CAAC;WACzB,EAAE,OAAO,IAAI,GAAG,CAAC;KACxB;KACA,EAAE,cAAc;KAChB,EAAE,YAAY;KACd,EAAE,OAAO;KAGT,EAAE,UAAU;KACZ,KAAK,IAAI,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG;MAC9B,MAAM,IAAI,IAAI,CAAC;MACf,IAAI,MAAM,GAAG,EAAE,OAAO,GAAG,CAAC;WACrB,EAAE,OAAO,GAAG,CAAC;KACpB;KACA,EAAE,cAAc,WAAW,QAAQ;KACnC,EAAE,YAAY;KACd,EAAE,WAAW;KACb,EAAE,UAAU;KACZ,EAAE,OAAO;KAGT,MAAM,QAAQ,IAAI,CAAC;KACnB,EAAE,YAAY,WAAW,QAAQ;KACjC,EAAE,cAAc;KAChB,EAAE,UAAU;KACZ,EAAE,IAAI,IAAI,GAAG,OAAO,GAAG,GAAG,KAAK,KAAK,CAAC;KACrC,EAAE,KAAK;KACP,EAAE,cAAc;KAChB,EAAE,UAAU;KACZ,EAAE,IAAI,IAAI,GAAG,OAAO,KAAK,GAAG,KAAK,KAAK,CAAC;KACvC,EAAE,KAAK;IACT;IAEA,IAAI,OAAO,WAAW,kCAAkC,CAAC,CAAC,SAAS;KACjE,MAAM,YAAY,IAAI,CAAC;KACvB,aAAa;MAAE,SAAS,WAAW;KAAE;IACvC;IACA,IAAI,MAAM;IACV,MAAM,QAAQ,QAAsB;KAClC,MAAM,sBAAsB,IAAI;KAChC,MAAM,GAAG;IACX;IACA,MAAM,sBAAsB,IAAI;IAChC,aAAa;KACX,SAAS,WAAW;KACpB,qBAAqB,GAAG;IAC1B;GACF,GAAG,CAAC,CAAC;GAGL,MAAM,SAAS,YAAY,KAAK,MAAM,GAAG,UAAU,eAAe,IAAI,YAAY;GAClF,MAAM,SAAS,KAAK,UAAU,OAC1B,KAAK,KAAK,MAAM,MAAM,GAAG,EAAE,MAC3B,KAAK,aAAa,OAChB,UAAU,KAAK,aACf,KAAK,gBAAgB,KACnB,KAAK,KAAK,YAAY,MAAM,GAAG,MAC/B,EAAE,MAAM;GAEhB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAU,MAAK;IAAQ,cAAY,EAAE,WAAW;IAAG,qBAAA;IAAkB,aAAW,GAAG;cAAlG;KACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;gBAAf,CACG,GAAG,KACJ,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,WAAU;iBAAkB,EAAE,UAAU;MAAQ,CAAA,CACnD;;KAEL,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MAAQ,KAAK;MAAW,WAAU;MAAa,eAAA;KAAa,CAAA;KAE5D,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;gBACb,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAgB,OAAO;iBAAS;MAAY,CAAA;KACxD,CAAA;IACF;;EAET;;;;;;;;;;;EChRA,MAAa,KAAK;;EAelB,MAAa,KAAgC;GAC3C,aAAa;GACb,YAAY;GACZ,iBAAiB;GACjB,iBAAiB;GACjB,iBAAiB;GACjB,eAAe;GACf,eAAe;GACf,kBAAkB;GAClB,kBAAkB;EACpB;;EAGA,MAAa,KAAgC;GAC3C,aAAa;GACb,YAAY;GACZ,iBAAiB;GACjB,iBAAiB;GACjB,iBAAiB;GACjB,eAAe;GACf,eAAe;GACf,kBAAkB;GAClB,kBAAkB;EACpB;;;;;;;;;;;;;;;;;;;;;;;EC1BA,MAAa,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECkB1B,MAAa,SAAS,CAAC,SAAS,QAAQ;;;;;;EAOxC,SAAgB,MAAM,KAA0B;GAC9C,IAAI,aAAa,IAAI,OAAO,SAAS,IAAI;IAAE;IAAI;GAAG,CAAC,GAAG,4BAA4B;GAElF,IAAI,aAAa;IACf,MAAM,MAAM,SAAS,cAAc,OAAO;IAC1C,IAAI,QAAQ,SAAS;IACrB,IAAI,cAAc;IAClB,SAAS,KAAK,YAAY,GAAG;IAC7B,aAAa;KAAE,IAAI,OAAO;IAAE;GAC9B,GAAG,sBAAsB;GAIzB,IAAI,aAAa;IAMf,MAAM,QAAQ;KAJZ;MAAE,WAAW;MAA6B,OAAO;KAAY;KAC7D;MAAE,WAAW;MAA2B,OAAO;KAAU;KACzD;MAAE,WAAW;MAA4B,OAAO;KAAW;IAE1C,CAAC,CAAC,KAAK,EAAE,gBAAgB;KAC1C,MAAM,KAAK,SAAS,cAAc,KAAK;KACvC,GAAG,YAAY;KACf,GAAG,aAAa,eAAe,MAAM;KACrC,SAAS,KAAK,YAAY,EAAE;KAC5B,OAAO;IACT,CAAC;IACD,aAAa;KACX,KAAK,MAAM,MAAM,OAAO,GAAG,OAAO;IACpC;GACF,GAAG,0BAA0B;GAE7B,IAAI,MAAM,OAAO,iCAAiC,IAAI,MAAM,SAAS;IACnE,MAAM;IACN,IAAI;IAGJ,OAAO;IACP,QAAQ;GACV,GAAG,SAAS,CAAC;EACf"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client/ecg.ts","../src/client/HeartLine.tsx","../src/client/locales.ts","../src/client/style.ts","../src/client/index.ts"],"sourcesContent":["/**\n * CHIRAL PULSE — ECG waveform synthesis.\n *\n * A cardiac cycle is a pure function of beat phase in [0,1): the classic\n * P-QRS-T complex as a sum of wrapped gaussian bumps. The monitor line is a\n * scrolling window over the time axis: the right edge shows the current\n * instant, the window spans `cycles` beats of history. BPM is the phase\n * clock speed, so the whole rhythm accelerates and slows with activity.\n */\n\n/** One wrapped gaussian bump: peak at `center` with `width`, amplitude `amp`. */\nfunction bump(phase: number, center: number, width: number, amp: number): number {\n let d = phase - center\n d -= Math.round(d)\n return amp * Math.exp(-(d * d) / (2 * width * width))\n}\n\n/**\n * Sample one cardiac cycle at beat phase in [0,1). Output range ≈ [-0.35, 1].\n * @param phase - beat phase, any real value (wrapping is internal).\n * @returns the waveform amplitude at that phase.\n */\nexport function ecgValue(phase: number): number {\n return (\n bump(phase, 0.14, 0.030, 0.16) // P wave\n - bump(phase, 0.30, 0.011, 0.26) // Q dip\n + bump(phase, 0.335, 0.016, 1.0) // R spike (wide enough to survive sampling)\n - bump(phase, 0.375, 0.011, 0.34) // S dip\n + bump(phase, 0.52, 0.048, 0.26) // T wave\n + bump(phase, 0.80, 0.012, 0.05) // U ripple\n )\n}\n\n/** One rendered frame of the scrolling monitor line. */\nexport interface EcgFrame {\n /** `x,y` pairs for an SVG polyline `points` attribute, rightmost = now. */\n points: string\n /** The y of the leading (rightmost) sample, in view units. */\n headY: number\n}\n\n/**\n * Build the polyline points for the scrolling window.\n *\n * Hospital monitor semantics: the paper moves at a FIXED speed — every pixel\n * represents a fixed amount of wall-clock time, so the trace scrolls left at\n * a constant rate regardless of heart rate. What changes with BPM is the\n * density of QRS complexes across that fixed window: a fast heart packs more\n * beats onto the screen, a resting one spaces them out.\n *\n * @param nowMs - wall-clock sample time (drives the scan).\n * @param bpm - current heart rate (beat density only; never the scan speed).\n * @param width - view width in user units (x spans 0..width).\n * @param height - view height in user units.\n * @param windowSeconds - how many seconds of signal the window shows.\n * @param step - x sampling step in user units.\n * @returns the frame.\n */\nexport function buildEcgFrame(\n nowMs: number,\n bpm: number,\n width: number,\n height: number,\n windowSeconds: number,\n step = 2,\n): EcgFrame {\n const mid = height / 2\n const amp = height * 0.44\n // Fixed paper speed: seconds per user-unit pixel.\n const secondsPerPixel = windowSeconds / width\n const tNow = nowMs / 1_000\n // Slow baseline wander, like a real monitor: two incommensurate sines keep\n // the resting trace from freezing into a straight flatline.\n const wander = 0.05 * Math.sin(tNow * 0.6)\n + 0.035 * Math.sin(tNow * 1.7 + 1.3)\n const points: string[] = []\n let headY = mid\n for (let x = 0; x <= width; x += step) {\n // x → absolute time: the right edge is \"now\", leftward is the past at a\n // constant rate. Sub-step peak guard: also sample the midpoints so a\n // narrow R spike between two samples still paints at full height.\n const tX = tNow - (width - x) * secondsPerPixel\n const phase = ((tX * (bpm / 60)) % 1 + 1) % 1\n let v = ecgValue(phase)\n if (step > 1) {\n const tMid = tX - secondsPerPixel * step * 0.5\n const phaseMid = ((tMid * (bpm / 60)) % 1 + 1) % 1\n v = Math.max(v, ecgValue(phaseMid))\n }\n const y = mid - (v + wander) * amp\n points.push(`${x.toFixed(1)},${y.toFixed(1)}`)\n if (x === width - step) headY = y\n }\n return { points: points.join(' '), headY }\n}\n\n/**\n * Steady-state BPM for a measured activity rate.\n * @param stepsPerMinute - measured step cadence (steps / minute over a window).\n * @returns target BPM within [42, 150] — a resting BB sleeps at 42, full\n * sprint peaks at 150.\n */\nexport function bpmForActivity(stepsPerMinute: number): number {\n return Math.min(150, Math.max(42, 42 + stepsPerMinute * 6))\n}\n","/**\n * HeartLine — the CHIRAL PULSE monitor strip, docked above the composer\n * (`conversation.input.dock`). A 26px \"monitor paper feed\": the scrolling\n * ECG waveform is the hero, flanked by the BPM read and the status word.\n * No duplicated figures — StatsLine already shows turns/tokens.\n *\n * The pulse is LIVE, not decorative:\n * - `partial` non-null → the model is thinking/generating → +38 BPM\n * - `runningCalls` non-empty → a tool is executing → +52 BPM\n * - `running` (session turn in flight) → +10 BPM\n * - otherwise the 10s step-window activity rate sets the base (~42 idle)\n * The BPM target is smoothed with a lerp; the paper speed stays FIXED and\n * only the beat density changes — hospital monitor semantics.\n *\n * Rendering: a single <canvas> redrawn per rAF at full frame rate. Fixed\n * memory (one canvas the size of the strip), no DOM attribute churn, no\n * string building — the trace is ~width straight segments per frame, which\n * is far cheaper than SVG polyline swaps and cannot stutter from throttling.\n */\nimport { useEffect, useRef, useState } from 'react'\nimport type {\n PropsLocale, PropsRuntime,\n} from '@deepseek-ai/dsh-client-ui-slots'\nimport type { SessionProjectionMap } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: merges the sessionStats key into SessionProjectionMap.\nimport type {} from '@deepseek-ai/dsh-session-stats/client'\nimport { ecgValue } from './ecg.ts'\nimport type { ChiralKey } from './locales.ts'\nimport { NS } from './locales.ts'\n\n/** Full props: the input-dock runtime seat plus the locale seat. */\nexport type HeartLineProps = PropsRuntime<'conversation.input.dock'> & PropsLocale<typeof NS>\n\n/** Monitor view height, CSS px. */\nconst ECG_HEIGHT = 22\n/**\n * FIXED paper speed in px/second — the real hospital-monitor invariant.\n * The trace scrolls at this absolute rate no matter the strip width; the\n * width only decides how much history fits on screen. A rate change (42→90)\n * therefore only densifies the beats — it never speeds the paper up, and\n * resizing the window cannot make the trace run faster either.\n */\nconst PAPER_SPEED_PX_PER_SECOND = 30\n/** Activity window for the step-rate base, ms. */\nconst ACTIVITY_WINDOW_MS = 10_000\n/** Rotating status lines (locale keys), one every STATUS_ROTATE_S ticks. */\nconst STATUS_KEYS: readonly ChiralKey[] = [\n 'status.stable', 'status.bonded', 'status.chiral', 'status.doom',\n 'status.keep', 'status.voidout', 'status.odradek',\n]\nconst STATUS_ROTATE_S = 4\n/** BPM boost while the model is streaming a partial (thinking/generating). */\nconst BOOST_THINKING = 38\n/** BPM boost while a tool call is running. */\nconst BOOST_TOOL = 52\n/** BPM boost while the session turn is simply in flight. */\nconst BOOST_RUNNING = 10\n/** BPM floor (a resting BB) and ceiling. */\nconst BPM_FLOOR = 42\nconst BPM_CEIL = 150\n/**\n * How fast the displayed heart rate ramps toward its target, in BPM/second.\n * A hospital monitor updates its HR figure on a ~2-3s rolling average and\n * the trace follows gradually — the rate change reads as a slow ramp, not a\n * snap: 42 → 90 takes (90-42)/6 = 8 seconds of visible densification.\n */\nconst BPM_RAMP_PER_SECOND = 6\n\n/** Trace color by activity mode: idle amber, thinking cyan, tool orange, run warm. */\nconst MODE_COLOR = {\n idle: '#ffb454',\n think: '#6fdbe2',\n tool: '#ff7a4d',\n run: '#ffc46b',\n flat: '#c0483c',\n} as const\ntype Mode = keyof typeof MODE_COLOR\n\n/** One activity sample: (time, steps) at a projection update. */\ninterface StepSample {\n t: number\n steps: number\n}\n\n/** Tail of the model's in-flight output: last non-empty text/reasoning block, whitespace-flattened. */\nfunction streamingTail(blocks: readonly { kind: string; text?: string }[]): string {\n for (let i = blocks.length - 1; i >= 0; i -= 1) {\n const block = blocks[i]\n const text = block.text\n if (text !== undefined && text.trim() !== '') {\n return text.replace(/\\s+/g, ' ').trim()\n }\n }\n return ''\n}\n\n/**\n * The CHIRAL PULSE dock entry.\n * @param props - runtime seat (useSession, useProjection) plus the locale seat.\n * @returns the monitor strip.\n */\nexport function HeartLine({ useSession, useProjection, t }: HeartLineProps) {\n const stats = useProjection('sessionStats') as SessionProjectionMap['sessionStats'] | undefined\n // One primitive-returning selector per signal: each returns a stable value\n // (boolean / string / null), so the component only re-renders when that\n // signal actually changes — a single object selector re-rendered on every\n // snapshot flush, which is far too often while streaming.\n const partial = useSession(s => s.partial !== null)\n const partialText = useSession(s => (s.partial === null ? '' : streamingTail(s.partial.blocks)))\n const toolName = useSession(s => (s.runningCalls[0]?.name ?? null))\n const running = useSession(s => s.running)\n const error = useSession(s => s.lastAgentError)\n // Flatline only on a LIVE retry stall. The retry chain keeps every attempt;\n // older attempts can linger in 'scheduled' forever (a retry superseded\n // without a retry-started event), so only the LAST attempt counts, within a\n // freshness window — a session merely waiting for user input must never\n // read as a stopped heart. Back-to-front scan stops at the first retry node\n // (which is the last one), so cost is O(distance from the tail), not O(n).\n const retrying = useSession(s => {\n const nodes = s.chat.legacy.nodes\n for (let i = nodes.length - 1; i >= 0; i -= 1) {\n const n = nodes[i]\n if (n.kind === 'model-retry') {\n return n.retryState === 'scheduled' && n.time > Date.now() - 120_000\n }\n }\n return false\n })\n const live = { partial, partialText, toolName, running, error, retrying }\n\n const steps = stats?.steps ?? 0\n\n // ── BPM engine: step-window base + live activity boost ────────────────\n // targetRef updates once per second (activity readout); bpmRef eases toward\n // it EVERY FRAME inside paint, so the trace phase never jumps — a stepped\n // BPM would snap the whole waveform sideways at every tick.\n const bpmRef = useRef(BPM_FLOOR)\n const targetRef = useRef(BPM_FLOOR)\n const samplesRef = useRef<StepSample[]>([])\n const lastStepsRef = useRef(steps)\n const liveRef = useRef(live)\n liveRef.current = live\n const modeRef = useRef<Mode>('idle')\n const [ui, setUi] = useState({ bpm: BPM_FLOOR, elapsed: 0, mode: 'idle' as Mode })\n useEffect(() => {\n if (steps !== lastStepsRef.current) {\n lastStepsRef.current = steps\n samplesRef.current.push({ t: performance.now(), steps })\n }\n }, [steps])\n\n useEffect(() => {\n const id = window.setInterval(() => {\n const now = performance.now()\n const samples = samplesRef.current\n while (samples.length > 0 && now - samples[0].t > ACTIVITY_WINDOW_MS) samples.shift()\n const first = samples[0]\n const span = first === undefined ? 0 : now - first.t\n const delta = first === undefined ? 0 : lastStepsRef.current - first.steps\n const perMinute = span > 0 ? (delta / span) * 60_000 : 0\n const base = Math.min(BPM_CEIL, Math.max(BPM_FLOOR, 42 + perMinute * 6))\n const act = liveRef.current\n // Retry stall → flatline: target 0, the trace flattens and the whale's\n // heart stops until the retry starts.\n targetRef.current = act.retrying\n ? 0\n : Math.min(BPM_CEIL, Math.max(BPM_FLOOR, base\n + (act.toolName !== null ? BOOST_TOOL : 0)\n + (act.partial ? BOOST_THINKING : 0)\n + (act.running ? BOOST_RUNNING : 0)))\n const mode: Mode = act.retrying ? 'flat'\n : act.toolName !== null ? 'tool'\n : act.partial ? 'think'\n : act.running ? 'run' : 'idle'\n modeRef.current = mode\n setUi(current => ({\n bpm: Math.round(bpmRef.current),\n elapsed: current.elapsed + 1,\n mode,\n }))\n }, 1_000)\n return () => { window.clearInterval(id) }\n }, [])\n\n // ── Canvas trace: one fixed-size canvas, redrawn per rAF ──────────────\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const ctxRef = useRef<CanvasRenderingContext2D | null>(null)\n const widthRef = useRef(640)\n const dprRef = useRef(1)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (canvas === null) return\n const ctx = canvas.getContext('2d')\n if (ctx === null) return\n ctxRef.current = ctx\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n const applySize = (): void => {\n const dpr = window.devicePixelRatio || 1\n dprRef.current = dpr\n canvas.width = Math.max(120, Math.round(widthRef.current * dpr))\n canvas.height = Math.round(ECG_HEIGHT * dpr)\n }\n applySize()\n const observer = new ResizeObserver((entries) => {\n const width = Math.max(120, Math.round(entries[0]?.contentRect.width ?? 640))\n if (width !== widthRef.current) {\n widthRef.current = width\n applySize()\n if (reduced) paint(performance.now()) // static mode repaints at the new width\n }\n })\n observer.observe(canvas)\n\n // Smooth display clock: the trace advances at most CLAMP_PER_FRAME worth\n // of time per frame, so a busy main thread (streaming markdown, mode\n // switches) that delays rAF can never make the paper jump forward.\n let displayNow = 0\n let lastPaintReal = 0\n // Refresh-synced display clock. Each NORMAL frame advances the trace by\n // exactly that frame's real interval — the paper speed is then a constant\n // 1× by construction, with zero lag. A DELAYED frame (busy main thread)\n // reuses the last normal interval instead, so the trace never jumps, just\n // runs a touch slow and resumes. No averaging: a smoothed period lags\n // frame-rate changes and the speed visibly surges when the frame rate\n // recovers (the \"left edge accelerates then settles\" artifact).\n let framePeriodMs = 16.7\n // Frozen pixel cache for true erase-bar rendering: the trace image is a\n // static buffer; only the pixels the sweep bar has just passed are\n // refreshed, everything else stays frozen. The image therefore never\n // scrolls, never jumps as a whole, and is frame-rate independent.\n let traceCache: number[] = []\n let lastScanX = -1\n const paint = (now: number): void => {\n if (lastPaintReal === 0) {\n lastPaintReal = now\n displayNow = now\n }\n const realDt = Math.max(0, (now - lastPaintReal) / 1_000)\n lastPaintReal = now\n if (realDt > 0 && realDt < 0.05) framePeriodMs = realDt * 1_000\n const dt = framePeriodMs / 1_000\n displayNow += dt * 1_000\n // Constant-rate ramp (hospital monitor cadence): the rate eases toward\n // its target at BPM_RAMP_PER_SECOND, so a rate change takes seconds and\n // the beat spacing visibly densifies beat by beat.\n const diff = targetRef.current - bpmRef.current\n const step = BPM_RAMP_PER_SECOND * dt\n if (diff > step) bpmRef.current += step\n else if (diff < -step) bpmRef.current -= step\n else bpmRef.current = targetRef.current\n const c = canvasRef.current\n const g = ctxRef.current\n if (c === null || g === null) return\n // Size is maintained by the ResizeObserver (widthRef / canvas.width);\n // paint reads it directly — no per-frame getBoundingClientRect, so no\n // forced synchronous layout. Fixed logical height keeps the amplitude\n // independent of the live layout (the first paint once saw height 0 and\n // collapsed the trace to a flat line).\n const dpr = dprRef.current\n const w = widthRef.current\n const h = ECG_HEIGHT\n const wantW = Math.round(w * dpr)\n const wantH = Math.round(h * dpr)\n if (c.width !== wantW || c.height !== wantH) {\n c.width = wantW\n c.height = wantH\n }\n g.setTransform(dpr, 0, 0, dpr, 0, 0)\n g.clearRect(0, 0, w, h)\n\n const tNow = displayNow / 1_000\n // Absolute paper speed: px per second is constant, width-independent.\n const secondsPerPixel = 1 / PAPER_SPEED_PX_PER_SECOND\n const mid = h / 2\n const amp = h * 0.5\n const wander = 0.05 * Math.sin(tNow * 0.6) + 0.035 * Math.sin(tNow * 1.7 + 1.3)\n const bpm = bpmRef.current\n // Flatline is a MODE, not a smoothed value: as soon as the target is a\n // stopped heart, draw the line — don't wait for the 6 BPM/s ramp to\n // cross an arbitrary threshold.\n const flatline = targetRef.current === 0\n\n // Erase-bar sweep: the bar moves right → left; pixels it has just\n // passed are re-sampled (frozen update), the rest of the image is\n // untouched. The right edge is pinned to \"now\"; beat spacing is\n // width-independent (fixed paper speed).\n const sweepPeriod = w / PAPER_SPEED_PX_PER_SECOND\n const tInSweep = ((tNow % sweepPeriod) + sweepPeriod) % sweepPeriod\n const scanX = w - tInSweep * PAPER_SPEED_PX_PER_SECOND // w → 0\n const scanXInt = Math.round(scanX)\n const yNow = (x: number): number => {\n if (flatline) return mid // the whale's heart has stopped — a flat line\n let v = -Infinity\n // Sweep sample: pixel x is (re)written when the bar crosses it, so the\n // sample must be anchored to the sweep, not the frame. The frame-time\n // lookback `tNow - (w - x)·secondsPerPixel` evaluated at a crossing\n // cancels its own offset — the bar reaches x exactly (w - x) px into\n // the sweep, so the lookback always lands on the sweep-start instant\n // k·sweepPeriod and EVERY swept pixel receives the SAME sample → the\n // trace flattens to one level once the bar has crossed the strip.\n // Anchoring to the sweep start (`(tNow - tInSweep) - (w - x)·spp`, with\n // tNow - tInSweep = k·sweepPeriod) keeps the right edge pinned to the\n // sweep start (\"now\") and each pixel's sample x-dependent — the swept\n // region redraws as a real, time-normal waveform window.\n for (let i = 0; i < 4; i += 1) {\n const tX = (tNow - tInSweep) - (w - (x + i * 0.25)) * secondsPerPixel\n const phase = ((tX * (bpm / 60)) % 1 + 1) % 1\n const s = ecgValue(phase)\n if (s > v) v = s\n }\n return mid - (v + wander) * amp\n }\n if (traceCache.length !== w + 1) {\n // Width changed: rebuild the buffer, right-anchored, repaint once.\n traceCache = new Array<number>(w + 1)\n for (let x = 0; x <= w; x += 1) traceCache[x] = yNow(x)\n lastScanX = scanXInt\n } else if (lastScanX > scanXInt) {\n // Refresh exactly the pixels the sweep has just passed.\n for (let x = scanXInt; x <= lastScanX && x <= w; x += 1) {\n traceCache[x] = yNow(x)\n }\n lastScanX = scanXInt\n } else if (lastScanX < scanXInt) {\n // Wrap: the bar jumped back to the right edge. Refresh its new head\n // pixel so the trace continues from the current instant instead of\n // leaving stale data at the right edge (the reported seam).\n traceCache[scanXInt] = yNow(scanXInt)\n lastScanX = scanXInt\n } else {\n lastScanX = scanXInt\n }\n\n // Chiral ghost over the frozen image, faint.\n g.beginPath()\n for (let x = 0; x <= w; x += 1) {\n const y = traceCache[x]\n if (x === 0) g.moveTo(x + 3, y)\n else g.lineTo(x + 3, y)\n }\n g.globalAlpha = 0.1\n g.strokeStyle = 'rgba(111, 219, 226, 1)'\n g.lineWidth = 1\n g.stroke()\n g.globalAlpha = 1\n\n // The frozen trace, whole window.\n g.beginPath()\n for (let x = 0; x <= w; x += 1) {\n const y = traceCache[x]\n if (x === 0) g.moveTo(x, y)\n else g.lineTo(x, y)\n }\n g.strokeStyle = MODE_COLOR[modeRef.current]\n g.lineWidth = 1.4\n g.lineJoin = 'round'\n g.lineCap = 'round'\n g.stroke()\n\n // The sweep bar itself: bright core + soft halo.\n g.fillStyle = 'rgba(255, 180, 84, 0.16)'\n g.fillRect(scanX - 5, 0, 10, h)\n g.fillStyle = 'rgba(255, 224, 190, 0.95)'\n g.fillRect(scanX - 1, 0, 2, h)\n }\n\n if (reduced) {\n paint(performance.now()) // one static frame\n return () => { observer.disconnect() }\n }\n let raf = 0\n const loop = (now: number): void => {\n raf = requestAnimationFrame(loop)\n paint(now)\n }\n raf = requestAnimationFrame(loop)\n return () => {\n observer.disconnect()\n cancelAnimationFrame(raf)\n }\n }, [])\n\n // Status word: real model state wins; the flavor rotation only plays while idle.\n const flavor = STATUS_KEYS[Math.floor(ui.elapsed / STATUS_ROTATE_S) % STATUS_KEYS.length]\n const status = live.error !== null\n ? `⚠ ${live.error.slice(0, 16)}`\n : live.retrying\n ? t('status.flatline')\n : live.toolName !== null\n ? `EXEC · ${live.toolName}`\n : live.partialText !== ''\n ? `⇢ ${live.partialText.slice(-18)}`\n : t(flavor)\n\n return (\n <div className=\"cp-line\" role=\"group\" aria-label={t('line.aria')} data-chiral-pulse data-mode={ui.mode} data-rev=\"20\">\n <div className=\"cp-lineBpm\">\n {ui.bpm}\n </div>\n\n <div className=\"cp-lineEcgWrap\">\n <canvas ref={canvasRef} className=\"cp-lineEcg\" aria-hidden />\n </div>\n\n <div className=\"cp-lineReadout\">\n <div className=\"cp-lineStatus\" title={status}>{status}</div>\n </div>\n </div>\n )\n}\n","/**\n * CHIRAL PULSE — dictionary namespace.\n *\n * The DS monitor idiom stays English in both locales (it is part of the\n * aesthetic: \"LINK STABLE\", \"TIME TO COMPLETION\"); the zh side translates\n * the labels a user actually reads.\n */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'chiral'\n\n/** Dictionary keys of the `chiral` namespace (string-literal union). */\nexport type ChiralKey =\n | 'line.aria'\n | 'bpm.unit'\n | 'status.stable'\n | 'status.bonded'\n | 'status.chiral'\n | 'status.doom'\n | 'status.keep'\n | 'status.voidout'\n | 'status.odradek'\n | 'status.flatline'\n\n/** English dictionary. */\nexport const en: Record<ChiralKey, string> = {\n 'line.aria': 'BB vital-signs strip — CHIRAL PULSE',\n 'bpm.unit': 'BPM',\n 'status.stable': 'LINK STABLE',\n 'status.bonded': 'BB BONDED',\n 'status.chiral': 'CHIRAL DENSITY: NOMINAL',\n 'status.doom': 'DOOMS LEVEL: 0',\n 'status.keep': 'KEEP ON KEEPING ON',\n 'status.voidout': 'NO VOIDOUT DETECTED',\n 'status.odradek': 'ODRADEK SYNC: OK',\n 'status.flatline': '♥ FLATLINE',\n}\n\n/** Chinese dictionary. */\nexport const zh: Record<ChiralKey, string> = {\n 'line.aria': 'BB 生命体征走纸 — CHIRAL PULSE 手性脉冲',\n 'bpm.unit': '次/分',\n 'status.stable': '链路稳定',\n 'status.bonded': 'BB 连接完成',\n 'status.chiral': '手性密度:正常',\n 'status.doom': 'DOOMS 等级:0',\n 'status.keep': '继续前进 · KEEP ON KEEPING ON',\n 'status.voidout': '未检测到虚爆',\n 'status.odradek': '奥卓克同步:正常',\n 'status.flatline': '♥ 心脏停跳',\n}\n","/**\n * CHIRAL PULSE — the Death Stranding sheet, two layers.\n *\n * LAYER 1 — the global skin. The whole app paints from `--dsw-*` variables\n * (ui-theme's design platform: alias tokens reference static tokens, so\n * remapping the palette re-skins every component without touching its\n * structure). This sheet FORCES the DS look under BOTH theme modes: a deep\n * blue-black machine body, cold blue-grey hairlines, amber reserved for\n * emphasis (the heartbeat waveform, hover blooms) — and the deepseek brand\n * blues are left untouched, so the whale mark stays DeepSeek blue.\n *\n * LAYER 2 — the atmosphere. A fixed full-viewport CRT scanline weave, a\n * faint chiral lattice, and a vignette, all pointer-transparent. Plus the\n * BB vital-signs strip that docks under the composer stats: a 26px monitor\n * paper feed whose scrolling ECG is the hero, with the BPM and status read.\n *\n * Every rule is scoped under `.cp-*` (except the token remap, which must\n * target `body`), rides one owned <style data-plugin> tag, and the loader\n * removes it on unload.\n */\n\nexport const CHIRAL_CSS = `\n/* ────────────────────────────────────────────────────────────────────────\n LAYER 1 · global DS skin — dark blue-black, both theme modes\n ──────────────────────────────────────────────────────────────────────── */\n\n/* Alias-level remap: independent of the static scale's role flip between\n themes, so the DS look is identical under light and dark settings. */\nbody[data-ds-dark-theme],\nbody:not([data-ds-dark-theme]) {\n /* machine body — blue-grey with air, not a black void */\n --dsw-alias-bg-base: rgb(13, 17, 23);\n --dsw-alias-bg-layer-1: rgb(17, 22, 29);\n --dsw-alias-bg-layer-2: rgb(21, 27, 35);\n --dsw-alias-bg-layer-3: rgb(26, 33, 42);\n --dsw-alias-bg-overlay: rgb(31, 40, 51);\n --dsw-alias-bg-mask-1: rgba(0, 0, 0, 0.5);\n --dsw-alias-bg-mask-2: rgba(0, 0, 0, 0.22);\n --dsw-alias-bg-mask-3: rgba(0, 0, 0, 0.5);\n --dsw-alias-bg-mask-drop: rgba(10, 14, 19, 0.72);\n --dsw-alias-bg-skeleton: rgba(150, 170, 200, 0.07);\n /* Setting rows / select chips (language, agent preset, model, permissions…)\n paint from these; without an override they stay the LIGHT theme's\n near-white and produce white-on-white text. */\n --dsw-alias-bg-module-platform: rgb(15, 20, 27);\n --dsw-alias-bg-multi-select: rgb(18, 24, 32);\n --dsw-alias-fill-tsp-secondary: rgb(18, 24, 32);\n\n /* text: cold blue-grey */\n --dsw-alias-label-primary: rgb(235, 240, 246);\n --dsw-alias-label-secondary: rgb(178, 190, 205);\n --dsw-alias-label-tertiary: rgb(140, 153, 170);\n --dsw-alias-label-caption: rgb(108, 122, 141);\n --dsw-alias-label-dimmed: rgb(94, 108, 127);\n --dsw-alias-label-primary-dimmed: rgb(205, 213, 222);\n --dsw-alias-label-primary-inverted: rgb(20, 26, 35);\n --dsw-alias-label-primary-foreground: rgb(13, 17, 23);\n --dsw-alias-label-primary-bluish: rgb(200, 214, 232);\n --dsw-alias-brand-text: rgb(235, 240, 246);\n --dsw-alias-brand-primary: rgb(103, 158, 254);\n --dsw-alias-brand-primary-invert: rgb(235, 240, 246);\n\n /* hairlines: cold blue, readable against the body */\n --dsw-alias-border-l1: rgba(140, 170, 215, 0.15);\n --dsw-alias-border-l2: rgba(140, 170, 215, 0.24);\n --dsw-alias-border-l2-darkmode-thin: rgba(140, 170, 215, 0.19);\n --dsw-alias-border-l3: rgba(140, 170, 215, 0.34);\n --dsw-alias-border-l4: rgba(140, 170, 215, 0.46);\n --dsw-alias-border-inverted: rgba(255, 255, 255, 0.09);\n --dsw-alias-border-inverted2: rgba(255, 255, 255, 0.11);\n\n /* hovers: amber bloom, kept subtle */\n --dsw-alias-interactive-bg-hover: rgba(255, 180, 84, 0.08);\n --dsw-alias-interactive-bg-active: rgba(255, 180, 84, 0.12);\n --dsw-alias-interactive-bg-hover-accent: rgba(255, 180, 84, 0.14);\n --dsw-alias-interactive-bg-hover-solid: rgb(22, 29, 38);\n --dsw-alias-interactive-bg-hover-danger: rgba(242, 90, 90, 0.14);\n\n /* buttons: brand blue stays the primary action */\n --dsw-alias-button-primary-dimmed: rgb(30, 40, 53);\n --dsw-alias-button-primary-hover: rgb(124, 172, 255);\n --dsw-alias-button-ghost-active-fill: rgb(22, 29, 38);\n --dsw-alias-button-ghost-active-hover: rgb(27, 35, 46);\n --dsw-alias-button-ghost-active-border: rgb(140, 170, 215);\n --dsw-alias-button-floating-fill: rgb(19, 25, 33);\n --dsw-alias-button-floating-hover: rgb(24, 31, 41);\n --dsw-alias-button-elevated-fill: rgb(22, 29, 38);\n /* Contrast fill (attachment rail): pale case + DARK inverted ink — the\n wordmark badge and toasts also ride label-primary-inverted, so the pair\n (pale fill, dark ink) stays readable everywhere. */\n --dsw-alias-button-contrast-fill: rgb(205, 213, 222);\n --dsw-alias-button-tool-bar-fill: rgba(140, 170, 215, 0.24);\n --dsw-alias-button-tool-bar-fill-invisible: rgba(140, 170, 215, 0.13);\n --dsw-alias-button-tool-bar-hover: rgba(140, 170, 215, 0.32);\n\n /* surfaces */\n --dsw-specific-sidebar-fill: rgb(9, 12, 17);\n --dsw-specific-sidebar-nav-item-active: rgb(20, 27, 36);\n --dsw-specific-sidebar-nav-item-active-accent: rgb(27, 36, 48);\n --dsw-specific-sidebar-nav-item-hover: rgb(15, 20, 28);\n --dsw-specific-bubble: rgb(18, 24, 32);\n --dsw-specific-bubble-highlight: rgb(24, 32, 42);\n --dsw-specific-input-major: rgb(15, 20, 27);\n --dsw-specific-login-input: rgb(12, 16, 22);\n --dsw-specific-menu: rgb(21, 27, 35);\n --dsw-specific-selector: rgb(20, 26, 34);\n --dsw-specific-tip: rgb(16, 21, 28);\n --dsw-alias-markdown-code-block: rgb(10, 14, 19);\n --dsw-alias-markdown-code-block-banner: rgb(13, 17, 23);\n --dsw-alias-markdown-inline-code: rgb(18, 24, 32);\n --dsw-alias-markdown-code-segment-selected: rgb(16, 21, 28);\n --dsw-alias-markdown-code-segment-unselected: rgb(12, 16, 22);\n --dsw-alias-markdown-placeholder: rgb(16, 21, 28);\n --dsw-alias-markdown-tag: rgb(18, 24, 32);\n --dsw-alias-markdown-citation: rgb(22, 29, 38);\n\n /* floats */\n --dsw-alias-toast-bg: rgb(22, 29, 38);\n --dsw-alias-tooltip-bg: rgb(20, 26, 34);\n --dsw-alias-scrollbar-bg-l1: rgb(13, 18, 25);\n --dsw-alias-scrollbar-bg-l2: rgb(17, 23, 31);\n --dsw-alias-scrollbar-hover-l1: rgb(34, 44, 58);\n --dsw-alias-scrollbar-hover-l2: rgb(42, 54, 70);\n\n /* status: amber stays the warn/emphasis hue; success leans chiral cyan */\n --dsw-alias-state-warn-primary: rgb(245, 158, 11);\n --dsw-alias-state-warn-secondary: rgb(247, 173, 49);\n --dsw-alias-state-warn-label: rgb(221, 134, 41);\n --dsw-alias-state-warn-tertiary: rgb(39, 36, 31);\n --dsw-alias-state-success-primary: rgb(52, 205, 168);\n --dsw-alias-state-success-secondary: rgb(94, 222, 189);\n --dsw-alias-state-success-tertiary: rgb(12, 28, 24);\n /* Business tint (hero \"preview\" badge et al.): dark case so the pale\n primary-bluish ink stays readable — the light-theme default is near-white. */\n --dsw-alias-state-business-tertiary: rgb(26, 34, 46);\n --dsw-static-green-400: rgb(94, 222, 189);\n --dsw-static-green-500: rgb(52, 205, 168);\n}\n\n/* Focus ring: amber, the DS highlight color. */\n:focus-visible {\n outline: 1px solid rgba(255, 180, 84, 0.65) !important;\n outline-offset: 2px;\n}\n\n/* Ambient bloom behind the app. */\nbody {\n background-image:\n radial-gradient(1100px 620px at 12% -8%, rgba(111, 219, 226, 0.04), transparent 60%),\n radial-gradient(900px 560px at 108% 112%, rgba(103, 158, 254, 0.05), transparent 60%);\n background-attachment: fixed;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n LAYER 2 · atmosphere overlays (injected as fixed elements)\n ──────────────────────────────────────────────────────────────────────── */\n.cp-atmo {\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 2147483000;\n}\n.cp-atmo-scanlines {\n background: repeating-linear-gradient(\n 0deg,\n rgba(255, 255, 255, 0.024) 0 1px,\n transparent 1px 3px\n );\n mix-blend-mode: overlay;\n}\n.cp-atmo-lattice {\n opacity: 0.55;\n background:\n repeating-linear-gradient(60deg, transparent 0 17px, rgba(103, 158, 254, 0.03) 17px 18px),\n repeating-linear-gradient(120deg, transparent 0 17px, rgba(111, 219, 226, 0.028) 17px 18px);\n}\n.cp-atmo-vignette {\n background: radial-gradient(120% 100% at 50% 40%, transparent 55%, rgba(0, 0, 0, 0.24) 100%);\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n BB vital-signs strip · the heartbeat paper feed\n ──────────────────────────────────────────────────────────────────────── */\n.cp-line {\n --cp-amber: #ffb454;\n --cp-amber-bright: #ffd9a0;\n --cp-cyan: #6fdbe2;\n --cp-dim: #64727f;\n /* flex: none — the hero (blank-session) composer column squeezes its\n children on short viewports; the monitor strip must never shrink. */\n flex: none;\n display: flex;\n align-items: center;\n gap: 12px;\n height: 26px;\n min-height: 26px;\n margin: 3px 0 4px;\n padding: 0 10px;\n border: 1px solid rgba(140, 170, 215, 0.26);\n border-radius: 0;\n background:\n linear-gradient(115deg, rgba(140, 190, 255, 0.06) 0%, transparent 30%),\n linear-gradient(180deg, rgba(14, 19, 26, 0.92), rgba(9, 13, 18, 0.94));\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.05),\n inset 0 0 16px rgba(103, 158, 254, 0.06);\n color: #c9d3dc;\n font-family: ui-monospace, \"Cascadia Mono\", \"JetBrains Mono\", Consolas, \"Courier New\", monospace;\n overflow: hidden;\n clip-path: polygon(\n 7px 0,\n 100% 0,\n 100% calc(100% - 7px),\n calc(100% - 7px) 100%,\n 0 100%,\n 0 7px\n );\n}\n\n.cp-lineBpm {\n /* Fixed width: a 3-digit readout (42 → 150) must not widen the block and\n squeeze the paper area — that would shrink the trace window and pull the\n left edge rightward as the rate climbs. */\n flex: none;\n width: 48px;\n text-align: center;\n font-size: 16px;\n line-height: 1;\n letter-spacing: 0.5px;\n color: var(--cp-amber-bright);\n text-shadow: 0 0 10px rgba(255, 180, 84, 0.5);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.cp-lineEcgWrap {\n /* basis 0 + grow: the paper area takes exactly the flex-allocated width;\n never shrink (a squeezed strip would shrink the canvas bitmap and make\n the trace speed depend on the window width). */\n flex: 1 1 0;\n min-width: 100px;\n height: 22px;\n position: relative;\n border-radius: 0;\n border: 1px solid rgba(140, 170, 215, 0.16);\n /* Static paper grid lives in CSS; the canvas above it only paints the trace. */\n background:\n repeating-linear-gradient(0deg, rgba(140, 170, 215, 0.07) 0 1px, transparent 1px 11px),\n repeating-linear-gradient(90deg, rgba(140, 170, 215, 0.06) 0 1px, transparent 1px 11px),\n rgba(7, 10, 15, 0.55);\n}\n/* The canvas fills its wrapper exactly (absolute), so its intrinsic size can\n never distort the flex layout or the trace during remounts. */\n.cp-lineEcg {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n border: 0;\n background: transparent;\n}\n\n.cp-lineReadout {\n flex: none;\n width: 132px;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n overflow: hidden;\n}\n.cp-lineStatus {\n font-size: 8px;\n letter-spacing: 1.8px;\n text-transform: uppercase;\n color: var(--cp-cyan);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 100%;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .cp-lineEcg {\n opacity: 0.9;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n ECG paper grid + activity-mode color coupling\n ──────────────────────────────────────────────────────────────────────── */\n.cp-line[data-mode=\"think\"] .cp-lineBpm {\n color: #7fe3e8;\n text-shadow: 0 0 10px rgba(111, 219, 226, 0.55);\n}\n.cp-line[data-mode=\"tool\"] .cp-lineBpm {\n color: #ff9b7a;\n text-shadow: 0 0 10px rgba(255, 122, 77, 0.6);\n}\n.cp-line[data-mode=\"run\"] .cp-lineBpm {\n color: #ffd9a0;\n}\n.cp-line[data-mode=\"think\"] .cp-lineStatus,\n.cp-line[data-mode=\"tool\"] .cp-lineStatus {\n color: #9fe8ec;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Message-flow dressing — DS glyphs on each node kind\n ──────────────────────────────────────────────────────────────────────── */\n[data-chat-flow-kind] {\n position: relative;\n}\n[data-chat-flow-kind=\"assistant\"] {\n padding-left: 18px;\n}\n[data-chat-flow-kind=\"assistant\"]::before {\n content: \"✦\";\n position: absolute;\n left: 4px;\n top: 12px;\n color: rgba(255, 180, 84, 0.85);\n font-size: 11px;\n line-height: 1;\n text-shadow: 0 0 8px rgba(255, 180, 84, 0.6);\n}\n[data-chat-flow-kind=\"assistant\"]::after {\n content: \"\";\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 1px;\n background: linear-gradient(180deg, transparent, rgba(255, 180, 84, 0.35), transparent);\n}\n[data-chat-flow-kind=\"user\"],\n[data-chat-flow-kind=\"steering\"] {\n padding-right: 18px;\n}\n[data-chat-flow-kind=\"user\"]::before,\n[data-chat-flow-kind=\"steering\"]::before {\n content: \"▸▸\";\n position: absolute;\n right: 2px;\n top: 4px;\n color: rgba(120, 150, 195, 0.75);\n font-size: 10px;\n line-height: 1;\n letter-spacing: -1px;\n}\n[data-chat-flow-kind=\"context\"] {\n padding-left: 16px;\n}\n[data-chat-flow-kind=\"context\"]::before {\n content: \"⇢\";\n position: absolute;\n left: 2px;\n top: 12px;\n color: rgba(111, 219, 226, 0.7);\n font-size: 11px;\n line-height: 1;\n}\n[data-variant=\"think\"] {\n border-left: 2px solid rgba(111, 219, 226, 0.35);\n padding-left: 10px;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Tool-card chassis — the ui-primitives block family gets a DS case\n ──────────────────────────────────────────────────────────────────────── */\n[data-tool],\n[data-search],\n[data-read],\n[data-web],\n[data-diff],\n[data-terminal],\n[data-context-injection-body] {\n border: 1px solid rgba(140, 170, 215, 0.24) !important;\n border-radius: 0 !important;\n background:\n linear-gradient(115deg, rgba(140, 190, 255, 0.06) 0%, transparent 30%),\n linear-gradient(180deg, rgba(15, 20, 27, 0.88), rgba(9, 13, 18, 0.92)) !important;\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.05),\n inset 0 0 18px rgba(103, 158, 254, 0.05);\n position: relative;\n clip-path: polygon(\n 7px 0,\n 100% 0,\n 100% calc(100% - 7px),\n calc(100% - 7px) 100%,\n 0 100%,\n 0 7px\n );\n}\n[data-terminal]::before {\n content: \"❯_\";\n position: absolute;\n right: 8px;\n top: 6px;\n color: rgba(111, 219, 226, 0.35);\n font-size: 10px;\n font-family: ui-monospace, Consolas, monospace;\n}\n[data-read]::before {\n content: \"▤\";\n position: absolute;\n right: 8px;\n top: 6px;\n color: rgba(120, 150, 195, 0.4);\n font-size: 11px;\n}\n[data-search]::before {\n content: \"⌕\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(255, 180, 84, 0.4);\n font-size: 13px;\n}\n[data-web]::before {\n content: \"⌖\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(111, 219, 226, 0.4);\n font-size: 12px;\n}\n[data-diff]::before {\n content: \"⇄\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(120, 150, 195, 0.4);\n font-size: 12px;\n}\n[data-tool]::before {\n content: \"⚙\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(255, 180, 84, 0.4);\n font-size: 11px;\n}\n[data-context-injection-body]::before {\n content: \"⇢\";\n position: absolute;\n right: 8px;\n top: 5px;\n color: rgba(111, 219, 226, 0.4);\n font-size: 11px;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Composer details\n ──────────────────────────────────────────────────────────────────────── */\n[data-composer-seat] textarea {\n caret-color: #ffb454;\n}\n[data-composer-seat] textarea:focus {\n caret-color: #ffd9a0;\n}\n/* Composer seat: squared, no extra frame — a visible outline on the big hero\n card read as a jarring border. */\n[data-composer-seat] {\n border-radius: 0;\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Dialogs, menus, tooltips, toasts — the floating DS surfaces\n ──────────────────────────────────────────────────────────────────────── */\n[role=\"dialog\"] {\n border: 1px solid rgba(255, 180, 84, 0.35) !important;\n /* Inner hairline frame — the DS double-cased panel. */\n outline: 1px solid rgba(140, 170, 215, 0.22);\n outline-offset: -6px;\n border-radius: 0 !important;\n background: linear-gradient(180deg, rgba(15, 20, 27, 0.98), rgba(10, 14, 19, 0.99)) !important;\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.07),\n inset 0 0 26px rgba(103, 158, 254, 0.06) !important;\n}\n[role=\"menu\"] {\n border: 1px solid rgba(140, 170, 215, 0.3) !important;\n border-radius: 0 !important;\n background: rgba(13, 18, 25, 0.97) !important;\n}\n[role=\"menuitem\"]:hover {\n background: rgba(255, 180, 84, 0.08) !important;\n}\n[role=\"tooltip\"] {\n border: 1px solid rgba(140, 170, 215, 0.32) !important;\n border-radius: 0 !important;\n background: rgba(15, 20, 27, 0.97) !important;\n}\n[role=\"alert\"] {\n border: 1px solid rgba(255, 180, 84, 0.38) !important;\n border-radius: 0 !important;\n background: rgba(15, 20, 27, 0.97) !important;\n clip-path: polygon(\n 8px 0,\n 100% 0,\n 100% calc(100% - 8px),\n calc(100% - 8px) 100%,\n 0 100%,\n 0 8px\n );\n}\n\n/* Toast (the only alert portaled straight onto body): DS gold badge —\n amber case, dark ink, chamfered. Inline error rows keep the dark case\n above; this rule wins for the fixed top-center banner. */\nbody > [role=\"alert\"] {\n border: 1px solid rgba(255, 196, 120, 0.7) !important;\n border-radius: 0 !important;\n background: linear-gradient(180deg, #ffbe6b, #e09a3c) !important;\n color: rgb(28, 18, 6) !important;\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, 0.4),\n 0 10px 32px rgba(0, 0, 0, 0.5) !important;\n clip-path: polygon(\n 10px 0,\n 100% 0,\n 100% calc(100% - 10px),\n calc(100% - 10px) 100%,\n 0 100%,\n 0 10px\n );\n}\n\n/* Session-header action + utility buttons (Session log, jobs…):\n DS chamfered buttons. Session log lives in .utilities, jobs in .actions. */\n[data-slot=\"conversation.session.header.actions\"] button,\n[data-slot=\"conversation.session.header.utilities\"] button {\n border-radius: 0 !important;\n clip-path: polygon(\n 6px 0,\n 100% 0,\n 100% calc(100% - 6px),\n calc(100% - 6px) 100%,\n 0 100%,\n 0 6px\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n Sidebar: DS hairline on the conversation-history column\n ──────────────────────────────────────────────────────────────────────── */\n[data-sidebar-collapsed] > div:first-child {\n border-right: 1px solid rgba(140, 170, 215, 0.22);\n box-shadow: inset -1px 0 0 rgba(255, 180, 84, 0.06);\n}\n\n/* Sidebar buttons (New Session etc.): DS chamfered corners. */\n[data-slot=\"sidebar\"] button {\n border-radius: 0 !important;\n clip-path: polygon(\n 6px 0,\n 100% 0,\n 100% calc(100% - 6px),\n calc(100% - 6px) 100%,\n 0 100%,\n 0 6px\n );\n}\n\n/* Workspace rows (workspaces, sessions, groups): DS chamfered entries. */\n[role=\"treeitem\"] {\n border-radius: 0 !important;\n clip-path: polygon(\n 5px 0,\n 100% 0,\n 100% calc(100% - 5px),\n calc(100% - 5px) 100%,\n 0 100%,\n 0 5px\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────\n DS chamfer everywhere else: kill the round-corner language\n ──────────────────────────────────────────────────────────────────────── */\nbutton,\ninput,\ntextarea,\nselect,\n[role=\"tab\"],\n[role=\"menuitem\"],\n[role=\"treeitem\"] {\n border-radius: 0 !important;\n}\n\n`\n","/**\n * CHIRAL PULSE, browser half: the Death Stranding skin plus the BB\n * vital-signs strip under the composer stats.\n *\n * Two contributions:\n * 1. The global DS skin 鈥?a `--dsw-*` token remap (blue-black machine body,\n * amber hairlines, sand-paper light variant), the DeepSeek whale mark's\n * brand blues untouched, plus three pointer-transparent atmosphere\n * overlays (CRT scanlines, chiral lattice, vignette).\n * 2. The heartbeat strip on `conversation.composer.dock` 鈥?a 26px monitor\n * paper feed whose scrolling ECG is the hero and whose BPM follows the\n * session's live activity (model streaming, tools executing).\n *\n * The plugin owns no state of its own beyond the component's local beat\n * engine; every figure arrives through the session standard kit. All styles\n * ride one owned <style data-plugin> tag so the loader removes them on\n * unload/reload.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-conversation SlotMap merge (the composer.dock entry).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { HeartLine } from './HeartLine.tsx'\nimport { en, NS, zh, type ChiralKey } from './locales.ts'\nimport { CHIRAL_CSS } from './style.ts'\n\nexport { HeartLine } from './HeartLine.tsx'\nexport type { HeartLineProps } from './HeartLine.tsx'\nexport type { ChiralKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The BB monitor strip's copy. */\n chiral: ChiralKey\n }\n}\n\n/** Required services: the slot registry and the locale service. */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: register dictionaries, inject the DS sheet and the\n * atmosphere overlays, and dock the heartbeat strip under the composer.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'chiral-pulse: dictionaries')\n\n ctx.effect(() => {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'chiral-pulse'\n tag.textContent = CHIRAL_CSS\n document.head.appendChild(tag)\n return () => { tag.remove() }\n }, 'chiral-pulse: styles')\n\n // Atmosphere overlays: scanlines + chiral lattice + vignette, all\n // pointer-transparent, riding the top of the stacking order.\n ctx.effect(() => {\n const layers = [\n { className: 'cp-atmo cp-atmo-scanlines', label: 'scanlines' },\n { className: 'cp-atmo cp-atmo-lattice', label: 'lattice' },\n { className: 'cp-atmo cp-atmo-vignette', label: 'vignette' },\n ]\n const nodes = layers.map(({ className }) => {\n const el = document.createElement('div')\n el.className = className\n el.setAttribute('aria-hidden', 'true')\n document.body.appendChild(el)\n return el\n })\n return () => {\n for (const el of nodes) el.remove()\n }\n }, 'chiral-pulse: atmosphere')\n\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\n name: 'conversation.input.dock',\n id: 'chiral-pulse',\n // Above the composer card, under the goal strip: the pulse feed rides\n // with the input it monitors.\n order: 20,\n locale: NS,\n }, HeartLine))\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "chiral-pulse",
|
|
3
|
+
"version": "1.2.4",
|
|
4
|
+
"description": "CHIRAL PULSE 闂?a Death Stranding-styled BB pod vital-signs monitor for the DeepSeek Harness web UI: the session\u0027s heartbeat waveform is the hero, and the pulse reacts to real agent activity.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"death-stranding",
|
|
10
|
+
"bb-pod",
|
|
11
|
+
"heartbeat",
|
|
12
|
+
"ecg",
|
|
13
|
+
"ui-plugin"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "lib/index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./lib/index.js",
|
|
19
|
+
"./client": "./lib/client.js",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib/index.js",
|
|
24
|
+
"lib/client.js",
|
|
25
|
+
"lib/client.js.map",
|
|
26
|
+
"cordis.patch.yml"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"bundle": "tsdown",
|
|
30
|
+
"watch": "tsdown --watch",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"platform": "web",
|
|
39
|
+
"inject": [
|
|
40
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
41
|
+
"@deepseek-ai/dsh-client-locale",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "workspace:^",
|
|
48
|
+
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
|
49
|
+
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
|
52
|
+
"react": "^18.2.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "workspace:^",
|
|
56
|
+
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
|
57
|
+
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
|
60
|
+
"@deepseek-ai/dsh-session-stats": "workspace:^",
|
|
61
|
+
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
|
62
|
+
"@types/react": "~18.3.1",
|
|
63
|
+
"react": "^18.2.0",
|
|
64
|
+
"tsdown": "^0.22.2",
|
|
65
|
+
"typescript": "^6.0.3"
|
|
66
|
+
},
|
|
67
|
+
"license": "MIT"
|
|
68
|
+
}
|