@chestnut23/dsh-conversation-outline 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -13
- package/README.zh.md +39 -14
- package/lib/client/OutlinePanel.js +2 -26
- package/lib/client/index.js +36 -3
- package/lib/client/official-nav.js +106 -0
- package/lib/client/styles.js +12 -0
- package/lib/client.js +146 -22
- package/lib/client.js.map +1 -1
- package/lib/types/client/OutlinePanel.d.ts +10 -25
- package/lib/types/client/index.d.ts +23 -0
- package/lib/types/client/official-nav.d.ts +41 -0
- package/lib/types/client/outline.d.ts +8 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -41,12 +41,19 @@ a flash highlight so you cannot miss it. Bilingual zh-CN / en.
|
|
|
41
41
|
|
|
42
42
|
## Install
|
|
43
43
|
|
|
44
|
-
> **Requirements**: DSH ≥ `0.1.5-rc.2` (the
|
|
45
|
-
>
|
|
46
|
-
>
|
|
47
|
-
>
|
|
44
|
+
> **Requirements**: DSH ≥ `0.1.5-rc.2` (the `next` channel / the DSH Desktop app).
|
|
45
|
+
>
|
|
46
|
+
> | Plugin version | Works with |
|
|
47
|
+
> |---|---|
|
|
48
|
+
> | `0.2.x` | DSH ≥ `0.1.5-rc.2` |
|
|
49
|
+
> | `0.1.2` and earlier | DSH `0.1.0-rc.x` (the Chat node graph still lived in `snapshot.chat`) |
|
|
48
50
|
|
|
49
51
|
|
|
52
|
+
> **Using the DSH Desktop app?** Run the same command in the app's built-in
|
|
53
|
+
> terminal **without `--profile`** — it targets the `desktop` profile
|
|
54
|
+
> automatically. The first install needs one restart of that DSH; later upgrades
|
|
55
|
+
> hot-reload (no restart).
|
|
56
|
+
|
|
50
57
|
**Prerequisites**: [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness)
|
|
51
58
|
(`dsh` available) **≥ 0.1.5-rc.2**; Node.js `^22.19` or `>=24`; pnpm 10+.
|
|
52
59
|
If you run DSH via `npx`, prefix the commands with `npx -p @deepseek-ai/dsh `.
|
|
@@ -54,13 +61,12 @@ On an older host, use plugin `0.1.3` — see the
|
|
|
54
61
|
[compatibility table](docs/troubleshooting.md#compatibility).
|
|
55
62
|
|
|
56
63
|
```sh
|
|
57
|
-
# 1)
|
|
58
|
-
dsh plugin --profile web add github:lzbaclz/dsh-conversation-outline
|
|
59
|
-
|
|
60
|
-
# 2) npm (published under our own scope; the unscoped name belongs to an
|
|
61
|
-
# unrelated project — always install the @chestnut23 scope)
|
|
64
|
+
# 1) npm (recommended — one command)
|
|
62
65
|
dsh plugin --profile web add @chestnut23/dsh-conversation-outline
|
|
63
66
|
|
|
67
|
+
# 2) GitHub (latest commit, no build step needed)
|
|
68
|
+
dsh plugin --profile web add github:lzbaclz/dsh-conversation-outline
|
|
69
|
+
|
|
64
70
|
# 3) from source (local development)
|
|
65
71
|
git clone https://github.com/lzbaclz/dsh-conversation-outline.git
|
|
66
72
|
cd dsh-conversation-outline
|
|
@@ -92,10 +98,32 @@ Upgrade with the same `add` command (optionally pin a version:
|
|
|
92
98
|
dsh plugin --profile web add @chestnut23/dsh-conversation-outline@latest # fetch the new version
|
|
93
99
|
```
|
|
94
100
|
|
|
95
|
-
The DSH
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
The DSH composition always mounts the `dsh-client-hmr` chain, which watches
|
|
102
|
+
installed plugin bundles and reloads them:
|
|
103
|
+
|
|
104
|
+
- **Upgrading an installed plugin** (same entry, new file content) is hot — no app
|
|
105
|
+
restart, usually not even a page refresh.
|
|
106
|
+
- **Installing a brand-new plugin** (a new roster entry) needs one DSH restart.
|
|
107
|
+
|
|
108
|
+
Every side effect (style tag, locale dictionaries, slot registrations) is owned by
|
|
109
|
+
the plugin fiber, so a hot reload never leaves stale code behind.
|
|
110
|
+
|
|
111
|
+
## Relationship to the built-in Turn navigation rail
|
|
112
|
+
|
|
113
|
+
DSH 0.1.5 renders its own right-edge rail (`TurnNavigatorRail`) that marks every
|
|
114
|
+
**turn** and labels its ticks `Turn N`: its prompt-preview field is empty, so the
|
|
115
|
+
ticks never show what you asked (a measured session had 72 turns and 2
|
|
116
|
+
questions), and 20x10px text-less ticks cannot say which question is which.
|
|
117
|
+
|
|
118
|
+
While this plugin is loaded it therefore hides that rail (identified by
|
|
119
|
+
`nav[aria-label]`, plus a structural fallback for rebuilt views; the marker is
|
|
120
|
+
re-applied through a MutationObserver) and owns the right edge itself. The change
|
|
121
|
+
is fully reversible:
|
|
122
|
+
|
|
123
|
+
- unloading the plugin restores the built-in rail automatically;
|
|
124
|
+
- keep it visible alongside this plugin with
|
|
125
|
+
`<html data-dsh-outline-keep-turn-nav>` (e.g. from a browser console:
|
|
126
|
+
`document.documentElement.setAttribute('data-dsh-outline-keep-turn-nav','')`).
|
|
99
127
|
|
|
100
128
|
## Usage
|
|
101
129
|
|
|
@@ -119,6 +147,7 @@ pnpm install # installs the pinned @deepseek-ai 0.1.5-rc.2 type packages
|
|
|
119
147
|
pnpm typecheck # host + client dual tsc programs
|
|
120
148
|
pnpm build # tsc(host) → tsc(client) → tsdown bundles lib/client.js
|
|
121
149
|
pnpm verify # offline smoke: manifest/exports/patch/bundle shape + pure-logic asserts
|
|
150
|
+
pnpm test:dom # headless DOM test: mounts the built rail in jsdom and asserts render/hover/jump
|
|
122
151
|
```
|
|
123
152
|
|
|
124
153
|
`pnpm typecheck` needs `lib/types/**` from the `@deepseek-ai/*` packages. Some DSH
|
package/README.zh.md
CHANGED
|
@@ -38,21 +38,29 @@
|
|
|
38
38
|
|
|
39
39
|
## 安装
|
|
40
40
|
|
|
41
|
-
> **环境要求**:DSH ≥ `0.1.5-rc.2
|
|
42
|
-
>
|
|
43
|
-
>
|
|
41
|
+
> **环境要求**:DSH ≥ `0.1.5-rc.2`(`next` 通道 / DSH Desktop 桌面版)。
|
|
42
|
+
>
|
|
43
|
+
> | 插件版本 | 适配的 DSH |
|
|
44
|
+
> |---|---|
|
|
45
|
+
> | `0.2.x` | DSH ≥ `0.1.5-rc.2` |
|
|
46
|
+
> | `0.1.2` 及更早 | DSH `0.1.0-rc.x`(Chat 节点图还在 `snapshot.chat` 里) |
|
|
44
47
|
|
|
45
48
|
|
|
49
|
+
> **用 DSH Desktop 桌面版?** 在应用内终端执行同样的命令、**不用写 `--profile`**
|
|
50
|
+
> (会自动指向 `desktop` profile)。首次安装需要重启一次;以后升级走热更新,无需重启。
|
|
51
|
+
|
|
46
52
|
**前置要求**:已安装 [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness)
|
|
47
53
|
(`dsh` 命令可用)且版本 **≥ 0.1.5-rc.2**;Node.js `^22.19` 或 `>=24`;pnpm 10+。
|
|
48
54
|
通过 `npx` 运行 DSH 的话,给下面命令加上 `npx -p @deepseek-ai/dsh ` 前缀。
|
|
49
55
|
更老的宿主请装插件 `0.1.3`,见[兼容表](docs/troubleshooting.md#compatibility)。
|
|
50
56
|
|
|
51
57
|
```sh
|
|
52
|
-
# 1)
|
|
53
|
-
dsh plugin --profile web add
|
|
58
|
+
# 1) npm(推荐,一条命令)
|
|
59
|
+
dsh plugin --profile web add @chestnut23/dsh-conversation-outline
|
|
54
60
|
|
|
55
|
-
# 2)
|
|
61
|
+
# 2) GitHub(最新提交,无需构建步骤)
|
|
62
|
+
dsh plugin --profile web add github:lzbaclz/dsh-conversation-outline
|
|
63
|
+
# 注意:npm 上无 scope 的同名包属于另一个无关项目,
|
|
56
64
|
# 务必安装 @chestnut23 scope)
|
|
57
65
|
dsh plugin --profile web add @chestnut23/dsh-conversation-outline
|
|
58
66
|
|
|
@@ -79,6 +87,19 @@ dsh plugin --profile web list
|
|
|
79
87
|
|
|
80
88
|
升级用同一条 `add` 命令(可钉版本:`@chestnut23/dsh-conversation-outline@0.1.4`)。
|
|
81
89
|
|
|
90
|
+
## 与官方「Turn navigation」轨道的关系
|
|
91
|
+
|
|
92
|
+
DSH 0.1.5 自带一条右边缘轨道(`TurnNavigatorRail`),它按 **turn** 打刻度、标签写作
|
|
93
|
+
`Turn N`——其 prompt 预览字段是空的,所以**从不显示你的提问原文**(实测某会话 72 个
|
|
94
|
+
turn 却只有 2 个提问),而且刻度仅 20×10px、无文字,无法分辨哪条对应哪个问题。
|
|
95
|
+
|
|
96
|
+
因此本插件加载期间会**隐藏那条轨道**(通过 `nav[aria-label]` 识别,另有面向视图重建的
|
|
97
|
+
结构化兜底,并用 MutationObserver 持续补标记),由本插件接管右边缘。该行为完全可逆:
|
|
98
|
+
|
|
99
|
+
- 卸载插件会自动恢复官方轨道;
|
|
100
|
+
- 想让它与本插件并存:在 `<html>` 上加 `data-dsh-outline-keep-turn-nav`,例如在浏览器
|
|
101
|
+
控制台执行 `document.documentElement.setAttribute('data-dsh-outline-keep-turn-nav','')`。
|
|
102
|
+
|
|
82
103
|
## 使用
|
|
83
104
|
|
|
84
105
|
装好重启后,打开任何一个已有消息的会话,右侧边缘会出现细竖条:悬停预览问题;**点击细条
|
|
@@ -98,17 +119,21 @@ pnpm install # 安装钉死版本的 @deepseek-ai 0.1.5-rc.2 类型包
|
|
|
98
119
|
pnpm typecheck # host + client 双 tsc program
|
|
99
120
|
pnpm build # tsc(host) → tsc(client) → tsdown 打包 lib/client.js
|
|
100
121
|
pnpm verify # 离线冒烟:manifest/exports/patch/产物形状 + 纯逻辑断言
|
|
122
|
+
pnpm test:dom # 无头 DOM 测试:jsdom 里挂载真实组件,断言细条渲染/悬停/点击跳转
|
|
101
123
|
```
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
类型面由 **devDependencies 里钉死版本的 `@deepseek-ai/*` 0.1.5-rc.2 包**提供(npm 上有
|
|
126
|
+
完整 `lib/types/**`)。注意 DSH Desktop 应用内置的副本**裁掉了声明文件**,所以不要把
|
|
127
|
+
类型指到 Electron 的 `node_modules`;类型版本与运行时版本保持一致,才能真正校验适配。
|
|
128
|
+
|
|
129
|
+
**热更新**:DSH 组合常驻 `dsh-client-hmr` 链(实测桌面版组合 `- id: client-hmr`,未禁用),
|
|
130
|
+
它监听已安装插件 bundle 的变化并自动重载:
|
|
131
|
+
|
|
132
|
+
- **升级已装插件** → 热更新,无需重启应用,页面通常自动生效(开发时 `pnpm exec tsdown --watch`
|
|
133
|
+
改完即生效;手动 `pnpm build` 后刷新页面亦可)
|
|
134
|
+
- **首次新增插件**(名册新增条目)→ 需要重启一次 DSH 服务
|
|
108
135
|
|
|
109
|
-
|
|
110
|
-
HMR 链(或简单刷新页面)即可生效。host / manifest 改动需要重启服务。scratch profile
|
|
111
|
-
测试配方:[docs/implementation-spec.md §4.2](docs/implementation-spec.md)。
|
|
136
|
+
scratch profile 测试配方:[docs/implementation-spec.md §4.2](docs/implementation-spec.md)。
|
|
112
137
|
|
|
113
138
|
## 一起聊聊
|
|
114
139
|
|
|
@@ -7,30 +7,6 @@ import { collectQuestions, filterQuestions, formatTime, isJumpTargetRow, } from
|
|
|
7
7
|
* a shape the host did not provide.
|
|
8
8
|
*/
|
|
9
9
|
const NO_FLOW = { order: [] };
|
|
10
|
-
/**
|
|
11
|
-
* Resolve one observable Chat source out of a session binding.
|
|
12
|
-
*
|
|
13
|
-
* The Chat node graph is a session-scoped store, reached exactly the way
|
|
14
|
-
* `@deepseek-ai/dsh-client-ui-chat` reaches it:
|
|
15
|
-
*
|
|
16
|
-
* ```js
|
|
17
|
-
* ctx.uiConversation.binding(binding).target('chat') // { getSnapshot, subscribe }
|
|
18
|
-
* ```
|
|
19
|
-
*
|
|
20
|
-
* Reading the provider off `binding.ctx` (rather than the root context) keeps
|
|
21
|
-
* this correct under any bus arrangement; a host that does not offer the
|
|
22
|
-
* service yields `undefined`, and the rail then renders nothing instead of
|
|
23
|
-
* crashing the slot.
|
|
24
|
-
*/
|
|
25
|
-
export function resolveChatFeed(binding) {
|
|
26
|
-
// Structural lookup on purpose: the binding's context face is a cordis
|
|
27
|
-
// Context in 0.1.5-rc.2 (`AgentContext`), whose `uiConversation` slot the
|
|
28
|
-
// service tracker only exposes at runtime, so a structural read is the
|
|
29
|
-
// portable way to reach it without value-importing another plugin.
|
|
30
|
-
const uiConversation = binding
|
|
31
|
-
?.ctx?.uiConversation;
|
|
32
|
-
return uiConversation?.binding?.(binding)?.target?.('chat');
|
|
33
|
-
}
|
|
34
10
|
const JUMP_HEADROOM = 96;
|
|
35
11
|
const JUMP_TIMEOUT_MS = 1500;
|
|
36
12
|
const FLASH_MS = 1900;
|
|
@@ -73,7 +49,7 @@ function scheduleTimeout(timeoutRef, fn, ms) {
|
|
|
73
49
|
}, ms);
|
|
74
50
|
timeoutRef.current.push(id);
|
|
75
51
|
}
|
|
76
|
-
export function OutlinePanel({ sessions, t, }) {
|
|
52
|
+
export function OutlinePanel({ sessions, resolveChatFeed, t, }) {
|
|
77
53
|
// Current-session feed: the sessions service owns it (`ISessions.list`), so
|
|
78
54
|
// subscribe to it here instead of taking a `useSessions` standard-kit prop —
|
|
79
55
|
// 0.1.5-rc.2 removed that hook from `GlobalStandardProps`.
|
|
@@ -87,7 +63,7 @@ export function OutlinePanel({ sessions, t, }) {
|
|
|
87
63
|
const session = binding?.session;
|
|
88
64
|
// Question flow: the session-scoped Chat store. History pagination lives on
|
|
89
65
|
// the session face, so its two flags are merged in here.
|
|
90
|
-
const chat = useMemo(() => resolveChatFeed(binding), [binding]);
|
|
66
|
+
const chat = useMemo(() => resolveChatFeed(binding), [resolveChatFeed, binding]);
|
|
91
67
|
const subscribe = useCallback((onChange) => (chat ? chat.subscribe(onChange) : () => { }), [chat]);
|
|
92
68
|
const getSnapshot = useCallback(() => (chat ? chat.getSnapshot() : NO_FLOW), [chat]);
|
|
93
69
|
const chatSnapshot = useSyncExternalStore(subscribe, getSnapshot);
|
package/lib/client/index.js
CHANGED
|
@@ -1,27 +1,60 @@
|
|
|
1
1
|
import { OutlinePanel } from "./OutlinePanel.js";
|
|
2
2
|
import { NS, dictionaries } from "./locales.js";
|
|
3
3
|
import { injectStyle } from "./styles.js";
|
|
4
|
+
import { hideOfficialTurnNavigation } from "./official-nav.js";
|
|
4
5
|
/**
|
|
5
6
|
* Client entry (implementation-spec §1.1/§2.4): the browser half of the
|
|
6
7
|
* plugin. Cordis services this fiber waits for before apply() runs.
|
|
8
|
+
*
|
|
9
|
+
* `uiConversation` is declared here on purpose: cordis guards `ctx` with a
|
|
10
|
+
* Proxy that THROWS on an undeclared service read, and the rail reaches the
|
|
11
|
+
* session's Chat store through it. Reading it off another context (e.g.
|
|
12
|
+
* `binding.ctx.uiConversation`) is not allowed even after declaring it — the
|
|
13
|
+
* guard is per-context, so `apply` captures it from its own ctx below.
|
|
7
14
|
*/
|
|
8
|
-
export const inject = ['slots', 'sessions', 'locale'];
|
|
15
|
+
export const inject = ['slots', 'sessions', 'locale', 'uiConversation'];
|
|
16
|
+
/**
|
|
17
|
+
* The conversation service captured from this plugin's own context. Set in
|
|
18
|
+
* `apply`; `undefined` only before activation, which the resolver tolerates.
|
|
19
|
+
*/
|
|
20
|
+
let pluginUiConversation;
|
|
9
21
|
export function apply(ctx) {
|
|
22
|
+
pluginUiConversation = ctx.uiConversation;
|
|
10
23
|
// Locale dictionaries — the typed register form (both shipped locales
|
|
11
24
|
// required, keys compile-checked). The disposer is owned by the fiber, so
|
|
12
25
|
// HMR removes the namespace on unload.
|
|
13
26
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-conversation-outline: locale');
|
|
14
27
|
// Panel CSS as an HMR-safe <style data-plugin> tag (effect-owned).
|
|
15
28
|
injectStyle(ctx);
|
|
29
|
+
// Take over the right edge: hide DSH's built-in Turn navigation rail while
|
|
30
|
+
// this plugin is loaded (reversible, with an opt-out root attribute). Owned by
|
|
31
|
+
// the fiber, so unloading the plugin restores the built-in rail.
|
|
32
|
+
if (typeof document !== 'undefined') {
|
|
33
|
+
ctx.effect(() => hideOfficialTurnNavigation(document), 'dsh-conversation-outline: hide the built-in turn navigation');
|
|
34
|
+
}
|
|
16
35
|
// Right-edge rail + hover panel in the frame-wide overlay layer.
|
|
17
36
|
// slots.inject waits for the layout's declaration of `shell.overlay` and
|
|
18
37
|
// routes the registration (and its unload cascade) through this fiber.
|
|
19
38
|
// `locale: NS` gives the component the typed `t` seat; the inject factory
|
|
20
|
-
// passes the
|
|
39
|
+
// passes the services the component reads.
|
|
21
40
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
22
41
|
name: 'shell.overlay',
|
|
23
42
|
id: 'dsh-conversation-outline.rail',
|
|
24
43
|
locale: NS,
|
|
25
|
-
inject: () => ({ sessions: ctx.sessions }),
|
|
44
|
+
inject: () => ({ sessions: ctx.sessions, resolveChatFeed }),
|
|
26
45
|
}, OutlinePanel));
|
|
27
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve one session binding's Chat feed through the conversation service.
|
|
49
|
+
*
|
|
50
|
+
* Returns `undefined` for every failure mode — no service (host without
|
|
51
|
+
* ui-conversation), no binding (no current session), no `chat` target — so the
|
|
52
|
+
* rail renders nothing instead of throwing inside its slot. Throwing here is
|
|
53
|
+
* especially costly: the slot error boundary swallows it and the plugin simply
|
|
54
|
+
* disappears with no visible error.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveChatFeed(binding) {
|
|
57
|
+
if (binding === null || binding === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
return pluginUiConversation?.binding?.(binding)?.target?.('chat');
|
|
60
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hide DSH's built-in Turn navigation rail while this plugin is active.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the built-in rail marks every *turn* and, in practice, labels
|
|
5
|
+
* its ticks `Turn N` — its prompt preview field is empty, so it never shows what
|
|
6
|
+
* the user actually asked. A measured session had 72 turns and 2 questions, and
|
|
7
|
+
* the ticks are 20x10px with no text, so the rail cannot answer "where is my
|
|
8
|
+
* question". The outline rail answers exactly that, and the two overlap on the
|
|
9
|
+
* same right edge. The escape hatch keeps the choice reversible:
|
|
10
|
+
*
|
|
11
|
+
* <html data-dsh-outline-keep-turn-nav> → built-in rail stays visible
|
|
12
|
+
*
|
|
13
|
+
* Identification avoids the CSS-module hash of the day (it changes with every
|
|
14
|
+
* DSH build): the rail is a `<nav aria-label>` the shell renders into a hashed
|
|
15
|
+
* slot, so matching is aria-label based with a structural fallback that pairs an
|
|
16
|
+
* aria-label with a hashed slot class. Switching sessions or views rebuilds that
|
|
17
|
+
* subtree, hence the observer; mutations are coalesced into one animation frame.
|
|
18
|
+
*
|
|
19
|
+
* Everything is reversible: the returned disposer disconnects the observer and
|
|
20
|
+
* removes every marker it set, so unloading the plugin restores the built-in rail.
|
|
21
|
+
*/
|
|
22
|
+
/** Selectors that identify the built-in Turn navigation rail. */
|
|
23
|
+
export const OFFICIAL_NAV_SELECTORS = [
|
|
24
|
+
// Stable, localized labels (English / Simplified Chinese shells).
|
|
25
|
+
'nav[aria-label="Turn navigation"]',
|
|
26
|
+
'nav[aria-label="轮次导航"]',
|
|
27
|
+
// Structural fallback: the shell renders the rail into a hashed slot wrapper,
|
|
28
|
+
// so an aria-labelled <nav> carrying a `*_slot` class is that rail even when a
|
|
29
|
+
// future build renames the label. This plugin never renders a <nav>.
|
|
30
|
+
'nav[aria-label][class*="_slot"]',
|
|
31
|
+
].join(', ');
|
|
32
|
+
/** Root attribute that keeps the built-in rail visible (escape hatch). */
|
|
33
|
+
export const KEEP_OFFICIAL_NAV_ATTRIBUTE = 'data-dsh-outline-keep-turn-nav';
|
|
34
|
+
/** Marker the CSS uses to hide the rail; also the cleanup ledger key. */
|
|
35
|
+
export const OFFICIAL_NAV_HIDDEN_ATTRIBUTE = 'data-dsh-outline-hides-official-nav';
|
|
36
|
+
/**
|
|
37
|
+
* Hide the built-in rail and keep it hidden across view rebuilds.
|
|
38
|
+
*
|
|
39
|
+
* @param doc - document to operate on (injectable for tests).
|
|
40
|
+
* @param scheduler - frame + observer factory (injectable for tests).
|
|
41
|
+
* @returns disposer that restores every element it marked.
|
|
42
|
+
*/
|
|
43
|
+
export function hideOfficialTurnNavigation(doc, scheduler = browserScheduler()) {
|
|
44
|
+
const root = doc.documentElement;
|
|
45
|
+
const marked = new Set();
|
|
46
|
+
let frame = null;
|
|
47
|
+
const restore = () => {
|
|
48
|
+
for (const element of marked)
|
|
49
|
+
element.removeAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE);
|
|
50
|
+
marked.clear();
|
|
51
|
+
};
|
|
52
|
+
const apply = () => {
|
|
53
|
+
frame = null;
|
|
54
|
+
if (root.hasAttribute(KEEP_OFFICIAL_NAV_ATTRIBUTE)) {
|
|
55
|
+
restore();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const element of doc.querySelectorAll(OFFICIAL_NAV_SELECTORS)) {
|
|
59
|
+
if (element.hasAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE))
|
|
60
|
+
continue;
|
|
61
|
+
element.setAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE, '');
|
|
62
|
+
marked.add(element);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const schedule = () => {
|
|
66
|
+
if (frame === null)
|
|
67
|
+
frame = scheduler.request(apply);
|
|
68
|
+
};
|
|
69
|
+
apply();
|
|
70
|
+
const disconnect = scheduler.observe(doc.documentElement, schedule);
|
|
71
|
+
return () => {
|
|
72
|
+
disconnect();
|
|
73
|
+
if (frame !== null) {
|
|
74
|
+
scheduler.cancel(frame);
|
|
75
|
+
frame = null;
|
|
76
|
+
}
|
|
77
|
+
restore();
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The real browser scheduler: one animation frame plus a subtree observer.
|
|
82
|
+
*
|
|
83
|
+
* Degrades safely where either API is missing (a non-DOM test host): the caller
|
|
84
|
+
* has already applied the markers once, and a missing observer only means later
|
|
85
|
+
* rebuilds are not re-marked.
|
|
86
|
+
*/
|
|
87
|
+
function browserScheduler() {
|
|
88
|
+
const frames = typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function';
|
|
89
|
+
const Observer = typeof MutationObserver === 'undefined' ? undefined : MutationObserver;
|
|
90
|
+
return {
|
|
91
|
+
request: (callback) => (frames ? requestAnimationFrame(callback) : setTimeout(callback, 0)),
|
|
92
|
+
cancel: (handle) => {
|
|
93
|
+
if (frames)
|
|
94
|
+
cancelAnimationFrame(handle);
|
|
95
|
+
else
|
|
96
|
+
clearTimeout(handle);
|
|
97
|
+
},
|
|
98
|
+
observe: (target, callback) => {
|
|
99
|
+
if (Observer === undefined)
|
|
100
|
+
return () => { };
|
|
101
|
+
const observer = new Observer(callback);
|
|
102
|
+
observer.observe(target, { childList: true, subtree: true });
|
|
103
|
+
return () => observer.disconnect();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/lib/client/styles.js
CHANGED
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
* pure overlay.
|
|
10
10
|
*/
|
|
11
11
|
export const panelCss = `
|
|
12
|
+
/* ---- Built-in Turn navigation rail -------------------------------------- */
|
|
13
|
+
/* DSH 0.1.5 renders its own right-edge rail that marks every TURN and labels
|
|
14
|
+
the ticks "Turn N" (its prompt preview is empty), so it never shows what the
|
|
15
|
+
user asked; this plugin's rail answers that question on the same edge. Hide
|
|
16
|
+
it while this plugin is loaded, keep the markers applied by official-nav.ts
|
|
17
|
+
for rebuilt subtrees, and honour <html data-dsh-outline-keep-turn-nav>. */
|
|
18
|
+
[data-dsh-outline-hides-official-nav],
|
|
19
|
+
html:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='Turn navigation'],
|
|
20
|
+
html:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='轮次导航'] {
|
|
21
|
+
display: none !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/* ---- Right-edge rail (always-visible minimap) --------------------------- */
|
|
13
25
|
.dso-rail {
|
|
14
26
|
position: fixed;
|
package/lib/client.js
CHANGED
|
@@ -108,24 +108,6 @@ window.__ModuleLoader__.load({
|
|
|
108
108
|
* a shape the host did not provide.
|
|
109
109
|
*/
|
|
110
110
|
const NO_FLOW = { order: [] };
|
|
111
|
-
/**
|
|
112
|
-
* Resolve one observable Chat source out of a session binding.
|
|
113
|
-
*
|
|
114
|
-
* The Chat node graph is a session-scoped store, reached exactly the way
|
|
115
|
-
* `@deepseek-ai/dsh-client-ui-chat` reaches it:
|
|
116
|
-
*
|
|
117
|
-
* ```js
|
|
118
|
-
* ctx.uiConversation.binding(binding).target('chat') // { getSnapshot, subscribe }
|
|
119
|
-
* ```
|
|
120
|
-
*
|
|
121
|
-
* Reading the provider off `binding.ctx` (rather than the root context) keeps
|
|
122
|
-
* this correct under any bus arrangement; a host that does not offer the
|
|
123
|
-
* service yields `undefined`, and the rail then renders nothing instead of
|
|
124
|
-
* crashing the slot.
|
|
125
|
-
*/
|
|
126
|
-
function resolveChatFeed(binding) {
|
|
127
|
-
return (binding?.ctx?.uiConversation)?.binding?.(binding)?.target?.("chat");
|
|
128
|
-
}
|
|
129
111
|
const JUMP_HEADROOM = 96;
|
|
130
112
|
const JUMP_TIMEOUT_MS = 1500;
|
|
131
113
|
const FLASH_MS = 1900;
|
|
@@ -167,11 +149,11 @@ window.__ModuleLoader__.load({
|
|
|
167
149
|
}, ms);
|
|
168
150
|
timeoutRef.current.push(id);
|
|
169
151
|
}
|
|
170
|
-
function OutlinePanel({ sessions, t }) {
|
|
152
|
+
function OutlinePanel({ sessions, resolveChatFeed, t }) {
|
|
171
153
|
const current = (0, react.useSyncExternalStore)((0, react.useCallback)((onChange) => sessions.list.subscribe(onChange), [sessions]), (0, react.useCallback)(() => sessions.list.getSnapshot(), [sessions])).current;
|
|
172
154
|
const binding = (0, react.useMemo)(() => current ? sessions.binding(current) : void 0, [sessions, current]);
|
|
173
155
|
const session = binding?.session;
|
|
174
|
-
const chat = (0, react.useMemo)(() => resolveChatFeed(binding), [binding]);
|
|
156
|
+
const chat = (0, react.useMemo)(() => resolveChatFeed(binding), [resolveChatFeed, binding]);
|
|
175
157
|
const chatSnapshot = (0, react.useSyncExternalStore)((0, react.useCallback)((onChange) => chat ? chat.subscribe(onChange) : () => {}, [chat]), (0, react.useCallback)(() => chat ? chat.getSnapshot() : NO_FLOW, [chat]));
|
|
176
158
|
const snapshot = (0, react.useMemo)(() => ({
|
|
177
159
|
...chatSnapshot,
|
|
@@ -544,6 +526,18 @@ window.__ModuleLoader__.load({
|
|
|
544
526
|
* pure overlay.
|
|
545
527
|
*/
|
|
546
528
|
const panelCss = `
|
|
529
|
+
/* ---- Built-in Turn navigation rail -------------------------------------- */
|
|
530
|
+
/* DSH 0.1.5 renders its own right-edge rail that marks every TURN and labels
|
|
531
|
+
the ticks "Turn N" (its prompt preview is empty), so it never shows what the
|
|
532
|
+
user asked; this plugin's rail answers that question on the same edge. Hide
|
|
533
|
+
it while this plugin is loaded, keep the markers applied by official-nav.ts
|
|
534
|
+
for rebuilt subtrees, and honour <html data-dsh-outline-keep-turn-nav>. */
|
|
535
|
+
[data-dsh-outline-hides-official-nav],
|
|
536
|
+
html:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='Turn navigation'],
|
|
537
|
+
html:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='轮次导航'] {
|
|
538
|
+
display: none !important;
|
|
539
|
+
}
|
|
540
|
+
|
|
547
541
|
/* ---- Right-edge rail (always-visible minimap) --------------------------- */
|
|
548
542
|
.dso-rail {
|
|
549
543
|
position: fixed;
|
|
@@ -862,29 +856,159 @@ window.__ModuleLoader__.load({
|
|
|
862
856
|
}, "dsh-conversation-outline: panel css");
|
|
863
857
|
}
|
|
864
858
|
//#endregion
|
|
859
|
+
//#region lib/client/official-nav.js
|
|
860
|
+
/**
|
|
861
|
+
* Hide DSH's built-in Turn navigation rail while this plugin is active.
|
|
862
|
+
*
|
|
863
|
+
* Why this exists: the built-in rail marks every *turn* and, in practice, labels
|
|
864
|
+
* its ticks `Turn N` — its prompt preview field is empty, so it never shows what
|
|
865
|
+
* the user actually asked. A measured session had 72 turns and 2 questions, and
|
|
866
|
+
* the ticks are 20x10px with no text, so the rail cannot answer "where is my
|
|
867
|
+
* question". The outline rail answers exactly that, and the two overlap on the
|
|
868
|
+
* same right edge. The escape hatch keeps the choice reversible:
|
|
869
|
+
*
|
|
870
|
+
* <html data-dsh-outline-keep-turn-nav> → built-in rail stays visible
|
|
871
|
+
*
|
|
872
|
+
* Identification avoids the CSS-module hash of the day (it changes with every
|
|
873
|
+
* DSH build): the rail is a `<nav aria-label>` the shell renders into a hashed
|
|
874
|
+
* slot, so matching is aria-label based with a structural fallback that pairs an
|
|
875
|
+
* aria-label with a hashed slot class. Switching sessions or views rebuilds that
|
|
876
|
+
* subtree, hence the observer; mutations are coalesced into one animation frame.
|
|
877
|
+
*
|
|
878
|
+
* Everything is reversible: the returned disposer disconnects the observer and
|
|
879
|
+
* removes every marker it set, so unloading the plugin restores the built-in rail.
|
|
880
|
+
*/
|
|
881
|
+
/** Selectors that identify the built-in Turn navigation rail. */
|
|
882
|
+
const OFFICIAL_NAV_SELECTORS = [
|
|
883
|
+
"nav[aria-label=\"Turn navigation\"]",
|
|
884
|
+
"nav[aria-label=\"轮次导航\"]",
|
|
885
|
+
"nav[aria-label][class*=\"_slot\"]"
|
|
886
|
+
].join(", ");
|
|
887
|
+
/** Marker the CSS uses to hide the rail; also the cleanup ledger key. */
|
|
888
|
+
const OFFICIAL_NAV_HIDDEN_ATTRIBUTE = "data-dsh-outline-hides-official-nav";
|
|
889
|
+
/**
|
|
890
|
+
* Hide the built-in rail and keep it hidden across view rebuilds.
|
|
891
|
+
*
|
|
892
|
+
* @param doc - document to operate on (injectable for tests).
|
|
893
|
+
* @param scheduler - frame + observer factory (injectable for tests).
|
|
894
|
+
* @returns disposer that restores every element it marked.
|
|
895
|
+
*/
|
|
896
|
+
function hideOfficialTurnNavigation(doc, scheduler = browserScheduler()) {
|
|
897
|
+
const root = doc.documentElement;
|
|
898
|
+
const marked = /* @__PURE__ */ new Set();
|
|
899
|
+
let frame = null;
|
|
900
|
+
const restore = () => {
|
|
901
|
+
for (const element of marked) element.removeAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE);
|
|
902
|
+
marked.clear();
|
|
903
|
+
};
|
|
904
|
+
const apply = () => {
|
|
905
|
+
frame = null;
|
|
906
|
+
if (root.hasAttribute("data-dsh-outline-keep-turn-nav")) {
|
|
907
|
+
restore();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
for (const element of doc.querySelectorAll(OFFICIAL_NAV_SELECTORS)) {
|
|
911
|
+
if (element.hasAttribute("data-dsh-outline-hides-official-nav")) continue;
|
|
912
|
+
element.setAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE, "");
|
|
913
|
+
marked.add(element);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
const schedule = () => {
|
|
917
|
+
if (frame === null) frame = scheduler.request(apply);
|
|
918
|
+
};
|
|
919
|
+
apply();
|
|
920
|
+
const disconnect = scheduler.observe(doc.documentElement, schedule);
|
|
921
|
+
return () => {
|
|
922
|
+
disconnect();
|
|
923
|
+
if (frame !== null) {
|
|
924
|
+
scheduler.cancel(frame);
|
|
925
|
+
frame = null;
|
|
926
|
+
}
|
|
927
|
+
restore();
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* The real browser scheduler: one animation frame plus a subtree observer.
|
|
932
|
+
*
|
|
933
|
+
* Degrades safely where either API is missing (a non-DOM test host): the caller
|
|
934
|
+
* has already applied the markers once, and a missing observer only means later
|
|
935
|
+
* rebuilds are not re-marked.
|
|
936
|
+
*/
|
|
937
|
+
function browserScheduler() {
|
|
938
|
+
const frames = typeof requestAnimationFrame === "function" && typeof cancelAnimationFrame === "function";
|
|
939
|
+
const Observer = typeof MutationObserver === "undefined" ? void 0 : MutationObserver;
|
|
940
|
+
return {
|
|
941
|
+
request: (callback) => frames ? requestAnimationFrame(callback) : setTimeout(callback, 0),
|
|
942
|
+
cancel: (handle) => {
|
|
943
|
+
if (frames) cancelAnimationFrame(handle);
|
|
944
|
+
else clearTimeout(handle);
|
|
945
|
+
},
|
|
946
|
+
observe: (target, callback) => {
|
|
947
|
+
if (Observer === void 0) return () => {};
|
|
948
|
+
const observer = new Observer(callback);
|
|
949
|
+
observer.observe(target, {
|
|
950
|
+
childList: true,
|
|
951
|
+
subtree: true
|
|
952
|
+
});
|
|
953
|
+
return () => observer.disconnect();
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
//#endregion
|
|
865
958
|
//#region lib/client/index.js
|
|
866
959
|
/**
|
|
867
960
|
* Client entry (implementation-spec §1.1/§2.4): the browser half of the
|
|
868
961
|
* plugin. Cordis services this fiber waits for before apply() runs.
|
|
962
|
+
*
|
|
963
|
+
* `uiConversation` is declared here on purpose: cordis guards `ctx` with a
|
|
964
|
+
* Proxy that THROWS on an undeclared service read, and the rail reaches the
|
|
965
|
+
* session's Chat store through it. Reading it off another context (e.g.
|
|
966
|
+
* `binding.ctx.uiConversation`) is not allowed even after declaring it — the
|
|
967
|
+
* guard is per-context, so `apply` captures it from its own ctx below.
|
|
869
968
|
*/
|
|
870
969
|
const inject = [
|
|
871
970
|
"slots",
|
|
872
971
|
"sessions",
|
|
873
|
-
"locale"
|
|
972
|
+
"locale",
|
|
973
|
+
"uiConversation"
|
|
874
974
|
];
|
|
975
|
+
/**
|
|
976
|
+
* The conversation service captured from this plugin's own context. Set in
|
|
977
|
+
* `apply`; `undefined` only before activation, which the resolver tolerates.
|
|
978
|
+
*/
|
|
979
|
+
let pluginUiConversation;
|
|
875
980
|
function apply(ctx) {
|
|
981
|
+
pluginUiConversation = ctx.uiConversation;
|
|
876
982
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-conversation-outline: locale");
|
|
877
983
|
injectStyle(ctx);
|
|
984
|
+
if (typeof document !== "undefined") ctx.effect(() => hideOfficialTurnNavigation(document), "dsh-conversation-outline: hide the built-in turn navigation");
|
|
878
985
|
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
879
986
|
name: "shell.overlay",
|
|
880
987
|
id: "dsh-conversation-outline.rail",
|
|
881
988
|
locale: NS,
|
|
882
|
-
inject: () => ({
|
|
989
|
+
inject: () => ({
|
|
990
|
+
sessions: ctx.sessions,
|
|
991
|
+
resolveChatFeed
|
|
992
|
+
})
|
|
883
993
|
}, OutlinePanel));
|
|
884
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* Resolve one session binding's Chat feed through the conversation service.
|
|
997
|
+
*
|
|
998
|
+
* Returns `undefined` for every failure mode — no service (host without
|
|
999
|
+
* ui-conversation), no binding (no current session), no `chat` target — so the
|
|
1000
|
+
* rail renders nothing instead of throwing inside its slot. Throwing here is
|
|
1001
|
+
* especially costly: the slot error boundary swallows it and the plugin simply
|
|
1002
|
+
* disappears with no visible error.
|
|
1003
|
+
*/
|
|
1004
|
+
function resolveChatFeed(binding) {
|
|
1005
|
+
if (binding === null || binding === void 0) return void 0;
|
|
1006
|
+
return pluginUiConversation?.binding?.(binding)?.target?.("chat");
|
|
1007
|
+
}
|
|
885
1008
|
//#endregion
|
|
886
1009
|
exports.apply = apply;
|
|
887
1010
|
exports.inject = inject;
|
|
1011
|
+
exports.resolveChatFeed = resolveChatFeed;
|
|
888
1012
|
return module.exports;
|
|
889
1013
|
}
|
|
890
1014
|
});
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["_Fragment"],"sources":["client/outline.js","client/OutlinePanel.js","client/locales.js","client/styles.js","client/index.js"],"sourcesContent":["/**\n * Pure logic for the conversation outline (implementation-spec §2.3).\n *\n * Deliberately has NO DOM access and NO React imports so scripts/verify.mjs can\n * import it directly in Node from the tsc output (lib/client/outline.js) and\n * assert the derivations offline.\n *\n * Input shapes are structural \"lite\" views of the runtime snapshot types\n * (@deepseek-ai/dsh-client-runtime/client) — the real objects are assignable\n * to them, and Node can import this module without the browser type graph.\n */\n/**\n * Resolve a Chat store snapshot into the flow to walk.\n *\n * Never throws: a snapshot that has not produced `order` + `nodes` yet (or a\n * host that handed over something else entirely) yields `undefined`, so the\n * caller renders an empty rail instead of crashing its slot.\n */\nexport function resolveOutlineFlow(snapshot) {\n if (snapshot === null || snapshot === undefined)\n return undefined;\n if (Array.isArray(snapshot.order) && snapshot.nodes !== undefined) {\n return { order: snapshot.order, nodes: snapshot.nodes };\n }\n return undefined;\n}\nconst IMAGE_PLACEHOLDER = '[image]';\n/**\n * Flatten message content blocks into a single display string: text blocks are\n * joined, image blocks become a placeholder, and whitespace is trimmed and\n * collapsed.\n */\nexport function flattenQuestionText(content) {\n return content\n .map((block) => {\n if (block.type === 'text')\n return block.text ?? '';\n if (block.type === 'image')\n return IMAGE_PLACEHOLDER;\n return '';\n })\n .join(' ')\n .trim()\n .replace(/\\s+/g, ' ');\n}\n/** Turn number of a node location: step/turn → location.turn.turn, else undefined. */\nfunction turnOf(location) {\n if (!location)\n return undefined;\n if (location.kind === 'step' || location.kind === 'turn')\n return location.turn?.turn;\n return undefined;\n}\n/**\n * Walk the chat flow in order, keep user/steering nodes, skip empty text, and\n * produce chronological outline items (spec §2.3).\n */\nexport function collectQuestions(snapshot) {\n const flow = resolveOutlineFlow(snapshot);\n if (flow === undefined)\n return [];\n const items = [];\n for (const key of flow.order) {\n const node = flow.nodes.get(key);\n if (!node)\n continue;\n if (node.kind !== 'user' && node.kind !== 'steering')\n continue;\n const data = node.data;\n const text = flattenQuestionText(data?.content ?? []);\n if (!text)\n continue;\n items.push({\n key: node.key,\n kind: node.kind,\n seq: data?.seq ?? 0,\n time: data?.time ?? 0,\n turn: turnOf(node.location),\n text,\n });\n }\n return items;\n}\n/** Case-insensitive substring filter over the flattened question text. */\nexport function filterQuestions(items, query) {\n const q = query.trim().toLowerCase();\n if (!q)\n return items;\n return items.filter((item) => item.text.toLowerCase().includes(q));\n}\n/** Local `HH:MM` from a unix-ms timestamp (spec §2.3). */\nexport function formatTime(ms) {\n const d = new Date(ms);\n const hh = String(d.getHours()).padStart(2, '0');\n const mm = String(d.getMinutes()).padStart(2, '0');\n return `${hh}:${mm}`;\n}\n/** Attributes the chat flow renders a node's identity under. */\nconst JUMP_KEY_ATTRIBUTES = ['data-chat-anchor-key', 'data-chat-flow-key'];\n/**\n * Pure DOM predicate for the jump loop: is this row the target node?\n *\n * Exact match only, against either attribute the flow wrapper carries — the\n * platform's own anchor lookup uses `data-chat-anchor-key`, and its sibling\n * `data-chat-flow-key` holds the same value, so a row is still found when only\n * one of the two is present.\n */\nexport function isJumpTargetRow(row, key) {\n return JUMP_KEY_ATTRIBUTES.some((attribute) => row.getAttribute(attribute) === key);\n}\n","import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from \"react/jsx-runtime\";\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';\nimport { collectQuestions, filterQuestions, formatTime, isJumpTargetRow, } from \"./outline.js\";\n/**\n * Stable getSnapshot value while no session (or no feed) is current (uSES\n * contract): an empty flow, so the rail renders nothing instead of throwing on\n * a shape the host did not provide.\n */\nconst NO_FLOW = { order: [] };\n/**\n * Resolve one observable Chat source out of a session binding.\n *\n * The Chat node graph is a session-scoped store, reached exactly the way\n * `@deepseek-ai/dsh-client-ui-chat` reaches it:\n *\n * ```js\n * ctx.uiConversation.binding(binding).target('chat') // { getSnapshot, subscribe }\n * ```\n *\n * Reading the provider off `binding.ctx` (rather than the root context) keeps\n * this correct under any bus arrangement; a host that does not offer the\n * service yields `undefined`, and the rail then renders nothing instead of\n * crashing the slot.\n */\nexport function resolveChatFeed(binding) {\n // Structural lookup on purpose: the binding's context face is a cordis\n // Context in 0.1.5-rc.2 (`AgentContext`), whose `uiConversation` slot the\n // service tracker only exposes at runtime, so a structural read is the\n // portable way to reach it without value-importing another plugin.\n const uiConversation = binding\n ?.ctx?.uiConversation;\n return uiConversation?.binding?.(binding)?.target?.('chat');\n}\nconst JUMP_HEADROOM = 96;\nconst JUMP_TIMEOUT_MS = 1500;\nconst FLASH_MS = 1900;\nconst COPIED_MS = 1500;\n/** Grace period before the hover panel collapses (lets the pointer travel). */\nconst COLLAPSE_DELAY_MS = 240;\n/** Rail capacity; older questions fold into the \"+N\" marker. */\nconst MAX_BARS = 60;\n/**\n * The conversation view's header tablist, scoped to the conversation root:\n * walk up from the scrollport's PARENT (the scrollport itself may contain\n * other tablists, e.g. the trajectory event-details tabs) and return the\n * nearest ancestor containing a `[role=\"tablist\"]`. Never falls back to a\n * document-wide query, so other tablists (trajectory event details,\n * settings, cordis source viewer) are not hit.\n */\nfunction findConversationTablist(scrollport) {\n let node = scrollport.parentElement;\n while (node) {\n const tablist = node.querySelector('[role=\"tablist\"]');\n if (tablist)\n return tablist;\n node = node.parentElement;\n }\n return null;\n}\n/** RAF that prunes itself from the tracking ref once it fires. */\nfunction scheduleRaf(rafRef, fn) {\n const id = requestAnimationFrame(() => {\n rafRef.current = rafRef.current.filter((x) => x !== id);\n fn();\n });\n rafRef.current.push(id);\n}\n/** Timeout that prunes itself from the tracking ref once it fires. */\nfunction scheduleTimeout(timeoutRef, fn, ms) {\n const id = window.setTimeout(() => {\n timeoutRef.current = timeoutRef.current.filter((x) => x !== id);\n fn();\n }, ms);\n timeoutRef.current.push(id);\n}\nexport function OutlinePanel({ sessions, t, }) {\n // Current-session feed: the sessions service owns it (`ISessions.list`), so\n // subscribe to it here instead of taking a `useSessions` standard-kit prop —\n // 0.1.5-rc.2 removed that hook from `GlobalStandardProps`.\n const listSubscribe = useCallback((onChange) => sessions.list.subscribe(onChange), [sessions]);\n const listSnapshot = useCallback(() => sessions.list.getSnapshot(), [sessions]);\n const current = useSyncExternalStore(listSubscribe, listSnapshot).current;\n // One binding per session, shared by the session face and the Chat feed: the\n // binding is the identity `uiConversation.binding()` caches its per-session\n // target on, so both must be resolved from the same object.\n const binding = useMemo(() => (current ? sessions.binding(current) : undefined), [sessions, current]);\n const session = binding?.session;\n // Question flow: the session-scoped Chat store. History pagination lives on\n // the session face, so its two flags are merged in here.\n const chat = useMemo(() => resolveChatFeed(binding), [binding]);\n const subscribe = useCallback((onChange) => (chat ? chat.subscribe(onChange) : () => { }), [chat]);\n const getSnapshot = useCallback(() => (chat ? chat.getSnapshot() : NO_FLOW), [chat]);\n const chatSnapshot = useSyncExternalStore(subscribe, getSnapshot);\n const snapshot = useMemo(() => ({\n ...chatSnapshot,\n hasMore: session?.getSnapshot?.().hasMore ?? false,\n loadingOlder: session?.getSnapshot?.().loadingOlder ?? false,\n }), [chatSnapshot, session]);\n // Two independent sources for the panel being open: `pinned` is a click\n // gesture (sticky until Escape / outside click / session change), `hovered`\n // is pointer presence over the rail or the panel. Deriving `open` from both\n // means a click on the rail opens the panel even when no hover event ever\n // fires (touch, synthetic/automation input, or a pointer that entered the\n // strip without crossing its boundary), and a hover preview still collapses\n // on its own once the pointer leaves.\n const [pinned, setPinned] = useState(false);\n const [hovered, setHovered] = useState(false);\n const open = pinned || hovered;\n const [query, setQuery] = useState('');\n const [copiedKey, setCopiedKey] = useState(null);\n // The question whose jump could not be completed; the panel says so instead\n // of failing silently (the row may sit outside the loaded history window).\n const [jumpFailed, setJumpFailed] = useState(null);\n const timeoutsRef = useRef([]);\n const rafsRef = useRef([]);\n const flashedRowRef = useRef(null);\n const collapseTimerRef = useRef(null);\n const railRef = useRef(null);\n const panelRef = useRef(null);\n // Cancel pending jump work (RAFs/timeouts), the flash highlight, and a\n // scheduled hover collapse.\n const clearPending = useCallback(() => {\n for (const id of rafsRef.current)\n window.cancelAnimationFrame(id);\n for (const id of timeoutsRef.current)\n window.clearTimeout(id);\n rafsRef.current = [];\n timeoutsRef.current = [];\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n if (flashedRowRef.current) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, []);\n /** Close the panel from any trigger: drop the pin and the hover preview. */\n const closePanel = useCallback(() => {\n setPinned(false);\n setHovered(false);\n cancelCollapseRef.current();\n }, []);\n // Session change → cancel pending work and collapse (navigate closes the\n // panel, spec §2.1; a mid-jump poll must not target the stale session).\n useEffect(() => {\n clearPending();\n setPinned(false);\n setHovered(false);\n setJumpFailed(null);\n }, [current, clearPending]);\n // Escape closes the panel (hover-opened or click-pinned).\n useEffect(() => {\n if (!open)\n return;\n const onKey = (event) => {\n if (event.key === 'Escape')\n closePanel();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [open, closePanel]);\n // A click anywhere outside the rail and the panel releases a pin, so the\n // panel behaves like every other dismissible overlay on the page.\n useEffect(() => {\n if (!pinned)\n return;\n const onPointerDown = (event) => {\n const target = event.target;\n if (!(target instanceof Node))\n return;\n if (railRef.current?.contains(target) || panelRef.current?.contains(target))\n return;\n closePanel();\n };\n window.addEventListener('mousedown', onPointerDown);\n return () => window.removeEventListener('mousedown', onPointerDown);\n }, [pinned, closePanel]);\n // Unmount cleanup: cancel pending flash/copied timeouts, jump RAFs, collapse\n // timer, and the flash attribute.\n useEffect(() => clearPending, [clearPending]);\n const allItems = useMemo(() => collectQuestions(snapshot), [snapshot]);\n const items = useMemo(() => filterQuestions(allItems, query), [allItems, query]);\n const hasMore = snapshot?.hasMore ?? false;\n const loadingOlder = snapshot?.loadingOlder ?? false;\n // ---- open mechanics: click pins, hover previews, grace period between ---\n const cancelCollapse = useCallback(() => {\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n }, []);\n // `closePanel` above runs before this declaration; a ref keeps the two\n // callbacks independent of declaration order.\n const cancelCollapseRef = useRef(cancelCollapse);\n cancelCollapseRef.current = cancelCollapse;\n const scheduleCollapse = useCallback(() => {\n cancelCollapse();\n collapseTimerRef.current = window.setTimeout(() => {\n collapseTimerRef.current = null;\n // Only a preview collapses on its own: a pinned panel stays until it is\n // closed, and a pointer still inside the rail or the panel (Chromium\n // raises no further enter events while the pointer sits still) keeps the\n // preview up.\n if (railRef.current?.matches(':hover') || panelRef.current?.matches(':hover'))\n return;\n setHovered(false);\n }, COLLAPSE_DELAY_MS);\n }, [cancelCollapse]);\n /** Pointer entered the rail or the panel: cancel a pending collapse, preview. */\n const onEnter = useCallback(() => {\n cancelCollapse();\n setHovered(true);\n }, [cancelCollapse]);\n /** Pointer left: let the grace period decide, unless the panel is pinned. */\n const onLeave = useCallback(() => {\n if (pinned)\n return;\n scheduleCollapse();\n }, [pinned, scheduleCollapse]);\n /** A click on the rail (or any bar) opens the panel and keeps it open. */\n const togglePin = useCallback(() => {\n cancelCollapse();\n setPinned((value) => !value);\n setHovered(true);\n }, [cancelCollapse]);\n /** The panel's own controls: clicking inside never closes it. */\n const keepOpen = useCallback(() => {\n cancelCollapse();\n setHovered(true);\n }, [cancelCollapse]);\n /** Find the chat row for a node key (pure predicate over DOM rows). */\n const findRow = useCallback((key) => {\n // Exact match on either anchor attribute the chat flow renders for a node:\n // `data-chat-anchor-key` is the one the platform's own anchorElement() uses,\n // `data-chat-flow-key` is its sibling on the same wrapper. No guessing at\n // the key's internals — a wrong row is worse than a reported failure.\n const rows = document.querySelectorAll('[data-chat-anchor-key], [data-chat-flow-key]');\n for (const row of rows) {\n if (isJumpTargetRow(row, key))\n return row;\n }\n return null;\n }, []);\n /** Scroll the row into view (96px headroom) and flash a highlight. */\n const flashAndScroll = useCallback((row) => {\n const scrollport = row.closest('[data-conversation-scroll]') ?? document.querySelector('[data-conversation-scroll]');\n if (scrollport) {\n const flowTop = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top;\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n scrollport.scrollTo({\n top: scrollport.scrollTop + flowTop - JUMP_HEADROOM,\n behavior: reduced ? 'auto' : 'smooth',\n });\n }\n // A second jump within FLASH_MS: clear the previous row first.\n if (flashedRowRef.current && flashedRowRef.current !== row) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n }\n flashedRowRef.current = row;\n row.setAttribute('data-dsh-outline-flash', 'true');\n scheduleTimeout(timeoutsRef, () => {\n if (flashedRowRef.current === row) {\n row.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, FLASH_MS);\n }, []);\n /** Jump-to-message algorithm (spec §2.2). */\n const jumpTo = useCallback((key, label) => {\n // The conversation page must be mounted; abort silently otherwise.\n const scrollport = document.querySelector('[data-conversation-scroll]');\n if (!scrollport)\n return;\n setJumpFailed(null);\n // Ensure the Chat view is active: chat is order 0 — the FIRST tab of the\n // conversation-root tablist, when one exists (setView is idempotent).\n // A profile without extra views has no header tablist at all — that is\n // fine, the rows are already visible.\n const tablist = findConversationTablist(scrollport);\n if (tablist) {\n const firstTab = tablist.querySelector('button[role=\"tab\"]');\n if (firstTab)\n firstTab.click();\n }\n // Poll for the target row regardless of tablist presence — render timing\n // only; the row is guaranteed in the loaded window (it came from the\n // snapshot). Canceled on session change/unmount via clearPending.\n const deadline = Date.now() + JUMP_TIMEOUT_MS;\n const poll = () => {\n const row = findRow(key);\n if (row) {\n flashAndScroll(row);\n closePanel();\n return;\n }\n if (Date.now() < deadline) {\n scheduleRaf(rafsRef, poll);\n }\n else {\n // Never fail silently: the panel states what happened, and the\n // console keeps the key for bug reports.\n console.warn(t('jumpFailed'), { key });\n setJumpFailed(label ?? key);\n }\n };\n scheduleRaf(rafsRef, poll);\n }, [findRow, flashAndScroll, t, closePanel]);\n /** Copy a question's text (clipboard API with execCommand fallback). */\n const copyText = useCallback((text, key) => {\n const done = () => {\n setCopiedKey(key);\n scheduleTimeout(timeoutsRef, () => setCopiedKey((k) => (k === key ? null : k)), COPIED_MS);\n };\n const fallback = () => {\n try {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.setAttribute('readonly', '');\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.select();\n document.execCommand('copy');\n ta.remove();\n done();\n }\n catch {\n // clipboard unavailable — ignore\n }\n };\n if (navigator.clipboard?.writeText) {\n navigator.clipboard.writeText(text).then(done, fallback);\n }\n else {\n fallback();\n }\n }, []);\n // No current session, or a session with nothing to outline → render nothing\n // (rail included). This is driven by the question list, not by the session's\n // `blank` flag: the flag belongs to the conversation snapshot, while the rail\n // is derived from the Chat flow, and a session whose nodes are not loaded yet\n // must not take the slot down. MUST stay below every hook (this overlay entry\n // stays mounted across session changes, so a conditional hook would crash\n // React — reviewer MUST-FIX #1).\n if (!current || !session || allItems.length === 0)\n return null;\n // Rail bars: chronological top→bottom, capped at MAX_BARS with the overflow\n // folded into a \"+N\" marker (the rail is a minimap, not the full list).\n const start = Math.max(0, allItems.length - MAX_BARS);\n const bars = allItems.slice(start);\n const overflow = start;\n return (_jsxs(_Fragment, { children: [_jsxs(\"div\", { ref: railRef, className: \"dso-rail\", role: \"group\", \"aria-label\": t('title'), tabIndex: 0, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onClick: (event) => {\n // Any click on the strip pins the panel open — the container itself,\n // its padding, or the \"+N\" marker. Bars stop propagation and reach\n // the same pin through the shared jump gesture below.\n if (event.target === event.currentTarget || event.target instanceof HTMLSpanElement) {\n togglePin();\n }\n }, onKeyDown: (event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n togglePin();\n }\n }, \"data-dso-open\": open ? 'true' : undefined, children: [overflow > 0 && (_jsxs(\"span\", { className: \"dso-rail-more\", \"aria-label\": t('moreBars', { count: overflow }), children: [\"+\", overflow] })), bars.map((item, index) => (_jsx(\"button\", { type: \"button\", className: \"dso-bar\", \"aria-label\": t('barLabel', { n: start + index + 1 }), onClick: (event) => {\n event.stopPropagation();\n // The bar's own job is the jump; the panel comes up pinned first\n // so a click always has a visible effect, then jumpTo collapses\n // it once the row is on screen.\n setPinned(true);\n setHovered(true);\n jumpTo(item.key, item.text);\n } }, item.key)))] }), open && (_jsxs(\"div\", { ref: panelRef, className: \"dso-panel\", role: \"region\", \"aria-label\": t('title'), \"data-dso-pinned\": pinned ? 'true' : undefined, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onMouseDown: keepOpen, children: [_jsxs(\"header\", { className: \"dso-panel-header\", children: [_jsx(\"span\", { className: \"dso-panel-title\", children: t('title') }), _jsx(\"span\", { className: \"dso-panel-count\", children: t('count', { count: allItems.length }) }), _jsx(\"button\", { type: \"button\", className: \"dso-panel-close\", \"aria-label\": t('close'), onClick: closePanel, children: \"\\u00D7\" })] }), _jsx(\"input\", { className: \"dso-search\", type: \"search\", value: query, placeholder: t('searchPlaceholder'), \"aria-label\": t('searchPlaceholder'), onChange: (event) => setQuery(event.target.value) }), _jsx(\"div\", { className: \"dso-list\", children: items.length === 0 ? (_jsx(\"div\", { className: \"dso-empty\", children: t('empty') })) : (items.map((item) => (_jsxs(\"div\", { className: \"dso-row\", children: [_jsxs(\"button\", { type: \"button\", className: \"dso-row-main\", onClick: () => jumpTo(item.key, item.text), children: [_jsx(\"span\", { className: \"dso-turn\", children: item.turn !== undefined ? `#${item.turn}` : '' }), _jsx(\"span\", { className: \"dso-text\", children: item.text }), item.kind === 'steering' && _jsx(\"span\", { className: \"dso-steer\", children: t('steerTag') }), _jsx(\"span\", { className: \"dso-time\", children: formatTime(item.time) })] }), _jsx(\"button\", { type: \"button\", className: copiedKey === item.key ? 'dso-copy dso-copied' : 'dso-copy', \"aria-label\": copiedKey === item.key ? t('copied') : t('copy'), onClick: () => copyText(item.text, item.key), children: copiedKey === item.key ? t('copied') : t('copy') })] }, item.key)))) }), jumpFailed !== null && (_jsx(\"p\", { className: \"dso-jump-failed\", role: \"status\", \"data-dso-jump-failed\": \"true\", children: t('jumpFailed') })), hasMore && (_jsx(\"footer\", { className: \"dso-footer\", children: _jsx(\"button\", { type: \"button\", className: \"dso-load-older\", disabled: loadingOlder, onClick: () => {\n void session.loadOlder();\n }, children: loadingOlder ? t('loadingOlder') : t('loadOlder') }) }))] }))] }));\n}\n","export const NS = 'dsh-conversation-outline';\nexport const zh = {\n title: '会话大纲',\n count: '{count} 个问题',\n searchPlaceholder: '搜索问题…',\n empty: '当前会话还没有问题',\n loadOlder: '加载更早',\n loadingOlder: '加载中…',\n copy: '复制',\n copied: '已复制',\n steerTag: '追问',\n close: '关闭',\n jumpFailed: '跳转失败:未找到对应消息',\n barLabel: '跳到第 {n} 个问题',\n moreBars: '还有 {count} 个更早的问题',\n};\nexport const en = {\n title: 'Outline',\n count: '{count} questions',\n searchPlaceholder: 'Search questions…',\n empty: 'No questions in this conversation yet',\n loadOlder: 'Load older',\n loadingOlder: 'Loading…',\n copy: 'Copy',\n copied: 'Copied',\n steerTag: 'steer',\n close: 'Close',\n jumpFailed: 'Jump failed: target message not found',\n barLabel: 'Jump to question {n}',\n moreBars: '{count} more earlier questions',\n};\nexport const dictionaries = { zh, en };\n","/**\n * Outline rail + hover panel CSS (implementation-spec §1.8/§2.1, revised):\n * a thin right-edge rail (conversation minimap) that is always visible while\n * the session has questions, and a preview panel that expands on hover.\n * Plain CSS string injected via an HMR-safe\n * `<style data-plugin=\"dsh-conversation-outline\" data-plugin-css=\"...\">` tag.\n * Stable prefixed class names (`dso_*`), DSH theme vars (`--dsw-alias-*`) —\n * never hashed class names of other packages. No layout shift: the panel is a\n * pure overlay.\n */\nexport const panelCss = `\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n/* Clickability affordance: a hovered or pinned strip lights its bars up, so\n it reads as a control rather than as decoration. */\n.dso-rail:hover .dso-bar,\n.dso-rail[data-dso-open] .dso-bar {\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);\n}\n.dso-rail:hover .dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n/* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it\n no longer collapses when the pointer leaves. */\n.dso-panel[data-dso-pinned] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) — no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question —\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Jump failure notice ------------------------------------------------ */\n.dso-jump-failed {\n margin: 0;\n padding: 8px 14px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n`;\n/** Inject the style tag, owned by the client fiber (removed on dispose/HMR). */\nexport function injectStyle(ctx) {\n ctx.effect(() => {\n const tag = document.createElement('style');\n tag.dataset.plugin = 'dsh-conversation-outline';\n tag.dataset.pluginCss = 'dsh-conversation-outline/panel.css';\n tag.textContent = panelCss;\n document.head.appendChild(tag);\n return () => {\n tag.remove();\n };\n }, 'dsh-conversation-outline: panel css');\n}\n","import { OutlinePanel } from \"./OutlinePanel.js\";\nimport { NS, dictionaries } from \"./locales.js\";\nimport { injectStyle } from \"./styles.js\";\n/**\n * Client entry (implementation-spec §1.1/§2.4): the browser half of the\n * plugin. Cordis services this fiber waits for before apply() runs.\n */\nexport const inject = ['slots', 'sessions', 'locale'];\nexport function apply(ctx) {\n // Locale dictionaries — the typed register form (both shipped locales\n // required, keys compile-checked). The disposer is owned by the fiber, so\n // HMR removes the namespace on unload.\n ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-conversation-outline: locale');\n // Panel CSS as an HMR-safe <style data-plugin> tag (effect-owned).\n injectStyle(ctx);\n // Right-edge rail + hover panel in the frame-wide overlay layer.\n // slots.inject waits for the layout's declaration of `shell.overlay` and\n // routes the registration (and its unload cascade) through this fiber.\n // `locale: NS` gives the component the typed `t` seat; the inject factory\n // passes the sessions service face.\n ctx.slots.inject('shell.overlay', () => ctx.slots.register({\n name: 'shell.overlay',\n id: 'dsh-conversation-outline.rail',\n locale: NS,\n inject: () => ({ sessions: ctx.sessions }),\n }, OutlinePanel));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["_Fragment"],"sources":["client/outline.js","client/OutlinePanel.js","client/locales.js","client/styles.js","client/official-nav.js","client/index.js"],"sourcesContent":["/**\n * Pure logic for the conversation outline (implementation-spec §2.3).\n *\n * Deliberately has NO DOM access and NO React imports so scripts/verify.mjs can\n * import it directly in Node from the tsc output (lib/client/outline.js) and\n * assert the derivations offline.\n *\n * Input shapes are structural \"lite\" views of the runtime snapshot types\n * (@deepseek-ai/dsh-client-runtime/client) — the real objects are assignable\n * to them, and Node can import this module without the browser type graph.\n */\n/**\n * Resolve a Chat store snapshot into the flow to walk.\n *\n * Never throws: a snapshot that has not produced `order` + `nodes` yet (or a\n * host that handed over something else entirely) yields `undefined`, so the\n * caller renders an empty rail instead of crashing its slot.\n */\nexport function resolveOutlineFlow(snapshot) {\n if (snapshot === null || snapshot === undefined)\n return undefined;\n if (Array.isArray(snapshot.order) && snapshot.nodes !== undefined) {\n return { order: snapshot.order, nodes: snapshot.nodes };\n }\n return undefined;\n}\nconst IMAGE_PLACEHOLDER = '[image]';\n/**\n * Flatten message content blocks into a single display string: text blocks are\n * joined, image blocks become a placeholder, and whitespace is trimmed and\n * collapsed.\n */\nexport function flattenQuestionText(content) {\n return content\n .map((block) => {\n if (block.type === 'text')\n return block.text ?? '';\n if (block.type === 'image')\n return IMAGE_PLACEHOLDER;\n return '';\n })\n .join(' ')\n .trim()\n .replace(/\\s+/g, ' ');\n}\n/** Turn number of a node location: step/turn → location.turn.turn, else undefined. */\nfunction turnOf(location) {\n if (!location)\n return undefined;\n if (location.kind === 'step' || location.kind === 'turn')\n return location.turn?.turn;\n return undefined;\n}\n/**\n * Walk the chat flow in order, keep user/steering nodes, skip empty text, and\n * produce chronological outline items (spec §2.3).\n */\nexport function collectQuestions(snapshot) {\n const flow = resolveOutlineFlow(snapshot);\n if (flow === undefined)\n return [];\n const items = [];\n for (const key of flow.order) {\n const node = flow.nodes.get(key);\n if (!node)\n continue;\n if (node.kind !== 'user' && node.kind !== 'steering')\n continue;\n const data = node.data;\n const text = flattenQuestionText(data?.content ?? []);\n if (!text)\n continue;\n items.push({\n key: node.key,\n kind: node.kind,\n seq: data?.seq ?? 0,\n time: data?.time ?? 0,\n turn: turnOf(node.location),\n text,\n });\n }\n return items;\n}\n/** Case-insensitive substring filter over the flattened question text. */\nexport function filterQuestions(items, query) {\n const q = query.trim().toLowerCase();\n if (!q)\n return items;\n return items.filter((item) => item.text.toLowerCase().includes(q));\n}\n/** Local `HH:MM` from a unix-ms timestamp (spec §2.3). */\nexport function formatTime(ms) {\n const d = new Date(ms);\n const hh = String(d.getHours()).padStart(2, '0');\n const mm = String(d.getMinutes()).padStart(2, '0');\n return `${hh}:${mm}`;\n}\n/** Attributes the chat flow renders a node's identity under. */\nconst JUMP_KEY_ATTRIBUTES = ['data-chat-anchor-key', 'data-chat-flow-key'];\n/**\n * Pure DOM predicate for the jump loop: is this row the target node?\n *\n * Exact match only, against either attribute the flow wrapper carries — the\n * platform's own anchor lookup uses `data-chat-anchor-key`, and its sibling\n * `data-chat-flow-key` holds the same value, so a row is still found when only\n * one of the two is present.\n */\nexport function isJumpTargetRow(row, key) {\n return JUMP_KEY_ATTRIBUTES.some((attribute) => row.getAttribute(attribute) === key);\n}\n","import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from \"react/jsx-runtime\";\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';\nimport { collectQuestions, filterQuestions, formatTime, isJumpTargetRow, } from \"./outline.js\";\n/**\n * Stable getSnapshot value while no session (or no feed) is current (uSES\n * contract): an empty flow, so the rail renders nothing instead of throwing on\n * a shape the host did not provide.\n */\nconst NO_FLOW = { order: [] };\nconst JUMP_HEADROOM = 96;\nconst JUMP_TIMEOUT_MS = 1500;\nconst FLASH_MS = 1900;\nconst COPIED_MS = 1500;\n/** Grace period before the hover panel collapses (lets the pointer travel). */\nconst COLLAPSE_DELAY_MS = 240;\n/** Rail capacity; older questions fold into the \"+N\" marker. */\nconst MAX_BARS = 60;\n/**\n * The conversation view's header tablist, scoped to the conversation root:\n * walk up from the scrollport's PARENT (the scrollport itself may contain\n * other tablists, e.g. the trajectory event-details tabs) and return the\n * nearest ancestor containing a `[role=\"tablist\"]`. Never falls back to a\n * document-wide query, so other tablists (trajectory event details,\n * settings, cordis source viewer) are not hit.\n */\nfunction findConversationTablist(scrollport) {\n let node = scrollport.parentElement;\n while (node) {\n const tablist = node.querySelector('[role=\"tablist\"]');\n if (tablist)\n return tablist;\n node = node.parentElement;\n }\n return null;\n}\n/** RAF that prunes itself from the tracking ref once it fires. */\nfunction scheduleRaf(rafRef, fn) {\n const id = requestAnimationFrame(() => {\n rafRef.current = rafRef.current.filter((x) => x !== id);\n fn();\n });\n rafRef.current.push(id);\n}\n/** Timeout that prunes itself from the tracking ref once it fires. */\nfunction scheduleTimeout(timeoutRef, fn, ms) {\n const id = window.setTimeout(() => {\n timeoutRef.current = timeoutRef.current.filter((x) => x !== id);\n fn();\n }, ms);\n timeoutRef.current.push(id);\n}\nexport function OutlinePanel({ sessions, resolveChatFeed, t, }) {\n // Current-session feed: the sessions service owns it (`ISessions.list`), so\n // subscribe to it here instead of taking a `useSessions` standard-kit prop —\n // 0.1.5-rc.2 removed that hook from `GlobalStandardProps`.\n const listSubscribe = useCallback((onChange) => sessions.list.subscribe(onChange), [sessions]);\n const listSnapshot = useCallback(() => sessions.list.getSnapshot(), [sessions]);\n const current = useSyncExternalStore(listSubscribe, listSnapshot).current;\n // One binding per session, shared by the session face and the Chat feed: the\n // binding is the identity `uiConversation.binding()` caches its per-session\n // target on, so both must be resolved from the same object.\n const binding = useMemo(() => (current ? sessions.binding(current) : undefined), [sessions, current]);\n const session = binding?.session;\n // Question flow: the session-scoped Chat store. History pagination lives on\n // the session face, so its two flags are merged in here.\n const chat = useMemo(() => resolveChatFeed(binding), [resolveChatFeed, binding]);\n const subscribe = useCallback((onChange) => (chat ? chat.subscribe(onChange) : () => { }), [chat]);\n const getSnapshot = useCallback(() => (chat ? chat.getSnapshot() : NO_FLOW), [chat]);\n const chatSnapshot = useSyncExternalStore(subscribe, getSnapshot);\n const snapshot = useMemo(() => ({\n ...chatSnapshot,\n hasMore: session?.getSnapshot?.().hasMore ?? false,\n loadingOlder: session?.getSnapshot?.().loadingOlder ?? false,\n }), [chatSnapshot, session]);\n // Two independent sources for the panel being open: `pinned` is a click\n // gesture (sticky until Escape / outside click / session change), `hovered`\n // is pointer presence over the rail or the panel. Deriving `open` from both\n // means a click on the rail opens the panel even when no hover event ever\n // fires (touch, synthetic/automation input, or a pointer that entered the\n // strip without crossing its boundary), and a hover preview still collapses\n // on its own once the pointer leaves.\n const [pinned, setPinned] = useState(false);\n const [hovered, setHovered] = useState(false);\n const open = pinned || hovered;\n const [query, setQuery] = useState('');\n const [copiedKey, setCopiedKey] = useState(null);\n // The question whose jump could not be completed; the panel says so instead\n // of failing silently (the row may sit outside the loaded history window).\n const [jumpFailed, setJumpFailed] = useState(null);\n const timeoutsRef = useRef([]);\n const rafsRef = useRef([]);\n const flashedRowRef = useRef(null);\n const collapseTimerRef = useRef(null);\n const railRef = useRef(null);\n const panelRef = useRef(null);\n // Cancel pending jump work (RAFs/timeouts), the flash highlight, and a\n // scheduled hover collapse.\n const clearPending = useCallback(() => {\n for (const id of rafsRef.current)\n window.cancelAnimationFrame(id);\n for (const id of timeoutsRef.current)\n window.clearTimeout(id);\n rafsRef.current = [];\n timeoutsRef.current = [];\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n if (flashedRowRef.current) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, []);\n /** Close the panel from any trigger: drop the pin and the hover preview. */\n const closePanel = useCallback(() => {\n setPinned(false);\n setHovered(false);\n cancelCollapseRef.current();\n }, []);\n // Session change → cancel pending work and collapse (navigate closes the\n // panel, spec §2.1; a mid-jump poll must not target the stale session).\n useEffect(() => {\n clearPending();\n setPinned(false);\n setHovered(false);\n setJumpFailed(null);\n }, [current, clearPending]);\n // Escape closes the panel (hover-opened or click-pinned).\n useEffect(() => {\n if (!open)\n return;\n const onKey = (event) => {\n if (event.key === 'Escape')\n closePanel();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [open, closePanel]);\n // A click anywhere outside the rail and the panel releases a pin, so the\n // panel behaves like every other dismissible overlay on the page.\n useEffect(() => {\n if (!pinned)\n return;\n const onPointerDown = (event) => {\n const target = event.target;\n if (!(target instanceof Node))\n return;\n if (railRef.current?.contains(target) || panelRef.current?.contains(target))\n return;\n closePanel();\n };\n window.addEventListener('mousedown', onPointerDown);\n return () => window.removeEventListener('mousedown', onPointerDown);\n }, [pinned, closePanel]);\n // Unmount cleanup: cancel pending flash/copied timeouts, jump RAFs, collapse\n // timer, and the flash attribute.\n useEffect(() => clearPending, [clearPending]);\n const allItems = useMemo(() => collectQuestions(snapshot), [snapshot]);\n const items = useMemo(() => filterQuestions(allItems, query), [allItems, query]);\n const hasMore = snapshot?.hasMore ?? false;\n const loadingOlder = snapshot?.loadingOlder ?? false;\n // ---- open mechanics: click pins, hover previews, grace period between ---\n const cancelCollapse = useCallback(() => {\n if (collapseTimerRef.current !== null) {\n window.clearTimeout(collapseTimerRef.current);\n collapseTimerRef.current = null;\n }\n }, []);\n // `closePanel` above runs before this declaration; a ref keeps the two\n // callbacks independent of declaration order.\n const cancelCollapseRef = useRef(cancelCollapse);\n cancelCollapseRef.current = cancelCollapse;\n const scheduleCollapse = useCallback(() => {\n cancelCollapse();\n collapseTimerRef.current = window.setTimeout(() => {\n collapseTimerRef.current = null;\n // Only a preview collapses on its own: a pinned panel stays until it is\n // closed, and a pointer still inside the rail or the panel (Chromium\n // raises no further enter events while the pointer sits still) keeps the\n // preview up.\n if (railRef.current?.matches(':hover') || panelRef.current?.matches(':hover'))\n return;\n setHovered(false);\n }, COLLAPSE_DELAY_MS);\n }, [cancelCollapse]);\n /** Pointer entered the rail or the panel: cancel a pending collapse, preview. */\n const onEnter = useCallback(() => {\n cancelCollapse();\n setHovered(true);\n }, [cancelCollapse]);\n /** Pointer left: let the grace period decide, unless the panel is pinned. */\n const onLeave = useCallback(() => {\n if (pinned)\n return;\n scheduleCollapse();\n }, [pinned, scheduleCollapse]);\n /** A click on the rail (or any bar) opens the panel and keeps it open. */\n const togglePin = useCallback(() => {\n cancelCollapse();\n setPinned((value) => !value);\n setHovered(true);\n }, [cancelCollapse]);\n /** The panel's own controls: clicking inside never closes it. */\n const keepOpen = useCallback(() => {\n cancelCollapse();\n setHovered(true);\n }, [cancelCollapse]);\n /** Find the chat row for a node key (pure predicate over DOM rows). */\n const findRow = useCallback((key) => {\n // Exact match on either anchor attribute the chat flow renders for a node:\n // `data-chat-anchor-key` is the one the platform's own anchorElement() uses,\n // `data-chat-flow-key` is its sibling on the same wrapper. No guessing at\n // the key's internals — a wrong row is worse than a reported failure.\n const rows = document.querySelectorAll('[data-chat-anchor-key], [data-chat-flow-key]');\n for (const row of rows) {\n if (isJumpTargetRow(row, key))\n return row;\n }\n return null;\n }, []);\n /** Scroll the row into view (96px headroom) and flash a highlight. */\n const flashAndScroll = useCallback((row) => {\n const scrollport = row.closest('[data-conversation-scroll]') ?? document.querySelector('[data-conversation-scroll]');\n if (scrollport) {\n const flowTop = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top;\n const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n scrollport.scrollTo({\n top: scrollport.scrollTop + flowTop - JUMP_HEADROOM,\n behavior: reduced ? 'auto' : 'smooth',\n });\n }\n // A second jump within FLASH_MS: clear the previous row first.\n if (flashedRowRef.current && flashedRowRef.current !== row) {\n flashedRowRef.current.removeAttribute('data-dsh-outline-flash');\n }\n flashedRowRef.current = row;\n row.setAttribute('data-dsh-outline-flash', 'true');\n scheduleTimeout(timeoutsRef, () => {\n if (flashedRowRef.current === row) {\n row.removeAttribute('data-dsh-outline-flash');\n flashedRowRef.current = null;\n }\n }, FLASH_MS);\n }, []);\n /** Jump-to-message algorithm (spec §2.2). */\n const jumpTo = useCallback((key, label) => {\n // The conversation page must be mounted; abort silently otherwise.\n const scrollport = document.querySelector('[data-conversation-scroll]');\n if (!scrollport)\n return;\n setJumpFailed(null);\n // Ensure the Chat view is active: chat is order 0 — the FIRST tab of the\n // conversation-root tablist, when one exists (setView is idempotent).\n // A profile without extra views has no header tablist at all — that is\n // fine, the rows are already visible.\n const tablist = findConversationTablist(scrollport);\n if (tablist) {\n const firstTab = tablist.querySelector('button[role=\"tab\"]');\n if (firstTab)\n firstTab.click();\n }\n // Poll for the target row regardless of tablist presence — render timing\n // only; the row is guaranteed in the loaded window (it came from the\n // snapshot). Canceled on session change/unmount via clearPending.\n const deadline = Date.now() + JUMP_TIMEOUT_MS;\n const poll = () => {\n const row = findRow(key);\n if (row) {\n flashAndScroll(row);\n closePanel();\n return;\n }\n if (Date.now() < deadline) {\n scheduleRaf(rafsRef, poll);\n }\n else {\n // Never fail silently: the panel states what happened, and the\n // console keeps the key for bug reports.\n console.warn(t('jumpFailed'), { key });\n setJumpFailed(label ?? key);\n }\n };\n scheduleRaf(rafsRef, poll);\n }, [findRow, flashAndScroll, t, closePanel]);\n /** Copy a question's text (clipboard API with execCommand fallback). */\n const copyText = useCallback((text, key) => {\n const done = () => {\n setCopiedKey(key);\n scheduleTimeout(timeoutsRef, () => setCopiedKey((k) => (k === key ? null : k)), COPIED_MS);\n };\n const fallback = () => {\n try {\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.setAttribute('readonly', '');\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.select();\n document.execCommand('copy');\n ta.remove();\n done();\n }\n catch {\n // clipboard unavailable — ignore\n }\n };\n if (navigator.clipboard?.writeText) {\n navigator.clipboard.writeText(text).then(done, fallback);\n }\n else {\n fallback();\n }\n }, []);\n // No current session, or a session with nothing to outline → render nothing\n // (rail included). This is driven by the question list, not by the session's\n // `blank` flag: the flag belongs to the conversation snapshot, while the rail\n // is derived from the Chat flow, and a session whose nodes are not loaded yet\n // must not take the slot down. MUST stay below every hook (this overlay entry\n // stays mounted across session changes, so a conditional hook would crash\n // React — reviewer MUST-FIX #1).\n if (!current || !session || allItems.length === 0)\n return null;\n // Rail bars: chronological top→bottom, capped at MAX_BARS with the overflow\n // folded into a \"+N\" marker (the rail is a minimap, not the full list).\n const start = Math.max(0, allItems.length - MAX_BARS);\n const bars = allItems.slice(start);\n const overflow = start;\n return (_jsxs(_Fragment, { children: [_jsxs(\"div\", { ref: railRef, className: \"dso-rail\", role: \"group\", \"aria-label\": t('title'), tabIndex: 0, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onClick: (event) => {\n // Any click on the strip pins the panel open — the container itself,\n // its padding, or the \"+N\" marker. Bars stop propagation and reach\n // the same pin through the shared jump gesture below.\n if (event.target === event.currentTarget || event.target instanceof HTMLSpanElement) {\n togglePin();\n }\n }, onKeyDown: (event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n togglePin();\n }\n }, \"data-dso-open\": open ? 'true' : undefined, children: [overflow > 0 && (_jsxs(\"span\", { className: \"dso-rail-more\", \"aria-label\": t('moreBars', { count: overflow }), children: [\"+\", overflow] })), bars.map((item, index) => (_jsx(\"button\", { type: \"button\", className: \"dso-bar\", \"aria-label\": t('barLabel', { n: start + index + 1 }), onClick: (event) => {\n event.stopPropagation();\n // The bar's own job is the jump; the panel comes up pinned first\n // so a click always has a visible effect, then jumpTo collapses\n // it once the row is on screen.\n setPinned(true);\n setHovered(true);\n jumpTo(item.key, item.text);\n } }, item.key)))] }), open && (_jsxs(\"div\", { ref: panelRef, className: \"dso-panel\", role: \"region\", \"aria-label\": t('title'), \"data-dso-pinned\": pinned ? 'true' : undefined, onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: onEnter, onBlur: onLeave, onMouseDown: keepOpen, children: [_jsxs(\"header\", { className: \"dso-panel-header\", children: [_jsx(\"span\", { className: \"dso-panel-title\", children: t('title') }), _jsx(\"span\", { className: \"dso-panel-count\", children: t('count', { count: allItems.length }) }), _jsx(\"button\", { type: \"button\", className: \"dso-panel-close\", \"aria-label\": t('close'), onClick: closePanel, children: \"\\u00D7\" })] }), _jsx(\"input\", { className: \"dso-search\", type: \"search\", value: query, placeholder: t('searchPlaceholder'), \"aria-label\": t('searchPlaceholder'), onChange: (event) => setQuery(event.target.value) }), _jsx(\"div\", { className: \"dso-list\", children: items.length === 0 ? (_jsx(\"div\", { className: \"dso-empty\", children: t('empty') })) : (items.map((item) => (_jsxs(\"div\", { className: \"dso-row\", children: [_jsxs(\"button\", { type: \"button\", className: \"dso-row-main\", onClick: () => jumpTo(item.key, item.text), children: [_jsx(\"span\", { className: \"dso-turn\", children: item.turn !== undefined ? `#${item.turn}` : '' }), _jsx(\"span\", { className: \"dso-text\", children: item.text }), item.kind === 'steering' && _jsx(\"span\", { className: \"dso-steer\", children: t('steerTag') }), _jsx(\"span\", { className: \"dso-time\", children: formatTime(item.time) })] }), _jsx(\"button\", { type: \"button\", className: copiedKey === item.key ? 'dso-copy dso-copied' : 'dso-copy', \"aria-label\": copiedKey === item.key ? t('copied') : t('copy'), onClick: () => copyText(item.text, item.key), children: copiedKey === item.key ? t('copied') : t('copy') })] }, item.key)))) }), jumpFailed !== null && (_jsx(\"p\", { className: \"dso-jump-failed\", role: \"status\", \"data-dso-jump-failed\": \"true\", children: t('jumpFailed') })), hasMore && (_jsx(\"footer\", { className: \"dso-footer\", children: _jsx(\"button\", { type: \"button\", className: \"dso-load-older\", disabled: loadingOlder, onClick: () => {\n void session.loadOlder();\n }, children: loadingOlder ? t('loadingOlder') : t('loadOlder') }) }))] }))] }));\n}\n","export const NS = 'dsh-conversation-outline';\nexport const zh = {\n title: '会话大纲',\n count: '{count} 个问题',\n searchPlaceholder: '搜索问题…',\n empty: '当前会话还没有问题',\n loadOlder: '加载更早',\n loadingOlder: '加载中…',\n copy: '复制',\n copied: '已复制',\n steerTag: '追问',\n close: '关闭',\n jumpFailed: '跳转失败:未找到对应消息',\n barLabel: '跳到第 {n} 个问题',\n moreBars: '还有 {count} 个更早的问题',\n};\nexport const en = {\n title: 'Outline',\n count: '{count} questions',\n searchPlaceholder: 'Search questions…',\n empty: 'No questions in this conversation yet',\n loadOlder: 'Load older',\n loadingOlder: 'Loading…',\n copy: 'Copy',\n copied: 'Copied',\n steerTag: 'steer',\n close: 'Close',\n jumpFailed: 'Jump failed: target message not found',\n barLabel: 'Jump to question {n}',\n moreBars: '{count} more earlier questions',\n};\nexport const dictionaries = { zh, en };\n","/**\n * Outline rail + hover panel CSS (implementation-spec §1.8/§2.1, revised):\n * a thin right-edge rail (conversation minimap) that is always visible while\n * the session has questions, and a preview panel that expands on hover.\n * Plain CSS string injected via an HMR-safe\n * `<style data-plugin=\"dsh-conversation-outline\" data-plugin-css=\"...\">` tag.\n * Stable prefixed class names (`dso_*`), DSH theme vars (`--dsw-alias-*`) —\n * never hashed class names of other packages. No layout shift: the panel is a\n * pure overlay.\n */\nexport const panelCss = `\n/* ---- Built-in Turn navigation rail -------------------------------------- */\n/* DSH 0.1.5 renders its own right-edge rail that marks every TURN and labels\n the ticks \"Turn N\" (its prompt preview is empty), so it never shows what the\n user asked; this plugin's rail answers that question on the same edge. Hide\n it while this plugin is loaded, keep the markers applied by official-nav.ts\n for rebuilt subtrees, and honour <html data-dsh-outline-keep-turn-nav>. */\n[data-dsh-outline-hides-official-nav],\nhtml:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='Turn navigation'],\nhtml:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='轮次导航'] {\n display: none !important;\n}\n\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n/* Clickability affordance: a hovered or pinned strip lights its bars up, so\n it reads as a control rather than as decoration. */\n.dso-rail:hover .dso-bar,\n.dso-rail[data-dso-open] .dso-bar {\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);\n}\n.dso-rail:hover .dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n/* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it\n no longer collapses when the pointer leaves. */\n.dso-panel[data-dso-pinned] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) — no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question —\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Jump failure notice ------------------------------------------------ */\n.dso-jump-failed {\n margin: 0;\n padding: 8px 14px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n`;\n/** Inject the style tag, owned by the client fiber (removed on dispose/HMR). */\nexport function injectStyle(ctx) {\n ctx.effect(() => {\n const tag = document.createElement('style');\n tag.dataset.plugin = 'dsh-conversation-outline';\n tag.dataset.pluginCss = 'dsh-conversation-outline/panel.css';\n tag.textContent = panelCss;\n document.head.appendChild(tag);\n return () => {\n tag.remove();\n };\n }, 'dsh-conversation-outline: panel css');\n}\n","/**\n * Hide DSH's built-in Turn navigation rail while this plugin is active.\n *\n * Why this exists: the built-in rail marks every *turn* and, in practice, labels\n * its ticks `Turn N` — its prompt preview field is empty, so it never shows what\n * the user actually asked. A measured session had 72 turns and 2 questions, and\n * the ticks are 20x10px with no text, so the rail cannot answer \"where is my\n * question\". The outline rail answers exactly that, and the two overlap on the\n * same right edge. The escape hatch keeps the choice reversible:\n *\n * <html data-dsh-outline-keep-turn-nav> → built-in rail stays visible\n *\n * Identification avoids the CSS-module hash of the day (it changes with every\n * DSH build): the rail is a `<nav aria-label>` the shell renders into a hashed\n * slot, so matching is aria-label based with a structural fallback that pairs an\n * aria-label with a hashed slot class. Switching sessions or views rebuilds that\n * subtree, hence the observer; mutations are coalesced into one animation frame.\n *\n * Everything is reversible: the returned disposer disconnects the observer and\n * removes every marker it set, so unloading the plugin restores the built-in rail.\n */\n/** Selectors that identify the built-in Turn navigation rail. */\nexport const OFFICIAL_NAV_SELECTORS = [\n // Stable, localized labels (English / Simplified Chinese shells).\n 'nav[aria-label=\"Turn navigation\"]',\n 'nav[aria-label=\"轮次导航\"]',\n // Structural fallback: the shell renders the rail into a hashed slot wrapper,\n // so an aria-labelled <nav> carrying a `*_slot` class is that rail even when a\n // future build renames the label. This plugin never renders a <nav>.\n 'nav[aria-label][class*=\"_slot\"]',\n].join(', ');\n/** Root attribute that keeps the built-in rail visible (escape hatch). */\nexport const KEEP_OFFICIAL_NAV_ATTRIBUTE = 'data-dsh-outline-keep-turn-nav';\n/** Marker the CSS uses to hide the rail; also the cleanup ledger key. */\nexport const OFFICIAL_NAV_HIDDEN_ATTRIBUTE = 'data-dsh-outline-hides-official-nav';\n/**\n * Hide the built-in rail and keep it hidden across view rebuilds.\n *\n * @param doc - document to operate on (injectable for tests).\n * @param scheduler - frame + observer factory (injectable for tests).\n * @returns disposer that restores every element it marked.\n */\nexport function hideOfficialTurnNavigation(doc, scheduler = browserScheduler()) {\n const root = doc.documentElement;\n const marked = new Set();\n let frame = null;\n const restore = () => {\n for (const element of marked)\n element.removeAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE);\n marked.clear();\n };\n const apply = () => {\n frame = null;\n if (root.hasAttribute(KEEP_OFFICIAL_NAV_ATTRIBUTE)) {\n restore();\n return;\n }\n for (const element of doc.querySelectorAll(OFFICIAL_NAV_SELECTORS)) {\n if (element.hasAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE))\n continue;\n element.setAttribute(OFFICIAL_NAV_HIDDEN_ATTRIBUTE, '');\n marked.add(element);\n }\n };\n const schedule = () => {\n if (frame === null)\n frame = scheduler.request(apply);\n };\n apply();\n const disconnect = scheduler.observe(doc.documentElement, schedule);\n return () => {\n disconnect();\n if (frame !== null) {\n scheduler.cancel(frame);\n frame = null;\n }\n restore();\n };\n}\n/**\n * The real browser scheduler: one animation frame plus a subtree observer.\n *\n * Degrades safely where either API is missing (a non-DOM test host): the caller\n * has already applied the markers once, and a missing observer only means later\n * rebuilds are not re-marked.\n */\nfunction browserScheduler() {\n const frames = typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function';\n const Observer = typeof MutationObserver === 'undefined' ? undefined : MutationObserver;\n return {\n request: (callback) => (frames ? requestAnimationFrame(callback) : setTimeout(callback, 0)),\n cancel: (handle) => {\n if (frames)\n cancelAnimationFrame(handle);\n else\n clearTimeout(handle);\n },\n observe: (target, callback) => {\n if (Observer === undefined)\n return () => { };\n const observer = new Observer(callback);\n observer.observe(target, { childList: true, subtree: true });\n return () => observer.disconnect();\n },\n };\n}\n","import { OutlinePanel } from \"./OutlinePanel.js\";\nimport { NS, dictionaries } from \"./locales.js\";\nimport { injectStyle } from \"./styles.js\";\nimport { hideOfficialTurnNavigation } from \"./official-nav.js\";\n/**\n * Client entry (implementation-spec §1.1/§2.4): the browser half of the\n * plugin. Cordis services this fiber waits for before apply() runs.\n *\n * `uiConversation` is declared here on purpose: cordis guards `ctx` with a\n * Proxy that THROWS on an undeclared service read, and the rail reaches the\n * session's Chat store through it. Reading it off another context (e.g.\n * `binding.ctx.uiConversation`) is not allowed even after declaring it — the\n * guard is per-context, so `apply` captures it from its own ctx below.\n */\nexport const inject = ['slots', 'sessions', 'locale', 'uiConversation'];\n/**\n * The conversation service captured from this plugin's own context. Set in\n * `apply`; `undefined` only before activation, which the resolver tolerates.\n */\nlet pluginUiConversation;\nexport function apply(ctx) {\n pluginUiConversation = ctx.uiConversation;\n // Locale dictionaries — the typed register form (both shipped locales\n // required, keys compile-checked). The disposer is owned by the fiber, so\n // HMR removes the namespace on unload.\n ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-conversation-outline: locale');\n // Panel CSS as an HMR-safe <style data-plugin> tag (effect-owned).\n injectStyle(ctx);\n // Take over the right edge: hide DSH's built-in Turn navigation rail while\n // this plugin is loaded (reversible, with an opt-out root attribute). Owned by\n // the fiber, so unloading the plugin restores the built-in rail.\n if (typeof document !== 'undefined') {\n ctx.effect(() => hideOfficialTurnNavigation(document), 'dsh-conversation-outline: hide the built-in turn navigation');\n }\n // Right-edge rail + hover panel in the frame-wide overlay layer.\n // slots.inject waits for the layout's declaration of `shell.overlay` and\n // routes the registration (and its unload cascade) through this fiber.\n // `locale: NS` gives the component the typed `t` seat; the inject factory\n // passes the services the component reads.\n ctx.slots.inject('shell.overlay', () => ctx.slots.register({\n name: 'shell.overlay',\n id: 'dsh-conversation-outline.rail',\n locale: NS,\n inject: () => ({ sessions: ctx.sessions, resolveChatFeed }),\n }, OutlinePanel));\n}\n/**\n * Resolve one session binding's Chat feed through the conversation service.\n *\n * Returns `undefined` for every failure mode — no service (host without\n * ui-conversation), no binding (no current session), no `chat` target — so the\n * rail renders nothing instead of throwing inside its slot. Throwing here is\n * especially costly: the slot error boundary swallows it and the plugin simply\n * disappears with no visible error.\n */\nexport function resolveChatFeed(binding) {\n if (binding === null || binding === undefined)\n return undefined;\n return pluginUiConversation?.binding?.(binding)?.target?.('chat');\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import type { ReactElement } from 'react';
|
|
|
2
2
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
3
|
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
4
4
|
import { NS } from './locales.ts';
|
|
5
|
-
import type {
|
|
5
|
+
import type { OutlineChatFeed } from './outline.ts';
|
|
6
6
|
/**
|
|
7
7
|
* Conversation-outline rail + hover panel (implementation-spec §2.1/§2.2,
|
|
8
8
|
* revised per user feedback): NO top-right badge — instead a thin, always-
|
|
@@ -20,32 +20,17 @@ import type { OutlineSnapshotLike } from './outline.ts';
|
|
|
20
20
|
export interface OutlinePanelProps {
|
|
21
21
|
/**
|
|
22
22
|
* The sessions service face, injected by the registration: resolves the
|
|
23
|
-
* binding
|
|
24
|
-
* the session's Chat store.
|
|
23
|
+
* session binding whose `.session` face carries history pagination.
|
|
25
24
|
*/
|
|
26
25
|
sessions: ISessions;
|
|
26
|
+
/**
|
|
27
|
+
* Chat-feed resolver injected by the registration. It closes over the
|
|
28
|
+
* conversation service captured from the plugin's OWN context, because cordis
|
|
29
|
+
* refuses service reads on a foreign context and throws on undeclared ones.
|
|
30
|
+
* Tolerates `undefined` (no current session) by yielding `undefined`.
|
|
31
|
+
*/
|
|
32
|
+
resolveChatFeed: (binding: unknown) => OutlineChatFeed | undefined;
|
|
27
33
|
/** Typed translate seat for our namespace (declared via `locale: NS`). */
|
|
28
34
|
t: TranslateNS<typeof NS>;
|
|
29
35
|
}
|
|
30
|
-
|
|
31
|
-
export interface OutlineChatFeed {
|
|
32
|
-
getSnapshot: () => OutlineSnapshotLike;
|
|
33
|
-
subscribe: (onChange: () => void) => () => void;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Resolve one observable Chat source out of a session binding.
|
|
37
|
-
*
|
|
38
|
-
* The Chat node graph is a session-scoped store, reached exactly the way
|
|
39
|
-
* `@deepseek-ai/dsh-client-ui-chat` reaches it:
|
|
40
|
-
*
|
|
41
|
-
* ```js
|
|
42
|
-
* ctx.uiConversation.binding(binding).target('chat') // { getSnapshot, subscribe }
|
|
43
|
-
* ```
|
|
44
|
-
*
|
|
45
|
-
* Reading the provider off `binding.ctx` (rather than the root context) keeps
|
|
46
|
-
* this correct under any bus arrangement; a host that does not offer the
|
|
47
|
-
* service yields `undefined`, and the rail then renders nothing instead of
|
|
48
|
-
* crashing the slot.
|
|
49
|
-
*/
|
|
50
|
-
export declare function resolveChatFeed(binding: unknown): OutlineChatFeed | undefined;
|
|
51
|
-
export declare function OutlinePanel({ sessions, t, }: OutlinePanelProps): ReactElement | null;
|
|
36
|
+
export declare function OutlinePanel({ sessions, resolveChatFeed, t, }: OutlinePanelProps): ReactElement | null;
|
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { OutlineChatFeed } from './outline.ts';
|
|
2
3
|
/**
|
|
3
4
|
* Client entry (implementation-spec §1.1/§2.4): the browser half of the
|
|
4
5
|
* plugin. Cordis services this fiber waits for before apply() runs.
|
|
6
|
+
*
|
|
7
|
+
* `uiConversation` is declared here on purpose: cordis guards `ctx` with a
|
|
8
|
+
* Proxy that THROWS on an undeclared service read, and the rail reaches the
|
|
9
|
+
* session's Chat store through it. Reading it off another context (e.g.
|
|
10
|
+
* `binding.ctx.uiConversation`) is not allowed even after declaring it — the
|
|
11
|
+
* guard is per-context, so `apply` captures it from its own ctx below.
|
|
5
12
|
*/
|
|
6
13
|
export declare const inject: string[];
|
|
14
|
+
/** Structural face of the conversation service the rail needs (no value import). */
|
|
15
|
+
export interface UiConversationLike {
|
|
16
|
+
binding?: (source: unknown) => {
|
|
17
|
+
target?: (name: string) => unknown;
|
|
18
|
+
} | undefined;
|
|
19
|
+
}
|
|
7
20
|
export declare function apply(ctx: ClientContext): void;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve one session binding's Chat feed through the conversation service.
|
|
23
|
+
*
|
|
24
|
+
* Returns `undefined` for every failure mode — no service (host without
|
|
25
|
+
* ui-conversation), no binding (no current session), no `chat` target — so the
|
|
26
|
+
* rail renders nothing instead of throwing inside its slot. Throwing here is
|
|
27
|
+
* especially costly: the slot error boundary swallows it and the plugin simply
|
|
28
|
+
* disappears with no visible error.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveChatFeed(binding: unknown): OutlineChatFeed | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hide DSH's built-in Turn navigation rail while this plugin is active.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the built-in rail marks every *turn* and, in practice, labels
|
|
5
|
+
* its ticks `Turn N` — its prompt preview field is empty, so it never shows what
|
|
6
|
+
* the user actually asked. A measured session had 72 turns and 2 questions, and
|
|
7
|
+
* the ticks are 20x10px with no text, so the rail cannot answer "where is my
|
|
8
|
+
* question". The outline rail answers exactly that, and the two overlap on the
|
|
9
|
+
* same right edge. The escape hatch keeps the choice reversible:
|
|
10
|
+
*
|
|
11
|
+
* <html data-dsh-outline-keep-turn-nav> → built-in rail stays visible
|
|
12
|
+
*
|
|
13
|
+
* Identification avoids the CSS-module hash of the day (it changes with every
|
|
14
|
+
* DSH build): the rail is a `<nav aria-label>` the shell renders into a hashed
|
|
15
|
+
* slot, so matching is aria-label based with a structural fallback that pairs an
|
|
16
|
+
* aria-label with a hashed slot class. Switching sessions or views rebuilds that
|
|
17
|
+
* subtree, hence the observer; mutations are coalesced into one animation frame.
|
|
18
|
+
*
|
|
19
|
+
* Everything is reversible: the returned disposer disconnects the observer and
|
|
20
|
+
* removes every marker it set, so unloading the plugin restores the built-in rail.
|
|
21
|
+
*/
|
|
22
|
+
/** Selectors that identify the built-in Turn navigation rail. */
|
|
23
|
+
export declare const OFFICIAL_NAV_SELECTORS: string;
|
|
24
|
+
/** Root attribute that keeps the built-in rail visible (escape hatch). */
|
|
25
|
+
export declare const KEEP_OFFICIAL_NAV_ATTRIBUTE = "data-dsh-outline-keep-turn-nav";
|
|
26
|
+
/** Marker the CSS uses to hide the rail; also the cleanup ledger key. */
|
|
27
|
+
export declare const OFFICIAL_NAV_HIDDEN_ATTRIBUTE = "data-dsh-outline-hides-official-nav";
|
|
28
|
+
/** Minimal scheduling surface, injectable so tests need no animation frames. */
|
|
29
|
+
export interface NavHideScheduler {
|
|
30
|
+
request: (callback: () => void) => number;
|
|
31
|
+
cancel: (handle: number) => void;
|
|
32
|
+
observe: (target: Node, callback: () => void) => () => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hide the built-in rail and keep it hidden across view rebuilds.
|
|
36
|
+
*
|
|
37
|
+
* @param doc - document to operate on (injectable for tests).
|
|
38
|
+
* @param scheduler - frame + observer factory (injectable for tests).
|
|
39
|
+
* @returns disposer that restores every element it marked.
|
|
40
|
+
*/
|
|
41
|
+
export declare function hideOfficialTurnNavigation(doc: Document, scheduler?: NavHideScheduler): () => void;
|
|
@@ -73,6 +73,14 @@ export interface OutlineSnapshotLike {
|
|
|
73
73
|
/** The session face is paging older history right now. */
|
|
74
74
|
loadingOlder?: boolean | undefined;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* One observable Chat source: the rail reads it through `useSyncExternalStore`
|
|
78
|
+
* (reference-stable snapshots) and receives change notifications through it.
|
|
79
|
+
*/
|
|
80
|
+
export interface OutlineChatFeed {
|
|
81
|
+
getSnapshot: () => OutlineSnapshotLike;
|
|
82
|
+
subscribe: (onChange: () => void) => () => void;
|
|
83
|
+
}
|
|
76
84
|
/** The node flow plus its index, after validating the store snapshot. */
|
|
77
85
|
export interface OutlineFlow {
|
|
78
86
|
order: readonly string[];
|
|
@@ -9,6 +9,6 @@ import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
|
9
9
|
* never hashed class names of other packages. No layout shift: the panel is a
|
|
10
10
|
* pure overlay.
|
|
11
11
|
*/
|
|
12
|
-
export declare const panelCss = "\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n/* Clickability affordance: a hovered or pinned strip lights its bars up, so\n it reads as a control rather than as decoration. */\n.dso-rail:hover .dso-bar,\n.dso-rail[data-dso-open] .dso-bar {\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);\n}\n.dso-rail:hover .dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n/* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it\n no longer collapses when the pointer leaves. */\n.dso-panel[data-dso-pinned] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) \u2014 no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question \u2014\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Jump failure notice ------------------------------------------------ */\n.dso-jump-failed {\n margin: 0;\n padding: 8px 14px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n";
|
|
12
|
+
export declare const panelCss = "\n/* ---- Built-in Turn navigation rail -------------------------------------- */\n/* DSH 0.1.5 renders its own right-edge rail that marks every TURN and labels\n the ticks \"Turn N\" (its prompt preview is empty), so it never shows what the\n user asked; this plugin's rail answers that question on the same edge. Hide\n it while this plugin is loaded, keep the markers applied by official-nav.ts\n for rebuilt subtrees, and honour <html data-dsh-outline-keep-turn-nav>. */\n[data-dsh-outline-hides-official-nav],\nhtml:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='Turn navigation'],\nhtml:not([data-dsh-outline-keep-turn-nav]) nav[aria-label='\u8F6E\u6B21\u5BFC\u822A'] {\n display: none !important;\n}\n\n/* ---- Right-edge rail (always-visible minimap) --------------------------- */\n.dso-rail {\n position: fixed;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 3px;\n width: 30px;\n max-height: 55vh;\n padding: 8px 10px;\n overflow: hidden;\n cursor: pointer;\n}\n.dso-rail-more {\n flex: none;\n font-size: 9px;\n font-weight: 600;\n line-height: 10px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n.dso-bar {\n flex: none;\n width: 10px;\n height: 6px;\n padding: 0;\n border: none;\n border-radius: 3px;\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);\n cursor: pointer;\n transition: background 0.12s, transform 0.12s;\n}\n.dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n transform: scaleX(1.35);\n}\n.dso-rail:focus-visible,\n.dso-bar:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n/* Clickability affordance: a hovered or pinned strip lights its bars up, so\n it reads as a control rather than as decoration. */\n.dso-rail:hover .dso-bar,\n.dso-rail[data-dso-open] .dso-bar {\n background: color-mix(in srgb, var(--dsw-alias-label-tertiary) 72%, transparent);\n}\n.dso-rail:hover .dso-bar:hover {\n background: var(--dsw-alias-state-business-primary);\n}\n\n/* ---- Hover-expanded preview panel --------------------------------------- */\n.dso-panel {\n position: fixed;\n top: 50%;\n right: 16px;\n transform: translateY(-50%);\n z-index: 2147483000;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(340px, calc(100vw - 24px));\n max-height: min(70vh, 600px);\n overflow: hidden;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 16px;\n background: color-mix(in srgb, var(--dsw-alias-bg-module-platform) 92%, transparent);\n backdrop-filter: blur(20px);\n box-shadow: 0 16px 48px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);\n color: var(--dsw-alias-label-primary);\n animation: dso-panel-in 140ms ease-out;\n}\n/* A pinned panel (clicked open) keeps a defined edge, so it is obvious that it\n no longer collapses when the pointer leaves. */\n.dso-panel[data-dso-pinned] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n@keyframes dso-panel-in {\n from { opacity: 0; transform: translateY(-50%) translateX(12px); }\n to { opacity: 1; transform: translateY(-50%) translateX(0); }\n}\n\n.dso-panel-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px 10px;\n border-bottom: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-panel-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dso-panel-count {\n flex: 1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-panel-close:focus-visible,\n.dso-search:focus-visible,\n.dso-row-main:focus-visible,\n.dso-copy:focus-visible,\n.dso-load-older:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dso-search {\n margin: 10px 12px 4px;\n box-sizing: border-box;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n.dso-search::placeholder { color: var(--dsw-alias-label-tertiary); }\n\n/* ---- Question list ------------------------------------------------------ */\n.dso-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px;\n}\n/* Row container: two sibling buttons (jump + copy) \u2014 no nested interactive\n controls. Hover highlights the whole row. */\n.dso-row {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px;\n border-radius: 10px;\n transition: background 0.12s;\n}\n.dso-row:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.dso-row-main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 2px 6px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background 0.12s;\n}\n.dso-row-main:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }\n\n.dso-turn {\n flex: none;\n min-width: 26px;\n height: 16px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: var(--dsw-alias-bg-fill-business);\n color: var(--dsw-alias-label-on-fill);\n font-size: 10px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Single-line truncation: shows the opening words of each question \u2014\n anything longer simply ellipsizes (per user request). */\n.dso-text {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n line-height: 18px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.dso-time {\n flex: none;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n}\n.dso-steer {\n flex: none;\n padding: 0 6px;\n border: 1px solid var(--dsw-alias-line-normal);\n border-radius: 999px;\n font-size: 10px;\n color: var(--dsw-alias-label-secondary);\n}\n.dso-copy {\n flex: none;\n padding: 2px 6px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s, color 0.12s, background 0.12s;\n}\n.dso-row:hover .dso-copy,\n.dso-row:focus-within .dso-copy,\n.dso-copy:focus-visible { opacity: 1; }\n.dso-copy:hover { background: var(--dsw-alias-bg-fill-neutral); }\n.dso-copy.dso-copied { color: var(--dsw-alias-state-success); }\n\n.dso-empty {\n padding: 28px 16px;\n text-align: center;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ---- Jump failure notice ------------------------------------------------ */\n.dso-jump-failed {\n margin: 0;\n padding: 8px 14px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warning, var(--dsw-alias-label-secondary));\n}\n\n/* ---- Footer / load older ------------------------------------------------ */\n.dso-footer {\n display: flex;\n justify-content: center;\n padding: 8px 12px 10px;\n border-top: 1px solid var(--dsw-alias-line-normal);\n}\n.dso-load-older {\n padding: 4px 14px;\n border: none;\n border-radius: 14px;\n background: var(--dsw-alias-interactive-bg-hover-solid);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 12px;\n cursor: pointer;\n}\n.dso-load-older:disabled { cursor: default; opacity: 0.6; }\n\n/* ---- Jump flash highlight ----------------------------------------------- */\n[data-dsh-outline-flash] {\n animation: dso-flash 1.8s ease-out;\n}\n@keyframes dso-flash {\n 0% {\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, transparent);\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent);\n }\n 70% {\n box-shadow: 0 0 0 6px transparent;\n background-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);\n }\n 100% {\n box-shadow: 0 0 0 0 transparent;\n background-color: transparent;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dso-panel { animation: none; }\n .dso-bar { transition: none; }\n [data-dsh-outline-flash] { animation: none; }\n}\n";
|
|
13
13
|
/** Inject the style tag, owned by the client fiber (removed on dispose/HMR). */
|
|
14
14
|
export declare function injectStyle(ctx: ClientContext): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chestnut23/dsh-conversation-outline",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Codex-style conversation outline for DeepSeek Harness: a floating panel listing every user question in the current conversation with search, load-older and click-to-jump (scroll + highlight). Bilingual zh-CN / en.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && tsdown",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
47
47
|
"verify": "node scripts/verify.mjs",
|
|
48
|
-
"prepublishOnly": "pnpm build && pnpm verify"
|
|
48
|
+
"prepublishOnly": "pnpm build && pnpm verify && pnpm test:dom",
|
|
49
|
+
"test:dom": "node scripts/dom-smoke.mjs"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
52
|
"react": "^18.2.0"
|
|
@@ -56,12 +57,14 @@
|
|
|
56
57
|
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
57
58
|
"@deepseek-ai/dsh-client-store": "0.1.5-rc.2",
|
|
58
59
|
"@deepseek-ai/dsh-client-ui-chat": "0.1.5-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
|
|
59
61
|
"@deepseek-ai/dsh-client-ui-layout": "0.1.5-rc.2",
|
|
60
62
|
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
|
|
61
63
|
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
|
|
62
64
|
"@types/node": "^22.0.0",
|
|
63
65
|
"@types/react": "~18.3.1",
|
|
64
66
|
"@types/react-dom": "^19.2.4",
|
|
67
|
+
"jsdom": "^30.0.1",
|
|
65
68
|
"lightningcss": "^1.33.0",
|
|
66
69
|
"react": "^18.2.0",
|
|
67
70
|
"react-dom": "^18.2.0",
|