@lament_z/dsh-client-ui-chat-timeline 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +1 -0
- package/README.md +10 -3
- package/README.zh.md +11 -3
- package/lib/client.js +128 -128
- package/lib/client.js.map +1 -1
- package/lib/types/client/rail.d.ts +5 -3
- package/lib/types/client/source.d.ts +16 -12
- package/lib/types/client/styles.d.ts +1 -1
- package/package.json +8 -7
package/README.i18n.yaml
CHANGED
package/README.md
CHANGED
|
@@ -10,12 +10,18 @@ A DSH web plugin that adds a **question navigator rail** beside the conversation
|
|
|
10
10
|
- **Click to jump, native mechanics.** Landing replicates the host ChatView's `navigateToTurn`/`landOnRow`: the turn's row is located by its native `data-chat-turn` anchor, scrolled instantly to sit 24px below the viewport top; turns outside the loaded window are paged in through the official `session.loadThrough(turn/start seq)` verb first.
|
|
11
11
|
- **Scroll-synced highlight.** The active tick follows the host's own reading-line rule (hit-test at `top + min(96, height * 0.2)`, last tick wins within 25px of the bottom), so the rail and the content never disagree.
|
|
12
12
|
- **Hover ripple and preview cards.** ZCode TurnNavigator parity: ripple `scaleX 2.6/1.7/1.25` at opacity `1/.86/.72/.58` over 150ms, 320px preview card after a 120ms delay (80ms close) showing the user prompt plus the assistant reply excerpt, styled with the host's native tooltip tokens (gray card, white text, follows light/dark theme).
|
|
13
|
-
- **Takes over the native rail, cleanly.** While the plugin is active, the host's built-in right-side Turn navigator is hidden; unloading the plugin restores it immediately. The plugin rail
|
|
14
|
-
- **
|
|
13
|
+
- **Takes over the native rail, cleanly.** While the plugin is active, the host's built-in right-side Turn navigator is hidden; unloading the plugin restores it immediately. The plugin rail sets no question-count or width gate of its own.
|
|
14
|
+
- **Native band geometry, computed in CSS.** The rail's vertical position is the host's own band rule — viewport height minus composer height, halved — expressed as a single CSS `calc()` over the host's `--dsh-conversation-viewport-height` / `--dsh-composer-height` variables. No JavaScript measures it, and the rail no longer needs the native navigator to exist in the DOM in order to locate its band.
|
|
15
|
+
|
|
16
|
+
## How it mounts
|
|
17
|
+
|
|
18
|
+
The rail registers on the host's session-scoped `conversation.input.overlay` seat, and that choice is load-bearing. Since DSH 0.1.7 the open session is no longer discoverable from the sessions service — the list snapshot dropped its `current` field and `binding(id)` only borrows an already-retained scope — so a session-scoped seat is the only supported channel: the host passes the scope's session id into the registration's `inject`. Registering there also places the rail inside the conversation scroller, which is where the band variables are defined and therefore inherited from.
|
|
19
|
+
|
|
20
|
+
Vertical geometry consequently needs no measurement at all. The horizontal left edge is the one measured value: the conversation column is a grid track that the right sidebar shrinks, so it is not derivable from a window-edge offset.
|
|
15
21
|
|
|
16
22
|
## Requirements
|
|
17
23
|
|
|
18
|
-
- DSH `>=0.1.1-rc.1
|
|
24
|
+
- DSH `>=0.1.1-rc.1`. This revision is built and tested against `0.1.7-rc.2`; the seat it registers on and that seat's session-scoped `inject` are verified on `0.1.5-rc.2` as well.
|
|
19
25
|
- Pure browser plugin: the host half is an empty cordis plugin; nothing to configure.
|
|
20
26
|
|
|
21
27
|
## Install
|
|
@@ -36,6 +42,7 @@ Then restart `dsh web` and reload the page. The rail appears beside any conversa
|
|
|
36
42
|
## Notes
|
|
37
43
|
|
|
38
44
|
- Jump and highlight reuse the host's stable contracts: `data-chat-turn` row anchors, `[data-conversation-scroll]` scrollport, `session.loadThrough`, and the `turnOutline` projection. When detection fails, the rail degrades to display-only instead of throwing.
|
|
45
|
+
- The rail is `position: fixed` and positioned from the host's layout variables, so it assumes no ancestor of the conversation establishes a containing block (`transform` / `filter` / `contain`). That holds in every layout this revision was verified against.
|
|
39
46
|
- The preview card is the only deliberately theme-coupled surface: it uses the host's tooltip tokens for the native gray look; the rail itself sticks to system `Canvas`/`CanvasText` colors.
|
|
40
47
|
- Failure mode is a display-only rail (or no rail), never a broken page.
|
|
41
48
|
|
package/README.zh.md
CHANGED
|
@@ -10,12 +10,18 @@ DSH Web 插件:在会话区旁加一条**问题导航刻度条**——每个
|
|
|
10
10
|
- **点击跳转,原生机制。** 落位复刻宿主 ChatView 的 `navigateToTurn`/`landOnRow`:按原生 `data-chat-turn` 锚点找到轮次行,瞬时滚动到行顶距视口顶 24px 处;轮次不在已加载窗口时,先走官方 `session.loadThrough(turn/start seq)` 翻页再落位。
|
|
11
11
|
- **滚动同步高亮。** 当前刻度跟随宿主自己的阅读线规则(`top + min(96, 高度*0.2)` 处命中测试,距底部 25px 内最后一刻度胜出),刻度条与内容永远一致。
|
|
12
12
|
- **悬停涟漪与预览卡。** 与 ZCode TurnNavigator 对齐:涟漪 `scaleX 2.6/1.7/1.25`、不透明度 `1/.86/.72/.58`、150ms 过渡;320px 预览卡延迟 120ms 弹出(80ms 关闭),展示用户提问 + 助手回复摘要;配色使用宿主原生 tooltip 令牌(灰底白字,跟随亮/暗主题)。
|
|
13
|
-
- **干净地接管原生导航条。** 插件存活期间,宿主内置的右侧 Turn
|
|
14
|
-
-
|
|
13
|
+
- **干净地接管原生导航条。** 插件存活期间,宿主内置的右侧 Turn 导航条被隐藏;卸载插件立即恢复。本刻度条自身不设条数或宽度门槛。
|
|
14
|
+
- **原生 band 几何,纯 CSS 计算。** 刻度条的纵向位置就是宿主自己的 band 规则——视口高减输入框高再取半——写成一条 CSS `calc()`,直接读宿主 `--dsh-conversation-viewport-height` / `--dsh-composer-height` 变量。没有任何 JavaScript 测量,也不再依赖原生导航条存在于 DOM 才能定位自己的 band。
|
|
15
|
+
|
|
16
|
+
## 挂载方式
|
|
17
|
+
|
|
18
|
+
刻度条注册在宿主的 **session 作用域** seat `conversation.input.overlay` 上,这个选择是承重的。DSH 0.1.7 起,打开的会话不再能从 sessions 服务里查到——list 快照删掉了 `current` 字段,`binding(id)` 也只借已保留的 scope——因此 session 作用域 seat 是唯一受支持的通道:宿主会把该作用域的 session id 传进注册的 `inject`。注册在这里同时让刻度条落在会话滚动容器内部,而 band 变量正是在该容器上定义、因而能被继承。
|
|
19
|
+
|
|
20
|
+
于是纵向几何完全不需要测量。唯一保留的测量值是横向左沿:会话列是一条 grid 轨道,右侧栏打开时会把它挤窄,因此无法由窗口边缘偏移推导。
|
|
15
21
|
|
|
16
22
|
## 要求
|
|
17
23
|
|
|
18
|
-
- DSH `>=0.1.1-rc.1
|
|
24
|
+
- DSH `>=0.1.1-rc.1`。本版基于 `0.1.7-rc.2` 构建与实测;它注册的 seat 及该 seat 的 session 作用域 `inject` 在 `0.1.5-rc.2` 上同样验证过。
|
|
19
25
|
- 纯浏览器插件:host 半区为空 cordis 插件,无需任何配置。
|
|
20
26
|
|
|
21
27
|
## 安装
|
|
@@ -36,7 +42,9 @@ dsh plugin --profile web add link:<本目录>
|
|
|
36
42
|
## 说明
|
|
37
43
|
|
|
38
44
|
- 跳转与高亮复用宿主稳定契约:`data-chat-turn` 行锚点、`[data-conversation-scroll]` 滚动容器、`session.loadThrough` 与 `turnOutline` 投影。探测失败时降级为纯展示轨(或无轨),绝不做坏页面。
|
|
45
|
+
- 刻度条为 `position: fixed` 且按宿主布局变量定位,因此前提是会话的祖先链上没有元素建立 containing block(`transform` / `filter` / `contain`)。本版验证过的所有布局均满足该前提。
|
|
39
46
|
- 预览卡是有意耦合主题的唯一表面:使用宿主 tooltip 令牌实现原生灰;刻度条本体坚持系统 `Canvas`/`CanvasText` 配色。
|
|
47
|
+
- 失败形态是纯展示轨(或无轨),绝不会是坏页面。
|
|
40
48
|
|
|
41
49
|
## 许可
|
|
42
50
|
|
package/lib/client.js
CHANGED
|
@@ -174,6 +174,17 @@ window.__ModuleLoader__.load({
|
|
|
174
174
|
position: fixed;
|
|
175
175
|
z-index: 10;
|
|
176
176
|
width: 48px;
|
|
177
|
+
/* 水平:会话列左沿,由 rail 写入 --dsh-tl-left。这是唯一保留的测量值——
|
|
178
|
+
列是 grid 轨道,右侧栏打开时会被挤压,不贴窗口右沿,无法由窗口宽度推导。 */
|
|
179
|
+
left: var(--dsh-tl-left, 0px);
|
|
180
|
+
/* 垂直:原生 TurnNavigator 的 band 模型(eGxaPq_frame 的 --turn-rail-band)。
|
|
181
|
+
会话可视带 = 视口高 − 输入框高;其中点在视口坐标下为
|
|
182
|
+
100dvh − (会话可视高 + 输入框高) / 2
|
|
183
|
+
两个变量由宿主写在滚动容器上,本 seat 在滚动容器内可直接继承,故整个垂直
|
|
184
|
+
几何是纯 CSS:不做测量,也不依赖原生 nav 是否存在于 DOM。 */
|
|
185
|
+
--dsh-tl-band: calc(var(--dsh-conversation-viewport-height, 100dvh) - var(--dsh-composer-height, 152px));
|
|
186
|
+
top: calc(100dvh - (var(--dsh-conversation-viewport-height, 100dvh) + var(--dsh-composer-height, 152px)) / 2);
|
|
187
|
+
transform: translateY(-50%);
|
|
177
188
|
pointer-events: none;
|
|
178
189
|
opacity: 1;
|
|
179
190
|
transition: opacity 150ms ease-out;
|
|
@@ -183,14 +194,11 @@ window.__ModuleLoader__.load({
|
|
|
183
194
|
visibility: hidden;
|
|
184
195
|
}
|
|
185
196
|
.dsh-tl-scroll {
|
|
186
|
-
position:
|
|
197
|
+
position: relative;
|
|
187
198
|
left: 12px;
|
|
188
|
-
/* 原生 TurnNavigator 定位(eGxaPq_frame):中心 = 内容顶 + (视口高 − 输入框高)/2,
|
|
189
|
-
即 --turn-rail-band 的中点;centerY 由 rail 按同一公式写入。 */
|
|
190
|
-
top: var(--dsh-tl-center-y, 50%);
|
|
191
|
-
transform: translateY(-50%);
|
|
192
199
|
width: 36px;
|
|
193
|
-
|
|
200
|
+
/* 原生 frame 的高度上限:band − 64px,再夹到 420px。 */
|
|
201
|
+
max-height: min(max(0px, calc(var(--dsh-tl-band) - 64px)), 420px);
|
|
194
202
|
overflow-x: hidden;
|
|
195
203
|
overflow-y: auto;
|
|
196
204
|
padding-block: 4px;
|
|
@@ -305,20 +313,24 @@ nav[aria-label='轮次导航'] {
|
|
|
305
313
|
//#endregion
|
|
306
314
|
//#region src/client/rail.tsx
|
|
307
315
|
/**
|
|
308
|
-
* TimelineRail — the
|
|
309
|
-
* conversation, one tick per human question. Visual and interaction
|
|
310
|
-
* replicate the ZCode TurnNavigator (see .scratch/chat-timeline/spec.md):
|
|
316
|
+
* TimelineRail — the conversation.input.overlay entry: a left-edge tick rail
|
|
317
|
+
* over the conversation, one tick per human question. Visual and interaction
|
|
318
|
+
* constants replicate the ZCode TurnNavigator (see .scratch/chat-timeline/spec.md):
|
|
311
319
|
* hover ripple scaleX 2.6/1.7/1.25 with opacity 1/.86/.72/.58 over 150ms,
|
|
312
|
-
* 320px preview cards after a 120ms delay,
|
|
313
|
-
*
|
|
320
|
+
* 320px preview cards after a 120ms delay, prefers-reduced-motion fallback,
|
|
321
|
+
* full aria labelling.
|
|
322
|
+
*
|
|
323
|
+
* Geometry follows the native TurnNavigator model: the vertical band
|
|
324
|
+
* (--dsh-tl-band, the host's viewport-minus-composer rule) is computed in CSS
|
|
325
|
+
* from the host variables that the seat inherits, so no JS measures it. Only
|
|
326
|
+
* the horizontal left edge is measured — the conversation column is a grid
|
|
327
|
+
* track, so it is not flush to the window edge once the right sidebar opens.
|
|
314
328
|
*/
|
|
315
329
|
/** Slot height per tick (10px pitch, ZCode parity). */
|
|
316
330
|
const ITEM_PITCH = 10;
|
|
317
331
|
/** Preview-card hover delays (ms). */
|
|
318
332
|
const TIP_OPEN_DELAY = 120;
|
|
319
333
|
const TIP_CLOSE_DELAY = 80;
|
|
320
|
-
/** Default composer height when the host variable is absent (native fallback). */
|
|
321
|
-
const COMPOSER_HEIGHT_FALLBACK = 152;
|
|
322
334
|
/** Ripple visual per distance from the hovered tick (ZCode _5e parity).
|
|
323
335
|
* The ripple is interaction-only: at rest every tick is idle (scaleX 1),
|
|
324
336
|
* and the current position is signalled by color/opacity, not length. */
|
|
@@ -354,11 +366,11 @@ nav[aria-label='轮次导航'] {
|
|
|
354
366
|
* @param props - composed slot props.
|
|
355
367
|
* @returns the rail, or null when it should not render.
|
|
356
368
|
*/
|
|
357
|
-
function TimelineRail({ source, t }) {
|
|
369
|
+
function TimelineRail({ sessionId, source, t }) {
|
|
358
370
|
const state = (0, react.useSyncExternalStore)(source.subscribe, source.getSnapshot);
|
|
359
371
|
const probe = (0, react.useMemo)(() => typeof document === "undefined" ? null : probeChatDom(document), []);
|
|
360
|
-
const [
|
|
361
|
-
const [
|
|
372
|
+
const [container, setContainer] = (0, react.useState)(null);
|
|
373
|
+
const [navEl, setNavEl] = (0, react.useState)(null);
|
|
362
374
|
const [activeIndex, setActiveIndex] = (0, react.useState)(-1);
|
|
363
375
|
const [hoverIndex, setHoverIndex] = (0, react.useState)(void 0);
|
|
364
376
|
const [tipIndex, setTipIndex] = (0, react.useState)(void 0);
|
|
@@ -389,8 +401,9 @@ nav[aria-label='轮次导航'] {
|
|
|
389
401
|
let attempts = 0;
|
|
390
402
|
let timer;
|
|
391
403
|
const tick = () => {
|
|
392
|
-
|
|
393
|
-
|
|
404
|
+
const found = probe.getContainer();
|
|
405
|
+
if (found !== null || attempts >= 10) {
|
|
406
|
+
setContainer(found);
|
|
394
407
|
return;
|
|
395
408
|
}
|
|
396
409
|
attempts += 1;
|
|
@@ -400,32 +413,27 @@ nav[aria-label='轮次导航'] {
|
|
|
400
413
|
return () => {
|
|
401
414
|
if (timer !== void 0) clearTimeout(timer);
|
|
402
415
|
};
|
|
403
|
-
}, [probe,
|
|
416
|
+
}, [probe, sessionId]);
|
|
404
417
|
(0, react.useEffect)(() => {
|
|
405
|
-
if (
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
418
|
+
if (container === null || navEl === null) return;
|
|
419
|
+
const write = () => {
|
|
420
|
+
navEl.style.setProperty("--dsh-tl-left", `${container.getBoundingClientRect().left}px`);
|
|
421
|
+
};
|
|
422
|
+
write();
|
|
423
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(write);
|
|
424
|
+
observer?.observe(container);
|
|
425
|
+
window.addEventListener("resize", write);
|
|
426
|
+
return () => {
|
|
427
|
+
observer?.disconnect();
|
|
428
|
+
window.removeEventListener("resize", write);
|
|
429
|
+
};
|
|
430
|
+
}, [container, navEl]);
|
|
431
|
+
(0, react.useEffect)(() => {
|
|
432
|
+
if (container === null || probe === null) return;
|
|
411
433
|
const sync = () => {
|
|
412
434
|
if (frame.current !== 0) return;
|
|
413
435
|
frame.current = requestAnimationFrame(() => {
|
|
414
436
|
frame.current = 0;
|
|
415
|
-
const box = container.getBoundingClientRect();
|
|
416
|
-
const style = getComputedStyle(container);
|
|
417
|
-
const composerHeight = Number.parseFloat(style.getPropertyValue("--dsh-composer-height")) || COMPOSER_HEIGHT_FALLBACK;
|
|
418
|
-
const viewportHeight = Number.parseFloat(style.getPropertyValue("--dsh-conversation-viewport-height")) || window.innerHeight;
|
|
419
|
-
const slot = document.querySelector("nav[aria-label=\"Turn navigation\"]")?.parentElement;
|
|
420
|
-
const slotTop = slot !== null && slot !== void 0 ? slot.getBoundingClientRect().top : box.top;
|
|
421
|
-
const bandCenter = (viewportHeight - composerHeight) / 2;
|
|
422
|
-
setRect({
|
|
423
|
-
left: box.left,
|
|
424
|
-
top: box.top,
|
|
425
|
-
height: box.height,
|
|
426
|
-
width: box.width,
|
|
427
|
-
centerY: Math.max(0, slotTop - box.top + bandCenter)
|
|
428
|
-
});
|
|
429
437
|
setActiveIndex(probe.activeIndex(turnsRef.current));
|
|
430
438
|
});
|
|
431
439
|
};
|
|
@@ -441,17 +449,16 @@ nav[aria-label='轮次导航'] {
|
|
|
441
449
|
if (frame.current !== 0) cancelAnimationFrame(frame.current);
|
|
442
450
|
frame.current = 0;
|
|
443
451
|
};
|
|
444
|
-
}, [
|
|
452
|
+
}, [container, probe]);
|
|
445
453
|
(0, react.useEffect)(() => {
|
|
446
|
-
if (
|
|
454
|
+
if (container === null || probe === null) return;
|
|
447
455
|
setActiveIndex(probe.activeIndex(turns));
|
|
448
456
|
}, [
|
|
457
|
+
container,
|
|
449
458
|
probe,
|
|
450
|
-
containerVersion,
|
|
451
|
-
rect,
|
|
452
459
|
turns
|
|
453
460
|
]);
|
|
454
|
-
const visible = snapshot !== null
|
|
461
|
+
const visible = snapshot !== null;
|
|
455
462
|
const focusIndex = hoverIndex;
|
|
456
463
|
const scheduleTip = (0, react.useCallback)((index) => {
|
|
457
464
|
if (timers.current.open !== void 0) window.clearTimeout(timers.current.open);
|
|
@@ -469,21 +476,16 @@ nav[aria-label='轮次导航'] {
|
|
|
469
476
|
const jump = (0, react.useCallback)((index) => {
|
|
470
477
|
jumpToTurn(document, index, turnsRef.current, seqsRef.current, (seq) => sourceRef.current?.jumpThrough(seq) ?? Promise.resolve());
|
|
471
478
|
}, []);
|
|
472
|
-
if (!visible || probe === null
|
|
479
|
+
if (!visible || probe === null) return null;
|
|
473
480
|
const trackHeight = items.length * ITEM_PITCH;
|
|
474
481
|
const tip = tipIndex !== void 0 ? items[tipIndex] : void 0;
|
|
475
482
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
|
|
483
|
+
ref: setNavEl,
|
|
476
484
|
"aria-label": t("nav.label"),
|
|
477
485
|
className: "dsh-tl-nav",
|
|
478
486
|
"data-visible": visible ? "true" : "false",
|
|
479
487
|
"data-testid": "dsh-chat-timeline",
|
|
480
488
|
"data-item-count": items.length,
|
|
481
|
-
style: {
|
|
482
|
-
left: rect.left,
|
|
483
|
-
top: rect.top,
|
|
484
|
-
height: rect.height,
|
|
485
|
-
"--dsh-tl-center-y": `${rect.centerY}px`
|
|
486
|
-
},
|
|
487
489
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
488
490
|
className: "dsh-tl-scroll",
|
|
489
491
|
onPointerLeave: () => {
|
|
@@ -553,18 +555,19 @@ nav[aria-label='轮次导航'] {
|
|
|
553
555
|
}), tip !== void 0 && tipIndex !== void 0 ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TipCard, {
|
|
554
556
|
item: tip,
|
|
555
557
|
index: tipIndex,
|
|
556
|
-
|
|
558
|
+
nav: navEl,
|
|
557
559
|
t
|
|
558
560
|
}), document.body) : null] });
|
|
559
561
|
}
|
|
560
|
-
function TipCard({ item, index,
|
|
561
|
-
const box =
|
|
562
|
+
function TipCard({ item, index, nav, t }) {
|
|
563
|
+
const box = (nav?.querySelector("[data-testid=\"dsh-chat-timeline-item\"][data-item-index=\"" + String(index) + "\"]"))?.getBoundingClientRect();
|
|
564
|
+
const railBox = nav?.getBoundingClientRect();
|
|
562
565
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
563
566
|
className: "dsh-tl-tip",
|
|
564
567
|
"data-testid": "dsh-chat-timeline-tip",
|
|
565
568
|
style: {
|
|
566
|
-
left: box
|
|
567
|
-
top: box
|
|
569
|
+
left: box !== void 0 ? Math.min(box.right + 8, window.innerWidth - 336) : railBox !== void 0 ? railBox.right + 8 : 8,
|
|
570
|
+
top: box !== void 0 ? Math.max(8, Math.min(box.top - 4, window.innerHeight - 160)) : 8
|
|
568
571
|
},
|
|
569
572
|
role: "tooltip",
|
|
570
573
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -579,10 +582,6 @@ nav[aria-label='轮次导航'] {
|
|
|
579
582
|
}
|
|
580
583
|
//#endregion
|
|
581
584
|
//#region src/client/source.ts
|
|
582
|
-
const EMPTY_STATE = {
|
|
583
|
-
sessionId: void 0,
|
|
584
|
-
snapshot: null
|
|
585
|
-
};
|
|
586
585
|
/** Project one outline entry onto a tick (previews are host-bounded already). */
|
|
587
586
|
function itemFromOutline(entry, isLive, labels) {
|
|
588
587
|
const prompt = typeof entry.prompt === "string" ? entry.prompt : "";
|
|
@@ -605,55 +604,50 @@ nav[aria-label='轮次导航'] {
|
|
|
605
604
|
const last = outline.length - 1;
|
|
606
605
|
return outline.map((entry, index) => itemFromOutline(entry, running && index === last, labels));
|
|
607
606
|
}
|
|
607
|
+
/** Fallback preview labels when the outline entry carries no text. */
|
|
608
|
+
const PREVIEW_LABELS = {
|
|
609
|
+
userFallback: "(无文本)",
|
|
610
|
+
assistantRunning: "(运行中)",
|
|
611
|
+
assistantEmpty: "(空)"
|
|
612
|
+
};
|
|
608
613
|
/**
|
|
609
|
-
* Create the source.
|
|
610
|
-
* session
|
|
611
|
-
* is
|
|
612
|
-
*
|
|
614
|
+
* Create the source for one session. The id is supplied by the host — the
|
|
615
|
+
* session-scoped seat hands it to `inject` — so this no longer guesses which
|
|
616
|
+
* session is open and no longer watches the sessions list: it binds straight
|
|
617
|
+
* to that session's outline projection and republishes whenever the outline or
|
|
618
|
+
* the running flag moves.
|
|
619
|
+
*
|
|
620
|
+
* A bounded retry covers the materialization window (the seat can render a
|
|
621
|
+
* beat before `binding(id)` answers); after that the rail stays hidden.
|
|
613
622
|
* @param sessions - the sessions service face (pass ctx.sessions).
|
|
623
|
+
* @param sessionId - the session this rail belongs to.
|
|
614
624
|
* @returns the source face.
|
|
615
625
|
*/
|
|
616
|
-
function createTimelineSource(sessions) {
|
|
617
|
-
let state =
|
|
626
|
+
function createTimelineSource(sessions, sessionId) {
|
|
627
|
+
let state = {
|
|
628
|
+
sessionId,
|
|
629
|
+
snapshot: null
|
|
630
|
+
};
|
|
618
631
|
const listeners = /* @__PURE__ */ new Set();
|
|
619
632
|
let subscribed = false;
|
|
620
|
-
let unbindSession = null;
|
|
621
633
|
let retryTimer;
|
|
622
634
|
let retryAttempts = 0;
|
|
623
|
-
let noSessionPolls = 0;
|
|
624
635
|
const emit = () => {
|
|
625
636
|
for (const listener of listeners) listener();
|
|
626
637
|
};
|
|
627
|
-
const
|
|
628
|
-
if (retryTimer !== void 0) {
|
|
629
|
-
clearTimeout(retryTimer);
|
|
630
|
-
retryTimer = void 0;
|
|
631
|
-
}
|
|
632
|
-
let current;
|
|
638
|
+
const readBinding = () => {
|
|
633
639
|
try {
|
|
634
|
-
|
|
640
|
+
return sessions.binding(sessionId);
|
|
635
641
|
} catch {
|
|
636
|
-
current = void 0;
|
|
637
|
-
}
|
|
638
|
-
if (current === void 0) {
|
|
639
|
-
state = EMPTY_STATE;
|
|
640
|
-
emit();
|
|
641
|
-
if (subscribed && noSessionPolls < 60) {
|
|
642
|
-
noSessionPolls += 1;
|
|
643
|
-
retryTimer = setTimeout(rebind, 1e3);
|
|
644
|
-
}
|
|
645
642
|
return;
|
|
646
643
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
bound = sessions.binding(current);
|
|
653
|
-
} catch {
|
|
654
|
-
bound = void 0;
|
|
644
|
+
};
|
|
645
|
+
const bind = () => {
|
|
646
|
+
if (retryTimer !== void 0) {
|
|
647
|
+
clearTimeout(retryTimer);
|
|
648
|
+
retryTimer = void 0;
|
|
655
649
|
}
|
|
656
|
-
const session =
|
|
650
|
+
const session = readBinding()?.session;
|
|
657
651
|
const outlineFace = (() => {
|
|
658
652
|
try {
|
|
659
653
|
return session?.projections?.faceOf?.("turnOutline");
|
|
@@ -663,14 +657,14 @@ nav[aria-label='轮次导航'] {
|
|
|
663
657
|
})();
|
|
664
658
|
if (!session || !outlineFace) {
|
|
665
659
|
state = {
|
|
666
|
-
sessionId
|
|
660
|
+
sessionId,
|
|
667
661
|
snapshot: null
|
|
668
662
|
};
|
|
669
|
-
|
|
663
|
+
emit();
|
|
664
|
+
if (subscribed && retryAttempts < 10) {
|
|
670
665
|
retryAttempts += 1;
|
|
671
|
-
retryTimer = setTimeout(
|
|
666
|
+
retryTimer = setTimeout(bind, 250 * retryAttempts);
|
|
672
667
|
}
|
|
673
|
-
emit();
|
|
674
668
|
return;
|
|
675
669
|
}
|
|
676
670
|
retryAttempts = 0;
|
|
@@ -679,20 +673,16 @@ nav[aria-label='轮次导航'] {
|
|
|
679
673
|
const outline = outlineFace.getSnapshot();
|
|
680
674
|
const running = Boolean(session.getSnapshot()?.running);
|
|
681
675
|
state = {
|
|
682
|
-
sessionId
|
|
676
|
+
sessionId,
|
|
683
677
|
snapshot: {
|
|
684
|
-
sessionId
|
|
685
|
-
items: Array.isArray(outline) ? itemsFromOutline(outline, running,
|
|
686
|
-
userFallback: "(无文本)",
|
|
687
|
-
assistantRunning: "(运行中)",
|
|
688
|
-
assistantEmpty: "(空)"
|
|
689
|
-
}) : [],
|
|
678
|
+
sessionId,
|
|
679
|
+
items: Array.isArray(outline) ? itemsFromOutline(outline, running, PREVIEW_LABELS) : [],
|
|
690
680
|
running
|
|
691
681
|
}
|
|
692
682
|
};
|
|
693
683
|
} catch {
|
|
694
684
|
state = {
|
|
695
|
-
sessionId
|
|
685
|
+
sessionId,
|
|
696
686
|
snapshot: null
|
|
697
687
|
};
|
|
698
688
|
}
|
|
@@ -702,10 +692,7 @@ nav[aria-label='轮次导航'] {
|
|
|
702
692
|
try {
|
|
703
693
|
outlineFace.subscribe(push);
|
|
704
694
|
session.subscribe(push);
|
|
705
|
-
|
|
706
|
-
} catch {
|
|
707
|
-
unbindSession = null;
|
|
708
|
-
}
|
|
695
|
+
} catch {}
|
|
709
696
|
};
|
|
710
697
|
return {
|
|
711
698
|
subscribe(listener) {
|
|
@@ -713,10 +700,7 @@ nav[aria-label='轮次导航'] {
|
|
|
713
700
|
listeners.add(listener);
|
|
714
701
|
if (first) {
|
|
715
702
|
subscribed = true;
|
|
716
|
-
|
|
717
|
-
sessions.list.subscribe(rebind);
|
|
718
|
-
} catch {}
|
|
719
|
-
rebind();
|
|
703
|
+
bind();
|
|
720
704
|
}
|
|
721
705
|
return () => {
|
|
722
706
|
listeners.delete(listener);
|
|
@@ -726,21 +710,15 @@ nav[aria-label='轮次导航'] {
|
|
|
726
710
|
return state;
|
|
727
711
|
},
|
|
728
712
|
jumpThrough(seq) {
|
|
729
|
-
|
|
730
|
-
try {
|
|
731
|
-
current = sessions.list.getSnapshot()?.current;
|
|
732
|
-
} catch {
|
|
733
|
-
current = void 0;
|
|
734
|
-
}
|
|
735
|
-
const session = current === void 0 ? void 0 : sessions.binding(current)?.session;
|
|
713
|
+
const session = readBinding()?.session;
|
|
736
714
|
if (typeof session?.loadThrough !== "function") return Promise.resolve();
|
|
737
715
|
return session.loadThrough(seq).catch(() => {});
|
|
738
716
|
}
|
|
739
717
|
};
|
|
740
718
|
}
|
|
741
|
-
/** Build the source from the plugin client context. */
|
|
742
|
-
function timelineSourceFromContext(ctx) {
|
|
743
|
-
return createTimelineSource(ctx.sessions);
|
|
719
|
+
/** Build the source from the plugin client context and the seat's session id. */
|
|
720
|
+
function timelineSourceFromContext(ctx, sessionId) {
|
|
721
|
+
return createTimelineSource(ctx.sessions, sessionId);
|
|
744
722
|
}
|
|
745
723
|
//#endregion
|
|
746
724
|
//#region src/client/locales.ts
|
|
@@ -769,7 +747,25 @@ nav[aria-label='轮次导航'] {
|
|
|
769
747
|
//#region src/client/index.ts
|
|
770
748
|
/** Dictionary namespace owned by this plugin. */
|
|
771
749
|
const NS = "chat-timeline";
|
|
772
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* Session-scoped seat the rail occupies.
|
|
752
|
+
*
|
|
753
|
+
* DSH 0.1.7 moved "which session is open" out of the sessions service: the list
|
|
754
|
+
* snapshot no longer carries `current`, `binding(id)` only borrows an already
|
|
755
|
+
* retained scope, and session-controller states outright that view selection
|
|
756
|
+
* stays outside the Controller. The only supported way to learn the open
|
|
757
|
+
* session is a session-scoped seat, whose `inject` receives the scope
|
|
758
|
+
* binding's key — the session id. A root-scope seat (shell.overlay) cannot be
|
|
759
|
+
* told, which is why the rail no longer lives there.
|
|
760
|
+
*
|
|
761
|
+
* `conversation.input.overlay` is rendered for every open session, and unlike
|
|
762
|
+
* `conversation.view` it is not enumerated into the conversation's view tabs.
|
|
763
|
+
* It also sits inside the conversation scroller, so the host's geometry
|
|
764
|
+
* variables (--dsh-conversation-viewport-height / --dsh-composer-height)
|
|
765
|
+
* inherit to the rail.
|
|
766
|
+
*/
|
|
767
|
+
const SEAT = "conversation.input.overlay";
|
|
768
|
+
/** Unique occupant id inside the shared conversation.input.overlay list slot. */
|
|
773
769
|
const ENTRY_ID = "chat-timeline";
|
|
774
770
|
/** Services required by this plugin. */
|
|
775
771
|
const inject = [
|
|
@@ -792,16 +788,20 @@ nav[aria-label='轮次导航'] {
|
|
|
792
788
|
return () => {};
|
|
793
789
|
}
|
|
794
790
|
}, "chat-timeline: dictionaries");
|
|
795
|
-
ctx.slots.inject(
|
|
791
|
+
ctx.slots.inject(SEAT, () => {
|
|
796
792
|
try {
|
|
797
793
|
return ctx.slots.register({
|
|
798
|
-
name:
|
|
794
|
+
name: SEAT,
|
|
799
795
|
id: ENTRY_ID,
|
|
796
|
+
order: 0,
|
|
800
797
|
locale: NS,
|
|
801
|
-
inject: () => ({
|
|
798
|
+
inject: (sessionId) => ({
|
|
799
|
+
sessionId,
|
|
800
|
+
source: timelineSourceFromContext(ctx, sessionId)
|
|
801
|
+
})
|
|
802
802
|
}, TimelineRail);
|
|
803
803
|
} catch (err) {
|
|
804
|
-
console.error(
|
|
804
|
+
console.error(`chat-timeline: ${SEAT} register failed:`, err);
|
|
805
805
|
return () => {};
|
|
806
806
|
}
|
|
807
807
|
});
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client/dom.ts","../src/client/styles.ts","../src/client/rail.tsx","../src/client/source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Chat DOM probe — the only module allowed to know DSH's internal chat DOM.\n *\n * Contract replicated 1:1 from the native chat view (dsh-client-ui-chat\n * 0.1.5-rc.2, ChatView):\n * - every rendered row carries `data-chat-turn=\"<turn>\"` (plus\n * `data-chat-anchor-key` / `data-chat-flow-key` with the node key, which is\n * NOT seq-prefixed — never parse seqs out of flow keys);\n * - the scrollport is `row.closest('[data-conversation-scroll]')`;\n * - landing on a turn is an instant `el.scrollTop += flowTop(row, el) - 24`;\n * - a turn outside the loaded event window is paged in through the official\n * `session.loadThrough(turn/start seq)` verb, then landed once its row\n * renders (the native pendingJump/settle loop, plugin-side as a poll);\n * - the active turn reads the line `top + min(96, height * 0.2)` via\n * `elementsFromPoint` hit-testing with a row-scan fallback, and the last\n * turn wins within 25px of the scroll bottom.\n * Every access is defensive: when detection fails the probe degrades to a\n * no-op rail (no jump, no highlight) instead of throwing.\n */\n\n/** Read-only view of the chat DOM the rail interacts with. */\nexport interface ChatDomProbe {\n /** The chat scroll container, or null when not found. */\n getContainer(): HTMLElement | null\n /** Index of the tick nearest the reading line, or -1. */\n activeIndex(turns: readonly (number | undefined)[]): number\n}\n\n/**\n * Find the chat scroll container with a candidate chain.\n * 1. the explicit stable hook `[data-conversation-scroll]`;\n * 2. the closest scrollable ancestor of the flow column `[data-chat-flow]`;\n * 3. the largest scrollable element that contains keyed flow items.\n */\nexport function findChatContainer(doc: Document): HTMLElement | null {\n const direct = doc.querySelector<HTMLElement>('[data-conversation-scroll]')\n if (direct !== null) return direct\n const flow = doc.querySelector<HTMLElement>('[data-chat-flow]')\n const ancestor = flow !== null ? scrollableAncestor(flow) : null\n if (ancestor !== null) return ancestor\n let best: HTMLElement | null = null\n let bestHeight = 0\n for (const element of Array.from(doc.querySelectorAll<HTMLElement>('*'))) {\n if (!isScrollable(element)) continue\n if (element.querySelector('[data-chat-turn]') === null) continue\n const height = element.clientHeight\n if (height > bestHeight) {\n best = element\n bestHeight = height\n }\n }\n return best\n}\n\nfunction isScrollable(element: HTMLElement): boolean {\n const style = element.ownerDocument.defaultView?.getComputedStyle(element)\n if (style === undefined) return false\n if (!/(auto|scroll)/.test(style.overflowY)) return false\n return element.scrollHeight > element.clientHeight + 40 && element.clientHeight > 200\n}\n\nfunction scrollableAncestor(element: HTMLElement): HTMLElement | null {\n let node: HTMLElement | null = element\n while (node !== null) {\n if (isScrollable(node)) return node\n node = node.parentElement\n }\n return null\n}\n\n/**\n * The rendered row of `turn` (native anchor attribute `data-chat-turn`),\n * skipping hidden rows like the native `anchorElement`. Scoped to the chat\n * container when it is already located (native scopes to its own list), so a\n * stray second mount cannot hijack the anchor. Null when the turn's events\n * are not in the loaded window yet.\n */\nexport function findTurnRow(doc: Document, turn: number | undefined): HTMLElement | null {\n if (turn === undefined || !Number.isSafeInteger(turn) || turn < 0) return null\n const scope: ParentNode = findChatContainer(doc) ?? doc\n return scope.querySelector<HTMLElement>(`[data-chat-turn=\"${turn}\"]:not([hidden])`)\n}\n\n/** Native `scrollerOf`: the conversation scrollport owning the row. */\nfunction scrollerOfRow(doc: Document, row: HTMLElement): HTMLElement | null {\n return row.closest<HTMLElement>('[data-conversation-scroll]') ?? findChatContainer(doc)\n}\n\n/** Row position in scrollport coordinates (native `flowTop`). */\nfunction flowTop(row: HTMLElement, scroller: HTMLElement): number {\n return row.getBoundingClientRect().top - scroller.getBoundingClientRect().top\n}\n\n/**\n * Native `landOnRow`: instant scroll so the row's top sits 24px below the\n * scrollport top. Returns false when the scrollport cannot be resolved.\n */\nexport function landOnRow(doc: Document, row: HTMLElement): boolean {\n const el = scrollerOfRow(doc, row)\n if (el === null) return false\n el.scrollTop += flowTop(row, el) - 24\n return true\n}\n\n/**\n * Native `turnAtLine`: the turn owning the row at a scrollport line.\n * Hit-tests the line first (elementsFromPoint at the container's horizontal\n * center) and falls back to one row scan when layout cannot answer (jsdom,\n * pre-paint). Returns null when no loaded row covers the line.\n */\nexport function turnAtLine(doc: Document, container: HTMLElement, line: number): number | null {\n const content = container.getBoundingClientRect()\n if (typeof doc.elementsFromPoint === 'function' && content.width > 0) {\n for (const element of doc.elementsFromPoint(content.left + content.width / 2, line)) {\n const row = element instanceof HTMLElement ? element.closest<HTMLElement>('[data-chat-turn]') : null\n const turn = Number(row?.dataset.chatTurn)\n if (row !== null && container.contains(row) && Number.isSafeInteger(turn)) return turn\n }\n }\n let found: number | null = null\n for (const row of Array.from(container.querySelectorAll<HTMLElement>('[data-chat-turn]'))) {\n if (row.getBoundingClientRect().top > line) break\n const turn = Number(row.dataset.chatTurn)\n if (Number.isSafeInteger(turn)) found = turn\n }\n return found\n}\n\n/**\n * The active index (native `syncActiveTurn`): within 25px of the scroll\n * bottom the last tick wins; otherwise the reading line sits at\n * `top + min(96, height * 0.2)` and the active tick is the last one whose\n * turn does not exceed the turn read at that line. When the line covers no\n * loaded row, earlier ticks outside the window count as above the viewport\n * (the window head's predecessor is active, matching 0.1.1 parity).\n */\nexport function computeActiveIndex(\n container: HTMLElement,\n doc: Document,\n turns: readonly (number | undefined)[],\n): number {\n if (turns.length === 0) return -1\n if (container.scrollHeight - container.scrollTop - container.clientHeight <= 25) {\n return turns.length - 1\n }\n const firstDefined = turns.findIndex((turn) => turn !== undefined)\n if (firstDefined === -1) return -1\n const reading = container.getBoundingClientRect().top + Math.min(96, container.clientHeight * 0.2)\n const readTurn = turnAtLine(doc, container, reading)\n let active = -1\n if (readTurn !== null) {\n for (let index = 0; index < turns.length; index += 1) {\n const turn = turns[index]\n if (turn === undefined || turn > readTurn) continue\n active = index\n }\n }\n if (active !== -1) return active\n return firstDefined > 0 ? firstDefined - 1 : 0\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/** Landing poll budget after `loadThrough` (native settles via its effects; 5s here). */\nconst LAND_POLL_ATTEMPTS = 20\nconst LAND_POLL_INTERVAL_MS = 250\n\n/**\n * 跳到第 index 个刻度的完整落点流程(原生 navigateToTurn 的插件侧等价物):\n * 1. 按 `data-chat-turn` 找行并原生落位(瞬时、行顶距视口顶 24px);\n * 2. 行未挂载时先 `loadThrough(turn/start 的 seq)` 把事件窗口翻页到位;\n * 3. 轮询等行渲染后落位。任何一步成功即返回 true。\n */\nexport async function jumpToTurn(\n doc: Document,\n index: number,\n turns: readonly (number | undefined)[],\n seqs: readonly number[],\n loadThrough: ((seq: number) => Promise<void>) | undefined,\n): Promise<boolean> {\n const turn = turns[index]\n if (turn === undefined) return false\n if (landOnRowByTurn(doc, turn)) return true\n const seq = seqs[index]\n if (seq !== undefined && loadThrough !== undefined) {\n try {\n await loadThrough(seq)\n } catch {\n // 翻页失败仍然轮询(窗口可能已部分就位)。\n }\n }\n for (let attempt = 0; attempt < LAND_POLL_ATTEMPTS; attempt += 1) {\n await sleep(LAND_POLL_INTERVAL_MS)\n if (landOnRowByTurn(doc, turn)) return true\n }\n return false\n}\n\nfunction landOnRowByTurn(doc: Document, turn: number): boolean {\n const row = findTurnRow(doc, turn)\n if (row === null) return false\n return landOnRow(doc, row)\n}\n\n/** Create the probe against a document (injectable for tests). */\nexport function probeChatDom(doc: Document): ChatDomProbe {\n return {\n getContainer: () => findChatContainer(doc),\n activeIndex: (turns) => {\n const container = findChatContainer(doc)\n if (container === null) return -1\n return computeActiveIndex(container, doc, turns)\n },\n }\n}\n","/**\n * Stylesheet for the chat-timeline rail, injected once as a\n * `<style data-plugin-css=\"chat-timeline\">` tag. All classes carry the\n * `dsh-tl-` prefix; colors use the CSS system colors Canvas/CanvasText so the\n * rail follows the app's color-scheme without reaching into DSH theme tokens.\n * Visual constants replicate the ZCode TurnNavigator (spec section 2.2).\n */\nexport const TIMELINE_STYLES = `\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n pointer-events: none;\n opacity: 1;\n transition: opacity 150ms ease-out;\n}\n.dsh-tl-nav[data-visible=\"false\"] {\n opacity: 0;\n visibility: hidden;\n}\n.dsh-tl-scroll {\n position: absolute;\n left: 12px;\n /* 原生 TurnNavigator 定位(eGxaPq_frame):中心 = 内容顶 + (视口高 − 输入框高)/2,\n 即 --turn-rail-band 的中点;centerY 由 rail 按同一公式写入。 */\n top: var(--dsh-tl-center-y, 50%);\n transform: translateY(-50%);\n width: 36px;\n max-height: calc(100% - 96px);\n overflow-x: hidden;\n overflow-y: auto;\n padding-block: 4px;\n pointer-events: auto;\n scrollbar-width: none;\n color: CanvasText;\n}\n.dsh-tl-scroll::-webkit-scrollbar {\n display: none;\n}\n.dsh-tl-track {\n position: relative;\n width: 36px;\n}\n.dsh-tl-slot {\n position: absolute;\n left: 0;\n top: 0;\n height: 10px;\n width: 36px;\n padding: 0;\n border: 0;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n border-radius: 2px;\n cursor: pointer;\n}\n.dsh-tl-slot:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n}\n.dsh-tl-tick {\n display: block;\n height: 2px;\n width: 12px;\n border-radius: 999px;\n background: currentColor;\n transform-origin: left center;\n transition: height 150ms ease-out, opacity 150ms ease-out, transform 150ms ease-out, background-color 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .dsh-tl-nav,\n .dsh-tl-tick {\n transition: none;\n }\n}\n.dsh-tl-tip {\n position: fixed;\n z-index: 60;\n width: 320px;\n max-width: calc(100vw - 2rem);\n padding: 12px;\n border-radius: 10px;\n /* 原生 tooltip 配对(dsh-web-frontend _bubble_):灰底 + 静态白字,\n 主题切换由别名变量跟随;灰底与页面背景天然分层。 */\n background: var(--dsw-alias-tooltip-bg, #43454a);\n color: var(--dsw-static-neutral-bluish-00, #fff);\n border: none;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n}\n.dsh-tl-tip-user {\n margin: 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n}\n.dsh-tl-tip-assistant {\n margin: 8px 0 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-static-neutral-bluish-300, rgba(255, 255, 255, 0.8));\n}\n.dsh-tl-tip-assistant[data-kind=\"running\"],\n.dsh-tl-tip-assistant[data-kind=\"empty\"] {\n color: var(--dsw-static-neutral-bluish-400, rgba(255, 255, 255, 0.55));\n}\n/* 插件完全接管原生右侧 Turn 导航条:本样式表只随插件存在,插件卸载时\n removeStyles 移除标签,原生导航条立即恢复。aria-label 跟随宿主语言\n (英文 Turn navigation / 中文 轮次导航),两个都要盖住。 */\nnav[aria-label='Turn navigation'],\nnav[aria-label='轮次导航'] {\n display: none !important;\n}\n`\n\n/** Inject the stylesheet once per document. */\nexport function ensureStyles(doc: Document): void {\n const tagId = 'chat-timeline'\n if (doc.querySelector('style[data-plugin-css=\"chat-timeline\"]') !== null) return\n const tag = doc.createElement('style')\n tag.dataset.plugin = '@lament_z/dsh-client-ui-chat-timeline'\n tag.dataset.pluginCss = tagId\n tag.textContent = TIMELINE_STYLES\n doc.head.appendChild(tag)\n}\n\n/** Remove the stylesheet (plugin teardown): restores the native turn navigator. */\nexport function removeStyles(doc: Document): void {\n doc.querySelector('style[data-plugin-css=\"chat-timeline\"]')?.remove()\n}\n","/**\n * TimelineRail — the shell.overlay entry: a left-edge tick rail over the\n * conversation, one tick per human question. Visual and interaction constants\n * replicate the ZCode TurnNavigator (see .scratch/chat-timeline/spec.md):\n * hover ripple scaleX 2.6/1.7/1.25 with opacity 1/.86/.72/.58 over 150ms,\n * 320px preview cards after a 120ms delay, >=2 questions to render, >=864px\n * container width, prefers-reduced-motion fallback, full aria labelling.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { type TimelineItem } from './directory.ts'\nimport type { TimelineKey } from './locales.ts'\nimport { jumpToTurn, probeChatDom, type ChatDomProbe } from './dom.ts'\nimport { ensureStyles, removeStyles } from './styles.ts'\nimport type { TimelineSource } from './source.ts'\n\n/** Component props: the locale seat plus the injected session source. */\nexport type TimelineRailProps = PropsLocale<'chat-timeline'> & {\n /** The current-session snapshot source (built from ctx.sessions). */\n source: TimelineSource\n}\n\n/** Slot height per tick (10px pitch, ZCode parity). */\nconst ITEM_PITCH = 10\n/** Preview-card hover delays (ms). */\nconst TIP_OPEN_DELAY = 120\nconst TIP_CLOSE_DELAY = 80\n\ninterface Rect {\n left: number\n top: number\n height: number\n width: number\n /** Rail band center, viewport-relative to the container top (native --turn-rail-band rule). */\n centerY: number\n}\n\n/** Default composer height when the host variable is absent (native fallback). */\nconst COMPOSER_HEIGHT_FALLBACK = 152\n\n/** Ripple visual per distance from the hovered tick (ZCode _5e parity).\n * The ripple is interaction-only: at rest every tick is idle (scaleX 1),\n * and the current position is signalled by color/opacity, not length. */\nfunction ripple(distance: number): { opacity: number; scaleX: number; tone: 'peak' | 'near' | 'mid' | 'idle' } {\n if (distance === 0) return { opacity: 1, scaleX: 2.6, tone: 'peak' }\n if (distance === 1) return { opacity: 0.86, scaleX: 1.7, tone: 'near' }\n if (distance === 2) return { opacity: 0.72, scaleX: 1.25, tone: 'mid' }\n return { opacity: 0.58, scaleX: 1, tone: 'idle' }\n}\n\n/** Tick colors: foreground for the hovered peak and the current turn at rest,\n * the subtlest text tone otherwise (ZCode bg-foreground / -subtlest parity\n * through system colors). */\nconst TICK_COLOR_FOREGROUND = 'CanvasText'\nconst TICK_COLOR_SUBTLE = 'color-mix(in srgb, CanvasText 42%, transparent)'\n\n/**\n * Render the question navigator rail.\n * @param props - composed slot props.\n * @returns the rail, or null when it should not render.\n */\nexport function TimelineRail({ source, t }: TimelineRailProps) {\n const state = useSyncExternalStore(source.subscribe, source.getSnapshot)\n const probe = useMemo<ChatDomProbe | null>(() => (typeof document === 'undefined' ? null : probeChatDom(document)), [])\n const [containerVersion, setContainerVersion] = useState(0)\n const [rect, setRect] = useState<Rect | null>(null)\n const [activeIndex, setActiveIndex] = useState(-1)\n const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined)\n const [tipIndex, setTipIndex] = useState<number | undefined>(undefined)\n const timers = useRef<{ open: number | undefined; close: number | undefined }>({ open: undefined, close: undefined })\n const frame = useRef(0)\n\n const snapshot = state.snapshot\n // 官方 turnOutline 数据面:source 直接产出刻度(宿主已算好全史预览)。\n const items = useMemo<TimelineItem[]>(\n () => (snapshot === null ? [] : (snapshot.items as TimelineItem[])),\n [snapshot],\n )\n const sourceRef = useRef(source)\n sourceRef.current = source\n const itemsRef = useRef<TimelineItem[]>([])\n itemsRef.current = items\n // 0.1.5 原生锚点:行按 `data-chat-turn=\"<turn>\"` 键控(dsh-client-ui-chat\n // ChatView),跳转与高亮都按轮次号走原生逻辑;seq 仅用于 loadThrough 翻页。\n const seqs = useMemo(() => items.map((item) => item.seq), [items])\n const seqsRef = useRef<number[]>([])\n seqsRef.current = seqs\n const turns = useMemo(() => items.map((item) => item.turn), [items])\n const turnsRef = useRef<(number | undefined)[]>([])\n turnsRef.current = turns\n\n useEffect(() => {\n if (typeof document === 'undefined') return\n ensureStyles(document)\n // 卸载时移除样式表:隐藏原生 Turn 导航的规则随标签消失,原生立即恢复。\n return () => removeStyles(document)\n }, [])\n\n // Find the chat container: re-probe when the session changes and briefly\n // afterwards (the conversation mounts a beat later than the selection).\n useEffect(() => {\n if (probe === null) return\n let attempts = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n const tick = () => {\n const container = probe.getContainer()\n if (container !== null || attempts >= 10) {\n setContainerVersion((version) => version + 1)\n return\n }\n attempts += 1\n timer = setTimeout(tick, 300)\n }\n tick()\n return () => {\n if (timer !== undefined) clearTimeout(timer)\n }\n }, [probe, state.sessionId])\n\n // Attach scroll/size watchers to the container once found.\n useEffect(() => {\n if (probe === null || containerVersion === 0) return\n const container = probe.getContainer()\n if (container === null) {\n setRect(null)\n return\n }\n const sync = () => {\n if (frame.current !== 0) return\n frame.current = requestAnimationFrame(() => {\n frame.current = 0\n const box = container.getBoundingClientRect()\n // 原生 TurnNavigator(eGxaPq_frame)定位复刻:\n // 中心 = sticky 槽位 y + (视口高 − 输入框高) / 2,两个高度都读宿主变量。\n // 槽位即原生导航条的父元素(sticky, height 0),拿不到时退化为容器顶。\n const style = getComputedStyle(container)\n const composerHeight = Number.parseFloat(style.getPropertyValue('--dsh-composer-height')) || COMPOSER_HEIGHT_FALLBACK\n const viewportHeight = Number.parseFloat(style.getPropertyValue('--dsh-conversation-viewport-height')) || window.innerHeight\n const slot = document.querySelector('nav[aria-label=\"Turn navigation\"]')?.parentElement\n const slotTop = slot !== null && slot !== undefined ? slot.getBoundingClientRect().top : box.top\n const bandCenter = (viewportHeight - composerHeight) / 2\n setRect({\n left: box.left,\n top: box.top,\n height: box.height,\n width: box.width,\n centerY: Math.max(0, slotTop - box.top + bandCenter),\n })\n setActiveIndex(probe.activeIndex(turnsRef.current))\n })\n }\n sync()\n container.addEventListener('scroll', sync, { passive: true })\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(sync)\n observer?.observe(container)\n window.addEventListener('resize', sync)\n return () => {\n container.removeEventListener('scroll', sync)\n observer?.disconnect()\n window.removeEventListener('resize', sync)\n if (frame.current !== 0) cancelAnimationFrame(frame.current)\n frame.current = 0\n }\n }, [probe, containerVersion])\n\n // items 变化(事件窗口增长/切换会话)后重算一次 active 高亮。\n useEffect(() => {\n if (probe === null || containerVersion === 0 || rect === null) return\n setActiveIndex(probe.activeIndex(turns))\n }, [probe, containerVersion, rect, turns])\n\n // 完全模仿原生:只要会话可读且容器在,刻度条就渲染——不设条数/宽度门槛,\n // 原生导航条的隐藏完全交给本插件的接管规则。\n const visible = snapshot !== null && rect !== null\n // The ripple follows the pointer only (ZCode v5e parity): at rest every tick\n // is equal length and the current turn stands out by color/opacity alone.\n const focusIndex = hoverIndex\n\n const scheduleTip = useCallback((index: number | undefined) => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n if (index === undefined) {\n timers.current.close = window.setTimeout(() => setTipIndex(undefined), TIP_CLOSE_DELAY)\n return\n }\n timers.current.open = window.setTimeout(() => setTipIndex(index), TIP_OPEN_DELAY)\n }, [])\n\n useEffect(() => () => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n }, [])\n\n const jump = useCallback((index: number) => {\n // 原生 navigateToTurn 流程:行已挂载直接落位;未挂载走官方 loadThrough\n // 翻页后落位。落位是瞬时滚动(原生 landOnRow 语义),无需 motion 分支。\n void jumpToTurn(\n document,\n index,\n turnsRef.current,\n seqsRef.current,\n (seq: number) => sourceRef.current?.jumpThrough(seq) ?? Promise.resolve(),\n )\n }, [])\n\n if (!visible || probe === null || rect === null) return null\n\n const trackHeight = items.length * ITEM_PITCH\n const tip = tipIndex !== undefined ? items[tipIndex] : undefined\n\n return (\n <>\n <nav\n aria-label={t('nav.label')}\n className=\"dsh-tl-nav\"\n data-visible={visible ? 'true' : 'false'}\n data-testid=\"dsh-chat-timeline\"\n data-item-count={items.length}\n style={{ left: rect.left, top: rect.top, height: rect.height, '--dsh-tl-center-y': `${rect.centerY}px` } as React.CSSProperties}\n >\n <div\n className=\"dsh-tl-scroll\"\n onPointerLeave={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <div className=\"dsh-tl-track\" style={{ height: `${trackHeight}px` }}>\n {items.map((item, index) => {\n const isActive = index === activeIndex\n const distance = focusIndex === undefined ? 3 : Math.abs(index - focusIndex)\n const look = ripple(distance)\n const activeAtRest = focusIndex === undefined && isActive\n const foreground = look.tone === 'peak' || activeAtRest\n const opacity = activeAtRest\n ? 0.9\n : item.running\n ? Math.max(look.opacity, 0.72)\n : look.opacity\n return (\n <div\n key={item.key}\n className=\"dsh-tl-slot\"\n style={{ transform: `translateY(${index * ITEM_PITCH}px)` }}\n onMouseEnter={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onFocus={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onBlur={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <button\n type=\"button\"\n aria-current={isActive ? 'location' : undefined}\n aria-label={t('nav.jumpToQuery', { index: String(index + 1) })}\n aria-posinset={index + 1}\n aria-setsize={items.length}\n data-testid=\"dsh-chat-timeline-item\"\n data-item-index={index}\n data-active={isActive ? 'true' : 'false'}\n data-running={item.running ? 'true' : 'false'}\n onClick={() => jump(index)}\n className=\"dsh-tl-slot-inner\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-start',\n width: '100%',\n height: '100%',\n padding: 0,\n border: 0,\n background: 'transparent',\n cursor: 'pointer',\n }}\n >\n <span\n className=\"dsh-tl-tick\"\n style={{\n opacity,\n transform: `scaleX(${look.scaleX})`,\n backgroundColor: foreground ? TICK_COLOR_FOREGROUND : TICK_COLOR_SUBTLE,\n }}\n />\n </button>\n </div>\n )\n })}\n </div>\n </div>\n </nav>\n {tip !== undefined && tipIndex !== undefined\n ? createPortal(\n <TipCard item={tip} index={tipIndex} rect={rect} t={t} />,\n document.body,\n )\n : null}\n </>\n )\n}\n\nfunction TipCard({ item, index, rect, t }: {\n item: TimelineItem\n index: number\n rect: Rect\n t: (key: TimelineKey, params?: Record<string, string | number>) => string\n}) {\n const slot = document.querySelector<HTMLElement>('[data-testid=\"dsh-chat-timeline-item\"][data-item-index=\"' + String(index) + '\"]')\n const box = slot?.getBoundingClientRect()\n const left = box === undefined ? rect.left + 56 : Math.min(box.right + 8, window.innerWidth - 336)\n const top = box === undefined ? rect.top + 100 : Math.max(8, Math.min(box.top - 4, window.innerHeight - 160))\n return (\n <div\n className=\"dsh-tl-tip\"\n data-testid=\"dsh-chat-timeline-tip\"\n style={{ left, top }}\n role=\"tooltip\"\n >\n <p className=\"dsh-tl-tip-user\">{item.userFallback ? t('preview.userFallback') : item.userPreview}</p>\n <p className=\"dsh-tl-tip-assistant\" data-kind={item.assistantKind}>\n {item.assistantKind === 'running'\n ? t('preview.assistantRunning')\n : item.assistantKind === 'empty'\n ? t('preview.assistantEmpty')\n : item.assistantPreview}\n </p>\n </div>\n )\n}\n","/**\n * Timeline source: follows the current session through the cordis services\n * face (ctx.sessions) and exposes a useSyncExternalStore-compatible read\n * face over the host-computed turn outline. Client context usage here is\n * type-only; at runtime everything is reached through the ctx services, so\n * the client bundle stays free of cross-package value imports.\n */\n// DSH-0.1.2-A1-25: dsh-client-runtime 包已删除,ClientContext 即 cordis Context。\n// ctx.sessions 的 Context 合并自 session-controller 的 client 入口(A1-25 迁移表)。\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS, type TimelineItem } from './directory.ts'\n\n/**\n * The slice of the turn outline the timeline consumes — 官方 `turnOutline`\n * 投影(session-turn-outline 域)的 wire 条目:宿主对整份日志 fold 出的\n * 全史轮次索引,无需客户端翻页。\n */\nexport interface TurnOutlineEntryLike {\n readonly turn: number\n readonly seq: number\n readonly prompt?: string\n readonly response?: string\n}\n\n/** The slice of ConversationSnapshot the timeline consumes. */\nexport interface TimelineSnapshot {\n readonly sessionId: string\n /** One tick per known turn, ascending(官方 outline 顺序)。 */\n readonly items: readonly TimelineItem[]\n /** Whether the session currently has a running turn. */\n readonly running: boolean\n}\n\n/** State handed to React: null snapshot means \"no readable session right now\". */\nexport interface TimelineState {\n readonly sessionId: string | undefined\n readonly snapshot: TimelineSnapshot | null\n}\n\n/** useSyncExternalStore-compatible source face. */\nexport interface TimelineSource {\n subscribe(listener: () => void): () => void\n getSnapshot(): TimelineState\n /**\n * Page the history window back through the given turn's seq(官方\n * `loadThrough` turn-jump loader),so the turn's rows mount and the DOM\n * probe can scroll to them. No-op when the session face lacks the verb.\n */\n jumpThrough(seq: number): Promise<void>\n}\n\ninterface ObservableLike<T> {\n getSnapshot(): T\n subscribe(listener: () => void): () => void\n}\n\n/**\n * Structural face of ctx.sessions this module needs (kept narrow for tests).\n * 0.1.5: `binding.session.projections.faceOf('turnOutline')` is the official\n * whole-log turn index; `loadThrough(seq)` is the turn-jump pagination verb.\n */\nexport interface SessionsLike {\n list: ObservableLike<{ current?: string }>\n binding(\n id: string,\n ): {\n session: ObservableLike<{ running?: boolean }> & {\n loadThrough?(seq: number): Promise<void>\n projections?: {\n faceOf?(key: string): ObservableSnapshotLike<unknown>\n }\n }\n eventSource: ObservableLike<unknown>\n } | undefined\n}\n\n/** ObservableSnapshot 的最小结构面(getSnapshot/subscribe)。 */\ntype ObservableSnapshotLike<T> = ObservableLike<T>\n\nconst EMPTY_STATE: TimelineState = { sessionId: undefined, snapshot: null }\n\n/** Project one outline entry onto a tick (previews are host-bounded already). */\nexport function itemFromOutline(\n entry: TurnOutlineEntryLike,\n isLive: boolean,\n labels: { userFallback: string; assistantRunning: string; assistantEmpty: string },\n): TimelineItem {\n const prompt = typeof entry.prompt === 'string' ? entry.prompt : ''\n const response = typeof entry.response === 'string' ? entry.response : ''\n const assistantKind: TimelineItem['assistantKind'] = response\n ? 'text'\n : isLive\n ? 'running'\n : 'empty'\n return {\n key: `t${entry.turn}`,\n seq: entry.seq,\n time: 0,\n turn: entry.turn,\n userPreview: prompt || labels.userFallback,\n userFallback: !prompt,\n assistantPreview: response || (isLive ? labels.assistantRunning : labels.assistantEmpty),\n assistantKind,\n running: isLive,\n }\n}\n\n/** Build the tick list from the outline plus the live running flag. */\nexport function itemsFromOutline(\n outline: readonly TurnOutlineEntryLike[],\n running: boolean,\n labels: { userFallback: string; assistantRunning: string; assistantEmpty: string },\n): TimelineItem[] {\n const last = outline.length - 1\n return outline.map((entry, index) =>\n itemFromOutline(entry, running && index === last, labels),\n )\n}\n\n// 保留目录数学的导出面(rail 不再走 nodes 路径,preview 预算常量仍被引用)。\nexport { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS }\n\n/**\n * Create the source. Subscribes to the sessions list, rebinds to the current\n * session's outline projection on selection change (polling while no session\n * is selected — the list store need not notify on pure selection switches),\n * and republishes a stable state whenever the outline or running flag moves.\n * @param sessions - the sessions service face (pass ctx.sessions).\n * @returns the source face.\n */\nexport function createTimelineSource(sessions: SessionsLike): TimelineSource {\n let state: TimelineState = EMPTY_STATE\n const listeners = new Set<() => void>()\n let subscribed = false\n let unbindSession: (() => void) | null = null\n let boundSessionId: string | undefined\n let retryTimer: ReturnType<typeof setTimeout> | undefined\n let retryAttempts = 0\n let noSessionPolls = 0\n\n const emit = () => {\n for (const listener of listeners) listener()\n }\n\n const rebind = () => {\n if (retryTimer !== undefined) {\n clearTimeout(retryTimer)\n retryTimer = undefined\n }\n let current: string | undefined\n try {\n current = sessions.list.getSnapshot()?.current\n } catch {\n current = undefined\n }\n if (current === undefined) {\n // 尚未选中会话:list 存储在纯选择切换时可能不通知,轮询等它出现。\n state = EMPTY_STATE\n emit()\n if (subscribed && noSessionPolls < 60) {\n noSessionPolls += 1\n retryTimer = setTimeout(rebind, 1000)\n }\n return\n }\n noSessionPolls = 0\n boundSessionId = current\n unbindSession?.()\n unbindSession = null\n let bound: ReturnType<NonNullable<SessionsLike['binding']>> | undefined\n try {\n bound = sessions.binding(current)\n } catch {\n bound = undefined\n }\n const session = bound?.session\n const outlineFace = (() => {\n try {\n return session?.projections?.faceOf?.('turnOutline') as\n | ObservableLike<readonly TurnOutlineEntryLike[]>\n | undefined\n } catch {\n return undefined\n }\n })()\n if (!session || !outlineFace) {\n // Listed but not yet scoped (cold boot): keep an empty state and retry\n // with backoff so the rail appears once the binding lands.\n state = { sessionId: current, snapshot: null }\n if (retryAttempts < 10) {\n retryAttempts += 1\n retryTimer = setTimeout(rebind, 500 * retryAttempts)\n }\n emit()\n return\n }\n retryAttempts = 0\n const push = () => {\n try {\n const outline = outlineFace.getSnapshot()\n const running = Boolean(session.getSnapshot()?.running)\n state = {\n sessionId: current,\n snapshot: {\n sessionId: current,\n items: Array.isArray(outline)\n ? itemsFromOutline(outline, running, {\n userFallback: '(无文本)',\n assistantRunning: '(运行中)',\n assistantEmpty: '(空)',\n })\n : [],\n running,\n },\n }\n } catch {\n state = { sessionId: current, snapshot: null }\n }\n emit()\n }\n push()\n try {\n outlineFace.subscribe(push)\n session.subscribe(push)\n unbindSession = () => {\n // Observable faces expose no unsubscribe disposer contract here; the\n // subscription rides the plugin fiber and is torn down with it.\n }\n } catch {\n unbindSession = null\n }\n }\n\n return {\n subscribe(listener: () => void): () => void {\n const first = listeners.size === 0\n listeners.add(listener)\n if (first) {\n subscribed = true\n try {\n sessions.list.subscribe(rebind)\n } catch {\n // Service unavailable: the rail stays hidden, nothing throws.\n }\n rebind()\n }\n return () => {\n listeners.delete(listener)\n }\n },\n getSnapshot(): TimelineState {\n return state\n },\n jumpThrough(seq: number): Promise<void> {\n let current: string | undefined\n try {\n current = sessions.list.getSnapshot()?.current\n } catch {\n current = undefined\n }\n const session = current === undefined ? undefined : sessions.binding(current)?.session\n if (typeof session?.loadThrough !== 'function') return Promise.resolve()\n return session.loadThrough(seq).catch(() => {})\n },\n }\n}\n\n/** Build the source from the plugin client context. */\nexport function timelineSourceFromContext(ctx: ClientContext): TimelineSource {\n return createTimelineSource(ctx.sessions as unknown as SessionsLike)\n}\n","/**\n * Locale dictionaries for the chat-timeline plugin. `zh` is the key-set source\n * of truth; `en` keeps a full key-for-key mirror. Registered through\n * ctx.locale.register(NS, { zh, en }).\n */\n\n/** Simplified Chinese dictionary (key-set source of truth). */\nexport const zh = {\n 'nav.label': '对话问题导航',\n 'nav.jumpToQuery': '跳转到第 {index} 条问题',\n 'preview.userFallback': '用户输入',\n 'preview.assistantEmpty': '暂无助手正文',\n 'preview.assistantRunning': '助手仍在工作',\n}\n\n/** The chat-timeline namespace key union. */\nexport type TimelineKey = keyof typeof zh\n\n/** English dictionary, key-for-key complete against zh. */\nexport const en: Record<TimelineKey, string> = {\n 'nav.label': 'Conversation question navigator',\n 'nav.jumpToQuery': 'Jump to question {index}',\n 'preview.userFallback': 'User input',\n 'preview.assistantEmpty': 'No assistant text yet',\n 'preview.assistantRunning': 'Assistant is still working',\n}\n","/**\n * Chat-timeline plugin — browser half. Registers the `chat-timeline` locale\n * dictionaries and a `shell.overlay` entry that renders the question\n * navigator rail beside the conversation. Export discipline: the /client\n * surface carries only what cordis loading needs plus types.\n */\n// DSH-0.1.2-A1-25: dsh-client-runtime 包已删除,ClientContext 即 cordis Context。\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its\n// LocaleNamespaceMap merge table.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-layout SlotMap merge (the 'shell.overlay' hole).\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\n// DSH-0.1.5-A1-14/A2-13: ctx.slots 的 Context 合并自 ui-renderer 的 client 入口。\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport { TimelineRail } from './rail.tsx'\nimport { timelineSourceFromContext } from './source.ts'\nimport { en, zh, type TimelineKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Chat-timeline surface copy. */\n 'chat-timeline': TimelineKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'chat-timeline'\n\n/** Unique occupant id inside the shared shell.overlay list slot. */\nconst ENTRY_ID = 'chat-timeline'\n\n/** Services required by this plugin. */\nexport const inject = ['slots', 'locale', 'sessions']\n\n/**\n * Register the timeline surface.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => {\n try {\n return ctx.locale.register(NS, { zh, en })\n } catch {\n return () => {}\n }\n }, 'chat-timeline: dictionaries')\n\n // The rail floats over the conversation via the frame-wide additive seat.\n // Registration is declaration-aware via slots.inject. 注册失败降级为一条\n // console 错误(不抛进 loader,也不静默——留诊断线索)。\n ctx.slots.inject('shell.overlay', () => {\n try {\n return ctx.slots.register({\n name: 'shell.overlay',\n id: ENTRY_ID,\n locale: NS,\n inject: () => ({ source: timelineSourceFromContext(ctx) }),\n }, TimelineRail)\n } catch (err) {\n console.error('chat-timeline: shell.overlay register failed:', err)\n return () => {}\n }\n })\n}\n\nexport type { TimelineRailProps } from './rail.tsx'\nexport type { TimelineKey } from './locales.ts'\nexport type { TimelineSource, TimelineSnapshot, TimelineState } from './source.ts'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client/dom.ts","../src/client/styles.ts","../src/client/rail.tsx","../src/client/source.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Chat DOM probe — the only module allowed to know DSH's internal chat DOM.\n *\n * Contract replicated 1:1 from the native chat view (dsh-client-ui-chat\n * 0.1.5-rc.2, ChatView):\n * - every rendered row carries `data-chat-turn=\"<turn>\"` (plus\n * `data-chat-anchor-key` / `data-chat-flow-key` with the node key, which is\n * NOT seq-prefixed — never parse seqs out of flow keys);\n * - the scrollport is `row.closest('[data-conversation-scroll]')`;\n * - landing on a turn is an instant `el.scrollTop += flowTop(row, el) - 24`;\n * - a turn outside the loaded event window is paged in through the official\n * `session.loadThrough(turn/start seq)` verb, then landed once its row\n * renders (the native pendingJump/settle loop, plugin-side as a poll);\n * - the active turn reads the line `top + min(96, height * 0.2)` via\n * `elementsFromPoint` hit-testing with a row-scan fallback, and the last\n * turn wins within 25px of the scroll bottom.\n * Every access is defensive: when detection fails the probe degrades to a\n * no-op rail (no jump, no highlight) instead of throwing.\n */\n\n/** Read-only view of the chat DOM the rail interacts with. */\nexport interface ChatDomProbe {\n /** The chat scroll container, or null when not found. */\n getContainer(): HTMLElement | null\n /** Index of the tick nearest the reading line, or -1. */\n activeIndex(turns: readonly (number | undefined)[]): number\n}\n\n/**\n * Find the chat scroll container with a candidate chain.\n * 1. the explicit stable hook `[data-conversation-scroll]`;\n * 2. the closest scrollable ancestor of the flow column `[data-chat-flow]`;\n * 3. the largest scrollable element that contains keyed flow items.\n */\nexport function findChatContainer(doc: Document): HTMLElement | null {\n const direct = doc.querySelector<HTMLElement>('[data-conversation-scroll]')\n if (direct !== null) return direct\n const flow = doc.querySelector<HTMLElement>('[data-chat-flow]')\n const ancestor = flow !== null ? scrollableAncestor(flow) : null\n if (ancestor !== null) return ancestor\n let best: HTMLElement | null = null\n let bestHeight = 0\n for (const element of Array.from(doc.querySelectorAll<HTMLElement>('*'))) {\n if (!isScrollable(element)) continue\n if (element.querySelector('[data-chat-turn]') === null) continue\n const height = element.clientHeight\n if (height > bestHeight) {\n best = element\n bestHeight = height\n }\n }\n return best\n}\n\nfunction isScrollable(element: HTMLElement): boolean {\n const style = element.ownerDocument.defaultView?.getComputedStyle(element)\n if (style === undefined) return false\n if (!/(auto|scroll)/.test(style.overflowY)) return false\n return element.scrollHeight > element.clientHeight + 40 && element.clientHeight > 200\n}\n\nfunction scrollableAncestor(element: HTMLElement): HTMLElement | null {\n let node: HTMLElement | null = element\n while (node !== null) {\n if (isScrollable(node)) return node\n node = node.parentElement\n }\n return null\n}\n\n/**\n * The rendered row of `turn` (native anchor attribute `data-chat-turn`),\n * skipping hidden rows like the native `anchorElement`. Scoped to the chat\n * container when it is already located (native scopes to its own list), so a\n * stray second mount cannot hijack the anchor. Null when the turn's events\n * are not in the loaded window yet.\n */\nexport function findTurnRow(doc: Document, turn: number | undefined): HTMLElement | null {\n if (turn === undefined || !Number.isSafeInteger(turn) || turn < 0) return null\n const scope: ParentNode = findChatContainer(doc) ?? doc\n return scope.querySelector<HTMLElement>(`[data-chat-turn=\"${turn}\"]:not([hidden])`)\n}\n\n/** Native `scrollerOf`: the conversation scrollport owning the row. */\nfunction scrollerOfRow(doc: Document, row: HTMLElement): HTMLElement | null {\n return row.closest<HTMLElement>('[data-conversation-scroll]') ?? findChatContainer(doc)\n}\n\n/** Row position in scrollport coordinates (native `flowTop`). */\nfunction flowTop(row: HTMLElement, scroller: HTMLElement): number {\n return row.getBoundingClientRect().top - scroller.getBoundingClientRect().top\n}\n\n/**\n * Native `landOnRow`: instant scroll so the row's top sits 24px below the\n * scrollport top. Returns false when the scrollport cannot be resolved.\n */\nexport function landOnRow(doc: Document, row: HTMLElement): boolean {\n const el = scrollerOfRow(doc, row)\n if (el === null) return false\n el.scrollTop += flowTop(row, el) - 24\n return true\n}\n\n/**\n * Native `turnAtLine`: the turn owning the row at a scrollport line.\n * Hit-tests the line first (elementsFromPoint at the container's horizontal\n * center) and falls back to one row scan when layout cannot answer (jsdom,\n * pre-paint). Returns null when no loaded row covers the line.\n */\nexport function turnAtLine(doc: Document, container: HTMLElement, line: number): number | null {\n const content = container.getBoundingClientRect()\n if (typeof doc.elementsFromPoint === 'function' && content.width > 0) {\n for (const element of doc.elementsFromPoint(content.left + content.width / 2, line)) {\n const row = element instanceof HTMLElement ? element.closest<HTMLElement>('[data-chat-turn]') : null\n const turn = Number(row?.dataset.chatTurn)\n if (row !== null && container.contains(row) && Number.isSafeInteger(turn)) return turn\n }\n }\n let found: number | null = null\n for (const row of Array.from(container.querySelectorAll<HTMLElement>('[data-chat-turn]'))) {\n if (row.getBoundingClientRect().top > line) break\n const turn = Number(row.dataset.chatTurn)\n if (Number.isSafeInteger(turn)) found = turn\n }\n return found\n}\n\n/**\n * The active index (native `syncActiveTurn`): within 25px of the scroll\n * bottom the last tick wins; otherwise the reading line sits at\n * `top + min(96, height * 0.2)` and the active tick is the last one whose\n * turn does not exceed the turn read at that line. When the line covers no\n * loaded row, earlier ticks outside the window count as above the viewport\n * (the window head's predecessor is active, matching 0.1.1 parity).\n */\nexport function computeActiveIndex(\n container: HTMLElement,\n doc: Document,\n turns: readonly (number | undefined)[],\n): number {\n if (turns.length === 0) return -1\n if (container.scrollHeight - container.scrollTop - container.clientHeight <= 25) {\n return turns.length - 1\n }\n const firstDefined = turns.findIndex((turn) => turn !== undefined)\n if (firstDefined === -1) return -1\n const reading = container.getBoundingClientRect().top + Math.min(96, container.clientHeight * 0.2)\n const readTurn = turnAtLine(doc, container, reading)\n let active = -1\n if (readTurn !== null) {\n for (let index = 0; index < turns.length; index += 1) {\n const turn = turns[index]\n if (turn === undefined || turn > readTurn) continue\n active = index\n }\n }\n if (active !== -1) return active\n return firstDefined > 0 ? firstDefined - 1 : 0\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/** Landing poll budget after `loadThrough` (native settles via its effects; 5s here). */\nconst LAND_POLL_ATTEMPTS = 20\nconst LAND_POLL_INTERVAL_MS = 250\n\n/**\n * 跳到第 index 个刻度的完整落点流程(原生 navigateToTurn 的插件侧等价物):\n * 1. 按 `data-chat-turn` 找行并原生落位(瞬时、行顶距视口顶 24px);\n * 2. 行未挂载时先 `loadThrough(turn/start 的 seq)` 把事件窗口翻页到位;\n * 3. 轮询等行渲染后落位。任何一步成功即返回 true。\n */\nexport async function jumpToTurn(\n doc: Document,\n index: number,\n turns: readonly (number | undefined)[],\n seqs: readonly number[],\n loadThrough: ((seq: number) => Promise<void>) | undefined,\n): Promise<boolean> {\n const turn = turns[index]\n if (turn === undefined) return false\n if (landOnRowByTurn(doc, turn)) return true\n const seq = seqs[index]\n if (seq !== undefined && loadThrough !== undefined) {\n try {\n await loadThrough(seq)\n } catch {\n // 翻页失败仍然轮询(窗口可能已部分就位)。\n }\n }\n for (let attempt = 0; attempt < LAND_POLL_ATTEMPTS; attempt += 1) {\n await sleep(LAND_POLL_INTERVAL_MS)\n if (landOnRowByTurn(doc, turn)) return true\n }\n return false\n}\n\nfunction landOnRowByTurn(doc: Document, turn: number): boolean {\n const row = findTurnRow(doc, turn)\n if (row === null) return false\n return landOnRow(doc, row)\n}\n\n/** Create the probe against a document (injectable for tests). */\nexport function probeChatDom(doc: Document): ChatDomProbe {\n return {\n getContainer: () => findChatContainer(doc),\n activeIndex: (turns) => {\n const container = findChatContainer(doc)\n if (container === null) return -1\n return computeActiveIndex(container, doc, turns)\n },\n }\n}\n","/**\n * Stylesheet for the chat-timeline rail, injected once as a\n * `<style data-plugin-css=\"chat-timeline\">` tag. All classes carry the\n * `dsh-tl-` prefix; colors use the CSS system colors Canvas/CanvasText so the\n * rail follows the app's color-scheme without reaching into DSH theme tokens.\n * Visual constants replicate the ZCode TurnNavigator (spec section 2.2).\n */\nexport const TIMELINE_STYLES = `\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n /* 水平:会话列左沿,由 rail 写入 --dsh-tl-left。这是唯一保留的测量值——\n 列是 grid 轨道,右侧栏打开时会被挤压,不贴窗口右沿,无法由窗口宽度推导。 */\n left: var(--dsh-tl-left, 0px);\n /* 垂直:原生 TurnNavigator 的 band 模型(eGxaPq_frame 的 --turn-rail-band)。\n 会话可视带 = 视口高 − 输入框高;其中点在视口坐标下为\n 100dvh − (会话可视高 + 输入框高) / 2\n 两个变量由宿主写在滚动容器上,本 seat 在滚动容器内可直接继承,故整个垂直\n 几何是纯 CSS:不做测量,也不依赖原生 nav 是否存在于 DOM。 */\n --dsh-tl-band: calc(var(--dsh-conversation-viewport-height, 100dvh) - var(--dsh-composer-height, 152px));\n top: calc(100dvh - (var(--dsh-conversation-viewport-height, 100dvh) + var(--dsh-composer-height, 152px)) / 2);\n transform: translateY(-50%);\n pointer-events: none;\n opacity: 1;\n transition: opacity 150ms ease-out;\n}\n.dsh-tl-nav[data-visible=\"false\"] {\n opacity: 0;\n visibility: hidden;\n}\n.dsh-tl-scroll {\n position: relative;\n left: 12px;\n width: 36px;\n /* 原生 frame 的高度上限:band − 64px,再夹到 420px。 */\n max-height: min(max(0px, calc(var(--dsh-tl-band) - 64px)), 420px);\n overflow-x: hidden;\n overflow-y: auto;\n padding-block: 4px;\n pointer-events: auto;\n scrollbar-width: none;\n color: CanvasText;\n}\n.dsh-tl-scroll::-webkit-scrollbar {\n display: none;\n}\n.dsh-tl-track {\n position: relative;\n width: 36px;\n}\n.dsh-tl-slot {\n position: absolute;\n left: 0;\n top: 0;\n height: 10px;\n width: 36px;\n padding: 0;\n border: 0;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n border-radius: 2px;\n cursor: pointer;\n}\n.dsh-tl-slot:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n}\n.dsh-tl-tick {\n display: block;\n height: 2px;\n width: 12px;\n border-radius: 999px;\n background: currentColor;\n transform-origin: left center;\n transition: height 150ms ease-out, opacity 150ms ease-out, transform 150ms ease-out, background-color 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .dsh-tl-nav,\n .dsh-tl-tick {\n transition: none;\n }\n}\n.dsh-tl-tip {\n position: fixed;\n z-index: 60;\n width: 320px;\n max-width: calc(100vw - 2rem);\n padding: 12px;\n border-radius: 10px;\n /* 原生 tooltip 配对(dsh-web-frontend _bubble_):灰底 + 静态白字,\n 主题切换由别名变量跟随;灰底与页面背景天然分层。 */\n background: var(--dsw-alias-tooltip-bg, #43454a);\n color: var(--dsw-static-neutral-bluish-00, #fff);\n border: none;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n}\n.dsh-tl-tip-user {\n margin: 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n}\n.dsh-tl-tip-assistant {\n margin: 8px 0 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-static-neutral-bluish-300, rgba(255, 255, 255, 0.8));\n}\n.dsh-tl-tip-assistant[data-kind=\"running\"],\n.dsh-tl-tip-assistant[data-kind=\"empty\"] {\n color: var(--dsw-static-neutral-bluish-400, rgba(255, 255, 255, 0.55));\n}\n/* 插件完全接管原生右侧 Turn 导航条:本样式表只随插件存在,插件卸载时\n removeStyles 移除标签,原生导航条立即恢复。aria-label 跟随宿主语言\n (英文 Turn navigation / 中文 轮次导航),两个都要盖住。 */\nnav[aria-label='Turn navigation'],\nnav[aria-label='轮次导航'] {\n display: none !important;\n}\n`\n\n/** Inject the stylesheet once per document. */\nexport function ensureStyles(doc: Document): void {\n const tagId = 'chat-timeline'\n if (doc.querySelector('style[data-plugin-css=\"chat-timeline\"]') !== null) return\n const tag = doc.createElement('style')\n tag.dataset.plugin = '@lament_z/dsh-client-ui-chat-timeline'\n tag.dataset.pluginCss = tagId\n tag.textContent = TIMELINE_STYLES\n doc.head.appendChild(tag)\n}\n\n/** Remove the stylesheet (plugin teardown): restores the native turn navigator. */\nexport function removeStyles(doc: Document): void {\n doc.querySelector('style[data-plugin-css=\"chat-timeline\"]')?.remove()\n}\n","/**\n * TimelineRail — the conversation.input.overlay entry: a left-edge tick rail\n * over the conversation, one tick per human question. Visual and interaction\n * constants replicate the ZCode TurnNavigator (see .scratch/chat-timeline/spec.md):\n * hover ripple scaleX 2.6/1.7/1.25 with opacity 1/.86/.72/.58 over 150ms,\n * 320px preview cards after a 120ms delay, prefers-reduced-motion fallback,\n * full aria labelling.\n *\n * Geometry follows the native TurnNavigator model: the vertical band\n * (--dsh-tl-band, the host's viewport-minus-composer rule) is computed in CSS\n * from the host variables that the seat inherits, so no JS measures it. Only\n * the horizontal left edge is measured — the conversation column is a grid\n * track, so it is not flush to the window edge once the right sidebar opens.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { type TimelineItem } from './directory.ts'\nimport type { TimelineKey } from './locales.ts'\nimport { jumpToTurn, probeChatDom, type ChatDomProbe } from './dom.ts'\nimport { ensureStyles, removeStyles } from './styles.ts'\nimport type { TimelineSource } from './source.ts'\n\n/** Component props: the locale seat, the seat's session id, and the source. */\nexport type TimelineRailProps = PropsLocale<'chat-timeline'> & {\n /** The session this rail belongs to (host-supplied by the seat's inject). */\n sessionId: string\n /** That session's snapshot source (built from ctx.sessions). */\n source: TimelineSource\n}\n\n/** Slot height per tick (10px pitch, ZCode parity). */\nconst ITEM_PITCH = 10\n/** Preview-card hover delays (ms). */\nconst TIP_OPEN_DELAY = 120\nconst TIP_CLOSE_DELAY = 80\n\n/** Ripple visual per distance from the hovered tick (ZCode _5e parity).\n * The ripple is interaction-only: at rest every tick is idle (scaleX 1),\n * and the current position is signalled by color/opacity, not length. */\nfunction ripple(distance: number): { opacity: number; scaleX: number; tone: 'peak' | 'near' | 'mid' | 'idle' } {\n if (distance === 0) return { opacity: 1, scaleX: 2.6, tone: 'peak' }\n if (distance === 1) return { opacity: 0.86, scaleX: 1.7, tone: 'near' }\n if (distance === 2) return { opacity: 0.72, scaleX: 1.25, tone: 'mid' }\n return { opacity: 0.58, scaleX: 1, tone: 'idle' }\n}\n\n/** Tick colors: foreground for the hovered peak and the current turn at rest,\n * the subtlest text tone otherwise (ZCode bg-foreground / -subtlest parity\n * through system colors). */\nconst TICK_COLOR_FOREGROUND = 'CanvasText'\nconst TICK_COLOR_SUBTLE = 'color-mix(in srgb, CanvasText 42%, transparent)'\n\n/**\n * Render the question navigator rail.\n * @param props - composed slot props.\n * @returns the rail, or null when it should not render.\n */\nexport function TimelineRail({ sessionId, source, t }: TimelineRailProps) {\n const state = useSyncExternalStore(source.subscribe, source.getSnapshot)\n const probe = useMemo<ChatDomProbe | null>(() => (typeof document === 'undefined' ? null : probeChatDom(document)), [])\n const [container, setContainer] = useState<HTMLElement | null>(null)\n const [navEl, setNavEl] = useState<HTMLElement | null>(null)\n const [activeIndex, setActiveIndex] = useState(-1)\n const [hoverIndex, setHoverIndex] = useState<number | undefined>(undefined)\n const [tipIndex, setTipIndex] = useState<number | undefined>(undefined)\n const timers = useRef<{ open: number | undefined; close: number | undefined }>({ open: undefined, close: undefined })\n const frame = useRef(0)\n\n const snapshot = state.snapshot\n // 官方 turnOutline 数据面:source 直接产出刻度(宿主已算好全史预览)。\n const items = useMemo<TimelineItem[]>(\n () => (snapshot === null ? [] : (snapshot.items as TimelineItem[])),\n [snapshot],\n )\n const sourceRef = useRef(source)\n sourceRef.current = source\n const itemsRef = useRef<TimelineItem[]>([])\n itemsRef.current = items\n // 原生锚点:行按 `data-chat-turn=\"<turn>\"` 键控(dsh-client-ui-chat ChatView),\n // 跳转与高亮都按轮次号走原生逻辑;seq 仅用于 loadThrough 翻页。\n const seqs = useMemo(() => items.map((item) => item.seq), [items])\n const seqsRef = useRef<number[]>([])\n seqsRef.current = seqs\n const turns = useMemo(() => items.map((item) => item.turn), [items])\n const turnsRef = useRef<(number | undefined)[]>([])\n turnsRef.current = turns\n\n useEffect(() => {\n if (typeof document === 'undefined') return\n ensureStyles(document)\n // 卸载时移除样式表:隐藏原生 Turn 导航的规则随标签消失,原生立即恢复。\n return () => removeStyles(document)\n }, [])\n\n // Locate the chat scrollport. The seat renders as soon as the session scope\n // exists, which can be a beat before the conversation itself mounts.\n useEffect(() => {\n if (probe === null) return\n let attempts = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n const tick = () => {\n const found = probe.getContainer()\n if (found !== null || attempts >= 10) {\n setContainer(found)\n return\n }\n attempts += 1\n timer = setTimeout(tick, 300)\n }\n tick()\n return () => {\n if (timer !== undefined) clearTimeout(timer)\n }\n }, [probe, sessionId])\n\n // Horizontal geometry only: the rail's left edge, which is the conversation\n // column's left edge. Vertical geometry is pure CSS (styles.ts) because the\n // band variables live on the scrollport and the seat inherits them; the\n // column stays measured because it is a grid track that the right sidebar\n // shrinks, so it is not derivable from a window-edge offset.\n useEffect(() => {\n if (container === null || navEl === null) return\n const write = () => {\n navEl.style.setProperty('--dsh-tl-left', `${container.getBoundingClientRect().left}px`)\n }\n write()\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(write)\n observer?.observe(container)\n window.addEventListener('resize', write)\n return () => {\n observer?.disconnect()\n window.removeEventListener('resize', write)\n }\n }, [container, navEl])\n\n // Active-tick highlight follows the conversation's scrolling.\n useEffect(() => {\n if (container === null || probe === null) return\n const sync = () => {\n if (frame.current !== 0) return\n frame.current = requestAnimationFrame(() => {\n frame.current = 0\n setActiveIndex(probe.activeIndex(turnsRef.current))\n })\n }\n sync()\n container.addEventListener('scroll', sync, { passive: true })\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(sync)\n observer?.observe(container)\n window.addEventListener('resize', sync)\n return () => {\n container.removeEventListener('scroll', sync)\n observer?.disconnect()\n window.removeEventListener('resize', sync)\n if (frame.current !== 0) cancelAnimationFrame(frame.current)\n frame.current = 0\n }\n }, [container, probe])\n\n // items 变化(事件窗口增长/切换会话)后重算一次 active 高亮。\n useEffect(() => {\n if (container === null || probe === null) return\n setActiveIndex(probe.activeIndex(turns))\n }, [container, probe, turns])\n\n // 完全模仿原生:只要会话可读就渲染——不设条数/宽度门槛,原生导航条的隐藏\n // 完全交给本插件的接管规则。\n const visible = snapshot !== null\n // The ripple follows the pointer only (ZCode v5e parity): at rest every tick\n // is equal length and the current turn stands out by color/opacity alone.\n const focusIndex = hoverIndex\n\n const scheduleTip = useCallback((index: number | undefined) => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n if (index === undefined) {\n timers.current.close = window.setTimeout(() => setTipIndex(undefined), TIP_CLOSE_DELAY)\n return\n }\n timers.current.open = window.setTimeout(() => setTipIndex(index), TIP_OPEN_DELAY)\n }, [])\n\n useEffect(() => () => {\n if (timers.current.open !== undefined) window.clearTimeout(timers.current.open)\n if (timers.current.close !== undefined) window.clearTimeout(timers.current.close)\n }, [])\n\n const jump = useCallback((index: number) => {\n // 原生 navigateToTurn 流程:行已挂载直接落位;未挂载走官方 loadThrough\n // 翻页后落位。落位是瞬时滚动(原生 landOnRow 语义),无需 motion 分支。\n void jumpToTurn(\n document,\n index,\n turnsRef.current,\n seqsRef.current,\n (seq: number) => sourceRef.current?.jumpThrough(seq) ?? Promise.resolve(),\n )\n }, [])\n\n if (!visible || probe === null) return null\n\n const trackHeight = items.length * ITEM_PITCH\n const tip = tipIndex !== undefined ? items[tipIndex] : undefined\n\n return (\n <>\n <nav\n ref={setNavEl}\n aria-label={t('nav.label')}\n className=\"dsh-tl-nav\"\n data-visible={visible ? 'true' : 'false'}\n data-testid=\"dsh-chat-timeline\"\n data-item-count={items.length}\n >\n <div\n className=\"dsh-tl-scroll\"\n onPointerLeave={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <div className=\"dsh-tl-track\" style={{ height: `${trackHeight}px` }}>\n {items.map((item, index) => {\n const isActive = index === activeIndex\n const distance = focusIndex === undefined ? 3 : Math.abs(index - focusIndex)\n const look = ripple(distance)\n const activeAtRest = focusIndex === undefined && isActive\n const foreground = look.tone === 'peak' || activeAtRest\n const opacity = activeAtRest\n ? 0.9\n : item.running\n ? Math.max(look.opacity, 0.72)\n : look.opacity\n return (\n <div\n key={item.key}\n className=\"dsh-tl-slot\"\n style={{ transform: `translateY(${index * ITEM_PITCH}px)` }}\n onMouseEnter={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onFocus={() => {\n setHoverIndex(index)\n scheduleTip(index)\n }}\n onBlur={() => {\n setHoverIndex(undefined)\n scheduleTip(undefined)\n }}\n >\n <button\n type=\"button\"\n aria-current={isActive ? 'location' : undefined}\n aria-label={t('nav.jumpToQuery', { index: String(index + 1) })}\n aria-posinset={index + 1}\n aria-setsize={items.length}\n data-testid=\"dsh-chat-timeline-item\"\n data-item-index={index}\n data-active={isActive ? 'true' : 'false'}\n data-running={item.running ? 'true' : 'false'}\n onClick={() => jump(index)}\n className=\"dsh-tl-slot-inner\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-start',\n width: '100%',\n height: '100%',\n padding: 0,\n border: 0,\n background: 'transparent',\n cursor: 'pointer',\n }}\n >\n <span\n className=\"dsh-tl-tick\"\n style={{\n opacity,\n transform: `scaleX(${look.scaleX})`,\n backgroundColor: foreground ? TICK_COLOR_FOREGROUND : TICK_COLOR_SUBTLE,\n }}\n />\n </button>\n </div>\n )\n })}\n </div>\n </div>\n </nav>\n {tip !== undefined && tipIndex !== undefined\n ? createPortal(\n <TipCard item={tip} index={tipIndex} nav={navEl} t={t} />,\n document.body,\n )\n : null}\n </>\n )\n}\n\nfunction TipCard({ item, index, nav, t }: {\n item: TimelineItem\n index: number\n nav: HTMLElement | null\n t: (key: TimelineKey, params?: Record<string, string | number>) => string\n}) {\n const slot = nav?.querySelector<HTMLElement>('[data-testid=\"dsh-chat-timeline-item\"][data-item-index=\"' + String(index) + '\"]')\n const box = slot?.getBoundingClientRect()\n const railBox = nav?.getBoundingClientRect()\n const left = box !== undefined\n ? Math.min(box.right + 8, window.innerWidth - 336)\n : (railBox !== undefined ? railBox.right + 8 : 8)\n const top = box !== undefined ? Math.max(8, Math.min(box.top - 4, window.innerHeight - 160)) : 8\n return (\n <div\n className=\"dsh-tl-tip\"\n data-testid=\"dsh-chat-timeline-tip\"\n style={{ left, top }}\n role=\"tooltip\"\n >\n <p className=\"dsh-tl-tip-user\">{item.userFallback ? t('preview.userFallback') : item.userPreview}</p>\n <p className=\"dsh-tl-tip-assistant\" data-kind={item.assistantKind}>\n {item.assistantKind === 'running'\n ? t('preview.assistantRunning')\n : item.assistantKind === 'empty'\n ? t('preview.assistantEmpty')\n : item.assistantPreview}\n </p>\n </div>\n )\n}\n","/**\n * Timeline source: follows the current session through the cordis services\n * face (ctx.sessions) and exposes a useSyncExternalStore-compatible read\n * face over the host-computed turn outline. Client context usage here is\n * type-only; at runtime everything is reached through the ctx services, so\n * the client bundle stays free of cross-package value imports.\n */\n// DSH-0.1.2-A1-25: dsh-client-runtime 包已删除,ClientContext 即 cordis Context。\n// ctx.sessions 的 Context 合并自 session-controller 的 client 入口(A1-25 迁移表)。\nimport type {} from '@deepseek-ai/dsh-api-session-controller/client'\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS, type TimelineItem } from './directory.ts'\n\n/**\n * The slice of the turn outline the timeline consumes — 官方 `turnOutline`\n * 投影(session-turn-outline 域)的 wire 条目:宿主对整份日志 fold 出的\n * 全史轮次索引,无需客户端翻页。\n */\nexport interface TurnOutlineEntryLike {\n readonly turn: number\n readonly seq: number\n readonly prompt?: string\n readonly response?: string\n}\n\n/** The slice of ConversationSnapshot the timeline consumes. */\nexport interface TimelineSnapshot {\n readonly sessionId: string\n /** One tick per known turn, ascending(官方 outline 顺序)。 */\n readonly items: readonly TimelineItem[]\n /** Whether the session currently has a running turn. */\n readonly running: boolean\n}\n\n/** State handed to React: null snapshot means \"no readable session right now\". */\nexport interface TimelineState {\n readonly sessionId: string | undefined\n readonly snapshot: TimelineSnapshot | null\n}\n\n/** useSyncExternalStore-compatible source face. */\nexport interface TimelineSource {\n subscribe(listener: () => void): () => void\n getSnapshot(): TimelineState\n /**\n * Page the history window back through the given turn's seq(官方\n * `loadThrough` turn-jump loader),so the turn's rows mount and the DOM\n * probe can scroll to them. No-op when the session face lacks the verb.\n */\n jumpThrough(seq: number): Promise<void>\n}\n\ninterface ObservableLike<T> {\n getSnapshot(): T\n subscribe(listener: () => void): () => void\n}\n\n/**\n * Structural face of ctx.sessions this module needs (kept narrow for tests).\n * 0.1.7: the session id is handed in by the host (session-scoped seat inject),\n * `binding(id)` borrows the already-retained scope, `projections.faceOf\n * ('turnOutline')` is the official whole-log turn index, and `loadThrough(seq)`\n * is the turn-jump pagination verb.\n */\nexport interface SessionsLike {\n binding(\n id: string,\n ): {\n session: ObservableLike<{ running?: boolean }> & {\n loadThrough?(seq: number): Promise<void>\n projections?: {\n faceOf?(key: string): ObservableSnapshotLike<unknown>\n }\n }\n eventSource: ObservableLike<unknown>\n } | undefined\n}\n\n/** ObservableSnapshot 的最小结构面(getSnapshot/subscribe)。 */\ntype ObservableSnapshotLike<T> = ObservableLike<T>\n\n/** Project one outline entry onto a tick (previews are host-bounded already). */\nexport function itemFromOutline(\n entry: TurnOutlineEntryLike,\n isLive: boolean,\n labels: { userFallback: string; assistantRunning: string; assistantEmpty: string },\n): TimelineItem {\n const prompt = typeof entry.prompt === 'string' ? entry.prompt : ''\n const response = typeof entry.response === 'string' ? entry.response : ''\n const assistantKind: TimelineItem['assistantKind'] = response\n ? 'text'\n : isLive\n ? 'running'\n : 'empty'\n return {\n key: `t${entry.turn}`,\n seq: entry.seq,\n time: 0,\n turn: entry.turn,\n userPreview: prompt || labels.userFallback,\n userFallback: !prompt,\n assistantPreview: response || (isLive ? labels.assistantRunning : labels.assistantEmpty),\n assistantKind,\n running: isLive,\n }\n}\n\n/** Build the tick list from the outline plus the live running flag. */\nexport function itemsFromOutline(\n outline: readonly TurnOutlineEntryLike[],\n running: boolean,\n labels: { userFallback: string; assistantRunning: string; assistantEmpty: string },\n): TimelineItem[] {\n const last = outline.length - 1\n return outline.map((entry, index) =>\n itemFromOutline(entry, running && index === last, labels),\n )\n}\n\n// 保留目录数学的导出面(rail 不再走 nodes 路径,preview 预算常量仍被引用)。\nexport { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS }\n\n/** Fallback preview labels when the outline entry carries no text. */\nconst PREVIEW_LABELS = {\n userFallback: '(无文本)',\n assistantRunning: '(运行中)',\n assistantEmpty: '(空)',\n} as const\n\n/**\n * Create the source for one session. The id is supplied by the host — the\n * session-scoped seat hands it to `inject` — so this no longer guesses which\n * session is open and no longer watches the sessions list: it binds straight\n * to that session's outline projection and republishes whenever the outline or\n * the running flag moves.\n *\n * A bounded retry covers the materialization window (the seat can render a\n * beat before `binding(id)` answers); after that the rail stays hidden.\n * @param sessions - the sessions service face (pass ctx.sessions).\n * @param sessionId - the session this rail belongs to.\n * @returns the source face.\n */\nexport function createTimelineSource(sessions: SessionsLike, sessionId: string): TimelineSource {\n let state: TimelineState = { sessionId, snapshot: null }\n const listeners = new Set<() => void>()\n let subscribed = false\n let retryTimer: ReturnType<typeof setTimeout> | undefined\n let retryAttempts = 0\n\n const emit = () => {\n for (const listener of listeners) listener()\n }\n\n const readBinding = () => {\n try {\n return sessions.binding(sessionId)\n } catch {\n return undefined\n }\n }\n\n const bind = () => {\n if (retryTimer !== undefined) {\n clearTimeout(retryTimer)\n retryTimer = undefined\n }\n const session = readBinding()?.session\n const outlineFace = (() => {\n try {\n return session?.projections?.faceOf?.('turnOutline') as\n | ObservableLike<readonly TurnOutlineEntryLike[]>\n | undefined\n } catch {\n return undefined\n }\n })()\n if (!session || !outlineFace) {\n // Scope still materializing: keep an empty state and retry with backoff\n // so the rail appears once the binding lands.\n state = { sessionId, snapshot: null }\n emit()\n if (subscribed && retryAttempts < 10) {\n retryAttempts += 1\n retryTimer = setTimeout(bind, 250 * retryAttempts)\n }\n return\n }\n retryAttempts = 0\n const push = () => {\n try {\n const outline = outlineFace.getSnapshot()\n const running = Boolean(session.getSnapshot()?.running)\n state = {\n sessionId,\n snapshot: {\n sessionId,\n items: Array.isArray(outline)\n ? itemsFromOutline(outline, running, PREVIEW_LABELS)\n : [],\n running,\n },\n }\n } catch {\n state = { sessionId, snapshot: null }\n }\n emit()\n }\n push()\n try {\n outlineFace.subscribe(push)\n session.subscribe(push)\n } catch {\n // Observable faces expose no unsubscribe disposer contract here; the\n // subscription rides the plugin fiber and is torn down with it.\n }\n }\n\n return {\n subscribe(listener: () => void): () => void {\n const first = listeners.size === 0\n listeners.add(listener)\n if (first) {\n subscribed = true\n bind()\n }\n return () => {\n listeners.delete(listener)\n }\n },\n getSnapshot(): TimelineState {\n return state\n },\n jumpThrough(seq: number): Promise<void> {\n const session = readBinding()?.session\n if (typeof session?.loadThrough !== 'function') return Promise.resolve()\n return session.loadThrough(seq).catch(() => {})\n },\n }\n}\n\n/** Build the source from the plugin client context and the seat's session id. */\nexport function timelineSourceFromContext(ctx: ClientContext, sessionId: string): TimelineSource {\n return createTimelineSource(ctx.sessions as unknown as SessionsLike, sessionId)\n}\n","/**\n * Locale dictionaries for the chat-timeline plugin. `zh` is the key-set source\n * of truth; `en` keeps a full key-for-key mirror. Registered through\n * ctx.locale.register(NS, { zh, en }).\n */\n\n/** Simplified Chinese dictionary (key-set source of truth). */\nexport const zh = {\n 'nav.label': '对话问题导航',\n 'nav.jumpToQuery': '跳转到第 {index} 条问题',\n 'preview.userFallback': '用户输入',\n 'preview.assistantEmpty': '暂无助手正文',\n 'preview.assistantRunning': '助手仍在工作',\n}\n\n/** The chat-timeline namespace key union. */\nexport type TimelineKey = keyof typeof zh\n\n/** English dictionary, key-for-key complete against zh. */\nexport const en: Record<TimelineKey, string> = {\n 'nav.label': 'Conversation question navigator',\n 'nav.jumpToQuery': 'Jump to question {index}',\n 'preview.userFallback': 'User input',\n 'preview.assistantEmpty': 'No assistant text yet',\n 'preview.assistantRunning': 'Assistant is still working',\n}\n","/**\n * Chat-timeline plugin — browser half. Registers the `chat-timeline` locale\n * dictionaries and a `shell.overlay` entry that renders the question\n * navigator rail beside the conversation. Export discipline: the /client\n * surface carries only what cordis loading needs plus types.\n */\n// DSH-0.1.2-A1-25: dsh-client-runtime 包已删除,ClientContext 即 cordis Context。\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its\n// LocaleNamespaceMap merge table.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ui-layout SlotMap merge (the 'shell.overlay' hole).\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\n// Type-only: pulls the ui-conversation SlotMap merge (the\n// 'conversation.input.overlay' hole the rail registers into).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// DSH-0.1.5-A1-14/A2-13: ctx.slots 的 Context 合并自 ui-renderer 的 client 入口。\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport { TimelineRail } from './rail.tsx'\nimport { timelineSourceFromContext } from './source.ts'\nimport { en, zh, type TimelineKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Chat-timeline surface copy. */\n 'chat-timeline': TimelineKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'chat-timeline'\n\n/**\n * Session-scoped seat the rail occupies.\n *\n * DSH 0.1.7 moved \"which session is open\" out of the sessions service: the list\n * snapshot no longer carries `current`, `binding(id)` only borrows an already\n * retained scope, and session-controller states outright that view selection\n * stays outside the Controller. The only supported way to learn the open\n * session is a session-scoped seat, whose `inject` receives the scope\n * binding's key — the session id. A root-scope seat (shell.overlay) cannot be\n * told, which is why the rail no longer lives there.\n *\n * `conversation.input.overlay` is rendered for every open session, and unlike\n * `conversation.view` it is not enumerated into the conversation's view tabs.\n * It also sits inside the conversation scroller, so the host's geometry\n * variables (--dsh-conversation-viewport-height / --dsh-composer-height)\n * inherit to the rail.\n */\nconst SEAT = 'conversation.input.overlay'\n\n/** Unique occupant id inside the shared conversation.input.overlay list slot. */\nconst ENTRY_ID = 'chat-timeline'\n\n/** Services required by this plugin. */\nexport const inject = ['slots', 'locale', 'sessions']\n\n/**\n * Register the timeline surface.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => {\n try {\n return ctx.locale.register(NS, { zh, en })\n } catch {\n return () => {}\n }\n }, 'chat-timeline: dictionaries')\n\n // The rail is a frame-wide overlay, but it is registered on a session-scoped\n // seat: that is the only channel through which 0.1.7 hands out the open\n // session id. Registration is declaration-aware via slots.inject. 注册失败\n // 降级为一条 console 错误(不抛进 loader,也不静默——留诊断线索)。\n ctx.slots.inject(SEAT, () => {\n try {\n return ctx.slots.register({\n name: SEAT,\n id: ENTRY_ID,\n order: 0,\n locale: NS,\n // Session-scoped registrations receive the scope binding's key, i.e.\n // the session id (renderer runInject pushes binding.key).\n inject: (sessionId: string) => ({\n sessionId,\n source: timelineSourceFromContext(ctx, sessionId),\n }),\n }, TimelineRail)\n } catch (err) {\n console.error(`chat-timeline: ${SEAT} register failed:`, err)\n return () => {}\n }\n })\n}\n\nexport type { TimelineRailProps } from './rail.tsx'\nexport type { TimelineKey } from './locales.ts'\nexport type { TimelineSource, TimelineSnapshot, TimelineState } from './source.ts'\n"],"mappings":"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"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
2
|
import type { TimelineSource } from './source.ts';
|
|
3
|
-
/** Component props: the locale seat
|
|
3
|
+
/** Component props: the locale seat, the seat's session id, and the source. */
|
|
4
4
|
export type TimelineRailProps = PropsLocale<'chat-timeline'> & {
|
|
5
|
-
/** The
|
|
5
|
+
/** The session this rail belongs to (host-supplied by the seat's inject). */
|
|
6
|
+
sessionId: string;
|
|
7
|
+
/** That session's snapshot source (built from ctx.sessions). */
|
|
6
8
|
source: TimelineSource;
|
|
7
9
|
};
|
|
8
10
|
/**
|
|
@@ -10,4 +12,4 @@ export type TimelineRailProps = PropsLocale<'chat-timeline'> & {
|
|
|
10
12
|
* @param props - composed slot props.
|
|
11
13
|
* @returns the rail, or null when it should not render.
|
|
12
14
|
*/
|
|
13
|
-
export declare function TimelineRail({ source, t }: TimelineRailProps): import("react").JSX.Element | null;
|
|
15
|
+
export declare function TimelineRail({ sessionId, source, t }: TimelineRailProps): import("react").JSX.Element | null;
|
|
@@ -41,13 +41,12 @@ interface ObservableLike<T> {
|
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Structural face of ctx.sessions this module needs (kept narrow for tests).
|
|
44
|
-
* 0.1.
|
|
45
|
-
*
|
|
44
|
+
* 0.1.7: the session id is handed in by the host (session-scoped seat inject),
|
|
45
|
+
* `binding(id)` borrows the already-retained scope, `projections.faceOf
|
|
46
|
+
* ('turnOutline')` is the official whole-log turn index, and `loadThrough(seq)`
|
|
47
|
+
* is the turn-jump pagination verb.
|
|
46
48
|
*/
|
|
47
49
|
export interface SessionsLike {
|
|
48
|
-
list: ObservableLike<{
|
|
49
|
-
current?: string;
|
|
50
|
-
}>;
|
|
51
50
|
binding(id: string): {
|
|
52
51
|
session: ObservableLike<{
|
|
53
52
|
running?: boolean;
|
|
@@ -76,13 +75,18 @@ export declare function itemsFromOutline(outline: readonly TurnOutlineEntryLike[
|
|
|
76
75
|
}): TimelineItem[];
|
|
77
76
|
export { buildPreview, MAX_PREVIEW_CHARS, MAX_PREVIEW_PARAGRAPHS };
|
|
78
77
|
/**
|
|
79
|
-
* Create the source.
|
|
80
|
-
* session
|
|
81
|
-
* is
|
|
82
|
-
*
|
|
78
|
+
* Create the source for one session. The id is supplied by the host — the
|
|
79
|
+
* session-scoped seat hands it to `inject` — so this no longer guesses which
|
|
80
|
+
* session is open and no longer watches the sessions list: it binds straight
|
|
81
|
+
* to that session's outline projection and republishes whenever the outline or
|
|
82
|
+
* the running flag moves.
|
|
83
|
+
*
|
|
84
|
+
* A bounded retry covers the materialization window (the seat can render a
|
|
85
|
+
* beat before `binding(id)` answers); after that the rail stays hidden.
|
|
83
86
|
* @param sessions - the sessions service face (pass ctx.sessions).
|
|
87
|
+
* @param sessionId - the session this rail belongs to.
|
|
84
88
|
* @returns the source face.
|
|
85
89
|
*/
|
|
86
|
-
export declare function createTimelineSource(sessions: SessionsLike): TimelineSource;
|
|
87
|
-
/** Build the source from the plugin client context. */
|
|
88
|
-
export declare function timelineSourceFromContext(ctx: ClientContext): TimelineSource;
|
|
90
|
+
export declare function createTimelineSource(sessions: SessionsLike, sessionId: string): TimelineSource;
|
|
91
|
+
/** Build the source from the plugin client context and the seat's session id. */
|
|
92
|
+
export declare function timelineSourceFromContext(ctx: ClientContext, sessionId: string): TimelineSource;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* rail follows the app's color-scheme without reaching into DSH theme tokens.
|
|
6
6
|
* Visual constants replicate the ZCode TurnNavigator (spec section 2.2).
|
|
7
7
|
*/
|
|
8
|
-
export declare const TIMELINE_STYLES = "\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n
|
|
8
|
+
export declare const TIMELINE_STYLES = "\n.dsh-tl-nav {\n position: fixed;\n z-index: 10;\n width: 48px;\n /* \u6C34\u5E73\uFF1A\u4F1A\u8BDD\u5217\u5DE6\u6CBF\uFF0C\u7531 rail \u5199\u5165 --dsh-tl-left\u3002\u8FD9\u662F\u552F\u4E00\u4FDD\u7559\u7684\u6D4B\u91CF\u503C\u2014\u2014\n \u5217\u662F grid \u8F68\u9053\uFF0C\u53F3\u4FA7\u680F\u6253\u5F00\u65F6\u4F1A\u88AB\u6324\u538B\uFF0C\u4E0D\u8D34\u7A97\u53E3\u53F3\u6CBF\uFF0C\u65E0\u6CD5\u7531\u7A97\u53E3\u5BBD\u5EA6\u63A8\u5BFC\u3002 */\n left: var(--dsh-tl-left, 0px);\n /* \u5782\u76F4\uFF1A\u539F\u751F TurnNavigator \u7684 band \u6A21\u578B\uFF08eGxaPq_frame \u7684 --turn-rail-band\uFF09\u3002\n \u4F1A\u8BDD\u53EF\u89C6\u5E26 = \u89C6\u53E3\u9AD8 \u2212 \u8F93\u5165\u6846\u9AD8\uFF1B\u5176\u4E2D\u70B9\u5728\u89C6\u53E3\u5750\u6807\u4E0B\u4E3A\n 100dvh \u2212 (\u4F1A\u8BDD\u53EF\u89C6\u9AD8 + \u8F93\u5165\u6846\u9AD8) / 2\n \u4E24\u4E2A\u53D8\u91CF\u7531\u5BBF\u4E3B\u5199\u5728\u6EDA\u52A8\u5BB9\u5668\u4E0A\uFF0C\u672C seat \u5728\u6EDA\u52A8\u5BB9\u5668\u5185\u53EF\u76F4\u63A5\u7EE7\u627F\uFF0C\u6545\u6574\u4E2A\u5782\u76F4\n \u51E0\u4F55\u662F\u7EAF CSS\uFF1A\u4E0D\u505A\u6D4B\u91CF\uFF0C\u4E5F\u4E0D\u4F9D\u8D56\u539F\u751F nav \u662F\u5426\u5B58\u5728\u4E8E DOM\u3002 */\n --dsh-tl-band: calc(var(--dsh-conversation-viewport-height, 100dvh) - var(--dsh-composer-height, 152px));\n top: calc(100dvh - (var(--dsh-conversation-viewport-height, 100dvh) + var(--dsh-composer-height, 152px)) / 2);\n transform: translateY(-50%);\n pointer-events: none;\n opacity: 1;\n transition: opacity 150ms ease-out;\n}\n.dsh-tl-nav[data-visible=\"false\"] {\n opacity: 0;\n visibility: hidden;\n}\n.dsh-tl-scroll {\n position: relative;\n left: 12px;\n width: 36px;\n /* \u539F\u751F frame \u7684\u9AD8\u5EA6\u4E0A\u9650\uFF1Aband \u2212 64px\uFF0C\u518D\u5939\u5230 420px\u3002 */\n max-height: min(max(0px, calc(var(--dsh-tl-band) - 64px)), 420px);\n overflow-x: hidden;\n overflow-y: auto;\n padding-block: 4px;\n pointer-events: auto;\n scrollbar-width: none;\n color: CanvasText;\n}\n.dsh-tl-scroll::-webkit-scrollbar {\n display: none;\n}\n.dsh-tl-track {\n position: relative;\n width: 36px;\n}\n.dsh-tl-slot {\n position: absolute;\n left: 0;\n top: 0;\n height: 10px;\n width: 36px;\n padding: 0;\n border: 0;\n background: transparent;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n border-radius: 2px;\n cursor: pointer;\n}\n.dsh-tl-slot:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n}\n.dsh-tl-tick {\n display: block;\n height: 2px;\n width: 12px;\n border-radius: 999px;\n background: currentColor;\n transform-origin: left center;\n transition: height 150ms ease-out, opacity 150ms ease-out, transform 150ms ease-out, background-color 150ms ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .dsh-tl-nav,\n .dsh-tl-tick {\n transition: none;\n }\n}\n.dsh-tl-tip {\n position: fixed;\n z-index: 60;\n width: 320px;\n max-width: calc(100vw - 2rem);\n padding: 12px;\n border-radius: 10px;\n /* \u539F\u751F tooltip \u914D\u5BF9\uFF08dsh-web-frontend _bubble_\uFF09\uFF1A\u7070\u5E95 + \u9759\u6001\u767D\u5B57\uFF0C\n \u4E3B\u9898\u5207\u6362\u7531\u522B\u540D\u53D8\u91CF\u8DDF\u968F\uFF1B\u7070\u5E95\u4E0E\u9875\u9762\u80CC\u666F\u5929\u7136\u5206\u5C42\u3002 */\n background: var(--dsw-alias-tooltip-bg, #43454a);\n color: var(--dsw-static-neutral-bluish-00, #fff);\n border: none;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n pointer-events: none;\n}\n.dsh-tl-tip-user {\n margin: 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n}\n.dsh-tl-tip-assistant {\n margin: 8px 0 0;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n overflow: hidden;\n white-space: pre-line;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-static-neutral-bluish-300, rgba(255, 255, 255, 0.8));\n}\n.dsh-tl-tip-assistant[data-kind=\"running\"],\n.dsh-tl-tip-assistant[data-kind=\"empty\"] {\n color: var(--dsw-static-neutral-bluish-400, rgba(255, 255, 255, 0.55));\n}\n/* \u63D2\u4EF6\u5B8C\u5168\u63A5\u7BA1\u539F\u751F\u53F3\u4FA7 Turn \u5BFC\u822A\u6761\uFF1A\u672C\u6837\u5F0F\u8868\u53EA\u968F\u63D2\u4EF6\u5B58\u5728\uFF0C\u63D2\u4EF6\u5378\u8F7D\u65F6\n removeStyles \u79FB\u9664\u6807\u7B7E\uFF0C\u539F\u751F\u5BFC\u822A\u6761\u7ACB\u5373\u6062\u590D\u3002aria-label \u8DDF\u968F\u5BBF\u4E3B\u8BED\u8A00\n \uFF08\u82F1\u6587 Turn navigation / \u4E2D\u6587 \u8F6E\u6B21\u5BFC\u822A\uFF09\uFF0C\u4E24\u4E2A\u90FD\u8981\u76D6\u4F4F\u3002 */\nnav[aria-label='Turn navigation'],\nnav[aria-label='\u8F6E\u6B21\u5BFC\u822A'] {\n display: none !important;\n}\n";
|
|
9
9
|
/** Inject the stylesheet once per document. */
|
|
10
10
|
export declare function ensureStyles(doc: Document): void;
|
|
11
11
|
/** Remove the stylesheet (plugin teardown): restores the native turn navigator. */
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@lament_z/dsh-client-ui-chat-timeline",
|
|
3
3
|
"displayName": "DSH Chat Timeline",
|
|
4
4
|
"description": "DSH Web 对话时间线插件:在左侧提供仿 ZCode TurnNavigator 的提问导航条,每个用户问题一个刻度,支持悬停波纹反馈、轮次预览卡片、点击跳转定位与滚动同步高亮,快速回溯长对话。",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"types": "lib/types/index.d.ts",
|
|
@@ -40,9 +40,12 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/dsh-client-
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-
|
|
43
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.7-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.1.7-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
|
|
46
49
|
"@types/react": "~18.3.1",
|
|
47
50
|
"@types/react-dom": "^18.3.5",
|
|
48
51
|
"jsdom": "29.1.1",
|
|
@@ -50,9 +53,7 @@
|
|
|
50
53
|
"react-dom": "^18.3.1",
|
|
51
54
|
"tsdown": "0.22.2",
|
|
52
55
|
"typescript": "~5.7.2",
|
|
53
|
-
"vitest": "^4.1.8"
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
|
|
55
|
-
"@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.2"
|
|
56
|
+
"vitest": "^4.1.8"
|
|
56
57
|
},
|
|
57
58
|
"files": [
|
|
58
59
|
"lib",
|