@chestnut23/dsh-conversation-outline 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,8 +42,10 @@ a flash highlight so you cannot miss it. Bilingual zh-CN / en.
42
42
  ## Install
43
43
 
44
44
  **Prerequisites**: [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness)
45
- (`dsh` available); Node.js `^22.19` or `>=24`; pnpm 10+.
45
+ (`dsh` available) **≥ 0.1.5-rc.2**; Node.js `^22.19` or `>=24`; pnpm 10+.
46
46
  If you run DSH via `npx`, prefix the commands with `npx -p @deepseek-ai/dsh `.
47
+ On an older host, use plugin `0.1.3` — see the
48
+ [compatibility table](docs/troubleshooting.md#compatibility).
47
49
 
48
50
  ```sh
49
51
  # 1) GitHub (works right now — recommended)
@@ -76,7 +78,7 @@ dsh plugin --profile web list
76
78
  ```
77
79
 
78
80
  Upgrade with the same `add` command (optionally pin a version:
79
- `@chestnut23/dsh-conversation-outline@0.1.3`).
81
+ `@chestnut23/dsh-conversation-outline@0.1.4`).
80
82
 
81
83
  ## Usage
82
84
 
package/README.zh.md CHANGED
@@ -39,8 +39,9 @@
39
39
  ## 安装
40
40
 
41
41
  **前置要求**:已安装 [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness)
42
- (`dsh` 命令可用);Node.js `^22.19` 或 `>=24`;pnpm 10+。
42
+ (`dsh` 命令可用)且版本 **≥ 0.1.5-rc.2**;Node.js `^22.19` 或 `>=24`;pnpm 10+。
43
43
  通过 `npx` 运行 DSH 的话,给下面命令加上 `npx -p @deepseek-ai/dsh ` 前缀。
44
+ 更老的宿主请装插件 `0.1.3`,见[兼容表](docs/troubleshooting.md#compatibility)。
44
45
 
45
46
  ```sh
46
47
  # 1) GitHub(现在就能装,推荐)
@@ -71,7 +72,7 @@ dsh plugin --profile web add "link:$(pwd)"
71
72
  dsh plugin --profile web list
72
73
  ```
73
74
 
74
- 升级用同一条 `add` 命令(可钉版本:`@chestnut23/dsh-conversation-outline@0.1.3`)。
75
+ 升级用同一条 `add` 命令(可钉版本:`@chestnut23/dsh-conversation-outline@0.1.4`)。
75
76
 
76
77
  ## 使用
77
78
 
@@ -1,29 +1,44 @@
1
1
  # Troubleshooting
2
2
 
3
+ ## Compatibility
4
+
5
+ | Plugin | Host DSH | Notes |
6
+ | --- | --- | --- |
7
+ | **0.1.4+** | **≥ 0.1.5-rc.2** | reads the session-scoped Chat store only (`uiConversation…target('chat')`) |
8
+ | 0.1.3 | ≥ 0.1.5-rc.2, and older | store first, `ConversationSnapshot.chat` as fallback |
9
+ | ≤ 0.1.2 | hosts that still nest `chat` | **crashes on 0.1.5+** — see below |
10
+
3
11
  ## The rail is missing entirely, and the console shows a slot error
4
12
 
5
- Since 0.1.3 the plugin reads the Chat node graph from the session-scoped Chat store
6
- (`uiConversation…target('chat')`), which is where DSH 0.1.5-rc.2 keeps it, and falls
7
- back to `ConversationSnapshot.chat` for older builds.
13
+ The Chat node graph is a **session-scoped store** from DSH 0.1.5-rc.2 on, reached the same
14
+ way the platform's own Chat view reaches it:
15
+
16
+ ```js
17
+ ctx.uiConversation.binding(binding).target('chat') // { getSnapshot, subscribe }
18
+ ```
19
+
20
+ Its snapshot carries `order` / `nodes` at the top level. `ConversationSnapshot` no longer
21
+ has a `chat` member.
8
22
 
9
- Before 0.1.3 the plugin read `snapshot.chat` unconditionally. On DSH versions that no
10
- longer carry it, that threw on every render and the shell's slot error boundary replaced
11
- the whole overlay entry with an empty `[data-slot-error="shell.overlay"]` div — the rail
12
- simply never appeared, with no visible error:
23
+ Releases **≤ 0.1.2** read `snapshot.chat` unconditionally. On a 0.1.5+ host that threw on
24
+ every render, and the shell's slot error boundary replaced the entire overlay entry with
25
+ an empty `[data-slot-error="shell.overlay"]` div — the rail never appeared, with no
26
+ visible error anywhere:
13
27
 
14
28
  ```
15
29
  TypeError: can't access property "order", snapshot.chat is undefined
16
30
  slot entry crashed in 'shell.overlay'
17
31
  ```
18
32
 
19
- Fix: update the plugin to **0.1.3 or later**. To confirm what your host is running:
33
+ Fix: install **0.1.4 or later** for DSH 0.1.5-rc.2:
20
34
 
21
35
  ```sh
22
- dsh --version # the CLI/app version
36
+ dsh plugin --profile web add @chestnut23/dsh-conversation-outline@0.1.4
23
37
  ```
24
38
 
25
- If you are on an older host and cannot update, pin the plugin to `0.1.2` (that release
26
- still expects the nested `chat` object). Do not mix: 0.1.3 works on both generations.
39
+ Check the host version with `dsh --version`. On a host older than 0.1.5-rc.2, stay on
40
+ `0.1.3` (the last release that supports the nested `chat` snapshot); `0.1.4+` targets the
41
+ store contract only, on purpose.
27
42
 
28
43
  Note: on a host that has its own turn navigation rail, that built-in rail (one tick per
29
44
  turn, dark bar = current turn) sits in the same edge and can be mistaken for this
@@ -8,31 +8,23 @@ import { collectQuestions, filterQuestions, formatTime, isJumpTargetRow, } from
8
8
  */
9
9
  const NO_FLOW = { order: [] };
10
10
  /**
11
- * Resolve one observable Chat source out of a session binding for the two
12
- * contract generations DSH shipped:
11
+ * Resolve one observable Chat source out of a session binding.
13
12
  *
14
- * - `binding.ctx.uiConversation.binding(binding).target('chat')` 0.1.5-rc.2
15
- * and later, where the Chat node graph is a session-scoped store and
16
- * `ConversationSnapshot` no longer carries `chat`;
17
- * - the session face itself — earlier builds, whose snapshot still exposes
18
- * `chat` (and which `resolveOutlineFlow` prefers when it is present).
13
+ * The Chat node graph is a session-scoped store, reached exactly the way
14
+ * `@deepseek-ai/dsh-client-ui-chat` reaches it:
19
15
  *
20
- * Reading the provider off `binding.ctx` rather than the root context keeps it
21
- * correct under any bus arrangement, and a host without the service degrades to
22
- * the legacy face instead of crashing the slot.
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.
23
24
  */
24
- export function resolveChatFeed(session, binding) {
25
- if (session === undefined || binding === undefined)
26
- return undefined;
27
- const uiConversation = binding.ctx?.uiConversation;
28
- const chat = uiConversation?.binding?.(session)?.target?.('chat');
29
- if (chat !== undefined)
30
- return chat;
31
- // Legacy fallback: on builds whose ConversationSnapshot still nests `chat`,
32
- // the session face itself is the observable carrying it. The cast is
33
- // deliberate — that generation is not describable by this build's types, and
34
- // `resolveOutlineFlow` reads whatever shape it finds defensively.
35
- return session;
25
+ export function resolveChatFeed(binding) {
26
+ const uiConversation = binding?.ctx?.uiConversation;
27
+ return uiConversation?.binding?.(binding)?.target?.('chat');
36
28
  }
37
29
  const JUMP_HEADROOM = 96;
38
30
  const JUMP_TIMEOUT_MS = 1500;
@@ -78,10 +70,14 @@ function scheduleTimeout(timeoutRef, fn, ms) {
78
70
  }
79
71
  export function OutlinePanel({ sessions, useSessions, t, }) {
80
72
  const current = useSessions((s) => s.current);
81
- const session = current ? sessions.binding(current)?.session : undefined;
82
- // Question flow: the Chat store (0.1.5-rc.2+) or the session snapshot's own
83
- // `chat` (earlier builds). Pagination still lives on the session face in both.
84
- const chat = useMemo(() => resolveChatFeed(session, current ? sessions.binding(current) : undefined), [session, sessions, current]);
73
+ // One binding per session, shared by the session face and the Chat feed: the
74
+ // binding is the identity `uiConversation.binding()` caches its per-session
75
+ // target on, so both must be resolved from the same object.
76
+ const binding = useMemo(() => (current ? sessions.binding(current) : undefined), [sessions, current]);
77
+ const session = binding?.session;
78
+ // Question flow: the session-scoped Chat store. History pagination lives on
79
+ // the session face, so its two flags are merged in here.
80
+ const chat = useMemo(() => resolveChatFeed(binding), [binding]);
85
81
  const subscribe = useCallback((onChange) => (chat ? chat.subscribe(onChange) : () => { }), [chat]);
86
82
  const getSnapshot = useCallback(() => (chat ? chat.getSnapshot() : NO_FLOW), [chat]);
87
83
  const chatSnapshot = useSyncExternalStore(subscribe, getSnapshot);
@@ -10,19 +10,15 @@
10
10
  * to them, and Node can import this module without the browser type graph.
11
11
  */
12
12
  /**
13
- * Resolve one snapshot into the flow to walk, preferring the nested `chat`
14
- * object and falling back to the top-level store fields.
13
+ * Resolve a Chat store snapshot into the flow to walk.
15
14
  *
16
- * Never throws: a snapshot carrying neither shape yields `undefined`, so the
15
+ * Never throws: a snapshot that has not produced `order` + `nodes` yet (or a
16
+ * host that handed over something else entirely) yields `undefined`, so the
17
17
  * caller renders an empty rail instead of crashing its slot.
18
18
  */
19
19
  export function resolveOutlineFlow(snapshot) {
20
20
  if (snapshot === null || snapshot === undefined)
21
21
  return undefined;
22
- const chat = snapshot.chat;
23
- if (chat !== undefined && Array.isArray(chat.order) && chat.nodes !== undefined) {
24
- return { order: chat.order, nodes: chat.nodes };
25
- }
26
22
  if (Array.isArray(snapshot.order) && snapshot.nodes !== undefined) {
27
23
  return { order: snapshot.order, nodes: snapshot.nodes };
28
24
  }
package/lib/client.js CHANGED
@@ -19,19 +19,14 @@ window.__ModuleLoader__.load({
19
19
  * to them, and Node can import this module without the browser type graph.
20
20
  */
21
21
  /**
22
- * Resolve one snapshot into the flow to walk, preferring the nested `chat`
23
- * object and falling back to the top-level store fields.
22
+ * Resolve a Chat store snapshot into the flow to walk.
24
23
  *
25
- * Never throws: a snapshot carrying neither shape yields `undefined`, so the
24
+ * Never throws: a snapshot that has not produced `order` + `nodes` yet (or a
25
+ * host that handed over something else entirely) yields `undefined`, so the
26
26
  * caller renders an empty rail instead of crashing its slot.
27
27
  */
28
28
  function resolveOutlineFlow(snapshot) {
29
29
  if (snapshot === null || snapshot === void 0) return void 0;
30
- const chat = snapshot.chat;
31
- if (chat !== void 0 && Array.isArray(chat.order) && chat.nodes !== void 0) return {
32
- order: chat.order,
33
- nodes: chat.nodes
34
- };
35
30
  if (Array.isArray(snapshot.order) && snapshot.nodes !== void 0) return {
36
31
  order: snapshot.order,
37
32
  nodes: snapshot.nodes
@@ -114,24 +109,22 @@ window.__ModuleLoader__.load({
114
109
  */
115
110
  const NO_FLOW = { order: [] };
116
111
  /**
117
- * Resolve one observable Chat source out of a session binding for the two
118
- * contract generations DSH shipped:
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:
119
116
  *
120
- * - `binding.ctx.uiConversation.binding(binding).target('chat')` — 0.1.5-rc.2
121
- * and later, where the Chat node graph is a session-scoped store and
122
- * `ConversationSnapshot` no longer carries `chat`;
123
- * - the session face itself — earlier builds, whose snapshot still exposes
124
- * `chat` (and which `resolveOutlineFlow` prefers when it is present).
117
+ * ```js
118
+ * ctx.uiConversation.binding(binding).target('chat') // { getSnapshot, subscribe }
119
+ * ```
125
120
  *
126
- * Reading the provider off `binding.ctx` rather than the root context keeps it
127
- * correct under any bus arrangement, and a host without the service degrades to
128
- * the legacy face instead of crashing the slot.
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.
129
125
  */
130
- function resolveChatFeed(session, binding) {
131
- if (session === void 0 || binding === void 0) return void 0;
132
- const chat = (binding.ctx?.uiConversation)?.binding?.(session)?.target?.("chat");
133
- if (chat !== void 0) return chat;
134
- return session;
126
+ function resolveChatFeed(binding) {
127
+ return (binding?.ctx?.uiConversation)?.binding?.(binding)?.target?.("chat");
135
128
  }
136
129
  const JUMP_HEADROOM = 96;
137
130
  const JUMP_TIMEOUT_MS = 1500;
@@ -176,12 +169,9 @@ window.__ModuleLoader__.load({
176
169
  }
177
170
  function OutlinePanel({ sessions, useSessions, t }) {
178
171
  const current = useSessions((s) => s.current);
179
- const session = current ? sessions.binding(current)?.session : void 0;
180
- const chat = (0, react.useMemo)(() => resolveChatFeed(session, current ? sessions.binding(current) : void 0), [
181
- session,
182
- sessions,
183
- current
184
- ]);
172
+ const binding = (0, react.useMemo)(() => current ? sessions.binding(current) : void 0, [sessions, current]);
173
+ const session = binding?.session;
174
+ const chat = (0, react.useMemo)(() => resolveChatFeed(binding), [binding]);
185
175
  const chatSnapshot = (0, react.useSyncExternalStore)((0, react.useCallback)((onChange) => chat ? chat.subscribe(onChange) : () => {}, [chat]), (0, react.useCallback)(() => chat ? chat.getSnapshot() : NO_FLOW, [chat]));
186
176
  const snapshot = (0, react.useMemo)(() => ({
187
177
  ...chatSnapshot,
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 one snapshot into the flow to walk, preferring the nested `chat`\n * object and falling back to the top-level store fields.\n *\n * Never throws: a snapshot carrying neither shape 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 const chat = snapshot.chat;\n if (chat !== undefined && Array.isArray(chat.order) && chat.nodes !== undefined) {\n return { order: chat.order, nodes: chat.nodes };\n }\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 for the two\n * contract generations DSH shipped:\n *\n * - `binding.ctx.uiConversation.binding(binding).target('chat')` — 0.1.5-rc.2\n * and later, where the Chat node graph is a session-scoped store and\n * `ConversationSnapshot` no longer carries `chat`;\n * - the session face itself — earlier builds, whose snapshot still exposes\n * `chat` (and which `resolveOutlineFlow` prefers when it is present).\n *\n * Reading the provider off `binding.ctx` rather than the root context keeps it\n * correct under any bus arrangement, and a host without the service degrades to\n * the legacy face instead of crashing the slot.\n */\nexport function resolveChatFeed(session, binding) {\n if (session === undefined || binding === undefined)\n return undefined;\n const uiConversation = binding.ctx?.uiConversation;\n const chat = uiConversation?.binding?.(session)?.target?.('chat');\n if (chat !== undefined)\n return chat;\n // Legacy fallback: on builds whose ConversationSnapshot still nests `chat`,\n // the session face itself is the observable carrying it. The cast is\n // deliberate — that generation is not describable by this build's types, and\n // `resolveOutlineFlow` reads whatever shape it finds defensively.\n return session;\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, useSessions, t, }) {\n const current = useSessions((s) => s.current);\n const session = current ? sessions.binding(current)?.session : undefined;\n // Question flow: the Chat store (0.1.5-rc.2+) or the session snapshot's own\n // `chat` (earlier builds). Pagination still lives on the session face in both.\n const chat = useMemo(() => resolveChatFeed(session, current ? sessions.binding(current) : undefined), [session, sessions, current]);\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/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 const uiConversation = binding?.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, useSessions, t, }) {\n const current = useSessions((s) => s.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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"}
@@ -32,20 +32,21 @@ export interface OutlineChatFeed {
32
32
  subscribe: (onChange: () => void) => () => void;
33
33
  }
34
34
  /**
35
- * Resolve one observable Chat source out of a session binding for the two
36
- * contract generations DSH shipped:
35
+ * Resolve one observable Chat source out of a session binding.
37
36
  *
38
- * - `binding.ctx.uiConversation.binding(binding).target('chat')` 0.1.5-rc.2
39
- * and later, where the Chat node graph is a session-scoped store and
40
- * `ConversationSnapshot` no longer carries `chat`;
41
- * - the session face itself — earlier builds, whose snapshot still exposes
42
- * `chat` (and which `resolveOutlineFlow` prefers when it is present).
37
+ * The Chat node graph is a session-scoped store, reached exactly the way
38
+ * `@deepseek-ai/dsh-client-ui-chat` reaches it:
43
39
  *
44
- * Reading the provider off `binding.ctx` rather than the root context keeps it
45
- * correct under any bus arrangement, and a host without the service degrades to
46
- * the legacy face instead of crashing the slot.
40
+ * ```js
41
+ * ctx.uiConversation.binding(binding).target('chat') // { getSnapshot, subscribe }
42
+ * ```
43
+ *
44
+ * Reading the provider off `binding.ctx` (rather than the root context) keeps
45
+ * this correct under any bus arrangement; a host that does not offer the
46
+ * service yields `undefined`, and the rail then renders nothing instead of
47
+ * crashing the slot.
47
48
  */
48
- export declare function resolveChatFeed(session: OutlineChatFeed | undefined, binding: {
49
+ export declare function resolveChatFeed(binding: {
49
50
  ctx?: {
50
51
  uiConversation?: unknown;
51
52
  };
@@ -41,7 +41,7 @@ export interface OutlineChatNode {
41
41
  };
42
42
  };
43
43
  }
44
- /** One Chat target: the node flow and its index, nested under `chat`. */
44
+ /** One Chat target: the part of its snapshot the outline reads. */
45
45
  export interface OutlineChatLike {
46
46
  order: readonly string[];
47
47
  nodes: {
@@ -49,23 +49,21 @@ export interface OutlineChatLike {
49
49
  };
50
50
  }
51
51
  /**
52
- * Snapshot shape accepted by collectQuestions.
52
+ * Snapshot shape accepted by collectQuestions: the Chat store's own snapshot.
53
53
  *
54
- * Two generations of the DSH contract are accepted, because the Chat node graph
55
- * moved between them:
54
+ * Since DSH 0.1.5-rc.2 the Chat node graph is a session-scoped store reached
55
+ * through `uiConversation.binding(session).target('chat')`, and its snapshot
56
+ * carries `order` / `nodes` at the top level (see `EMPTY_CHAT_SNAPSHOT` in
57
+ * `@deepseek-ai/dsh-client-ui-chat`). It no longer lives on
58
+ * `ConversationSnapshot` — reading `snapshot.chat` there is what crashed the
59
+ * slot on 0.1.5 hosts.
56
60
  *
57
- * - `chat.order` / `chat.nodes` the layout this plugin was written against,
58
- * still what `@deepseek-ai/dsh-client-runtime` exposes as
59
- * `ConversationSnapshot.chat`;
60
- * - `order` / `nodes` at the top level the session-scoped Chat store that
61
- * `@deepseek-ai/dsh-client-ui-chat` publishes from 0.1.5-rc.2 on, where
62
- * `ConversationSnapshot` no longer carries `chat` at all.
63
- *
64
- * Both are optional so "this session has no chat yet" is a valid input rather
65
- * than a crash: the rail then has nothing to outline.
61
+ * `order` and `nodes` are optional so "the store has not produced a flow yet"
62
+ * is a valid input rather than a crash: the rail then has nothing to outline.
63
+ * `hasMore` / `loadingOlder` are merged in by the caller from the session face,
64
+ * which is where the 0.1.5 contract keeps history pagination.
66
65
  */
67
66
  export interface OutlineSnapshotLike {
68
- chat?: Partial<OutlineChatLike> | undefined;
69
67
  order?: readonly string[] | undefined;
70
68
  nodes?: {
71
69
  get(key: string): OutlineChatNode | undefined;
@@ -75,7 +73,7 @@ export interface OutlineSnapshotLike {
75
73
  /** The session face is paging older history right now. */
76
74
  loadingOlder?: boolean | undefined;
77
75
  }
78
- /** The node flow plus its index, after resolving both contract generations. */
76
+ /** The node flow plus its index, after validating the store snapshot. */
79
77
  export interface OutlineFlow {
80
78
  order: readonly string[];
81
79
  nodes: {
@@ -83,10 +81,10 @@ export interface OutlineFlow {
83
81
  };
84
82
  }
85
83
  /**
86
- * Resolve one snapshot into the flow to walk, preferring the nested `chat`
87
- * object and falling back to the top-level store fields.
84
+ * Resolve a Chat store snapshot into the flow to walk.
88
85
  *
89
- * Never throws: a snapshot carrying neither shape yields `undefined`, so the
86
+ * Never throws: a snapshot that has not produced `order` + `nodes` yet (or a
87
+ * host that handed over something else entirely) yields `undefined`, so the
90
88
  * caller renders an empty rail instead of crashing its slot.
91
89
  */
92
90
  export declare function resolveOutlineFlow(snapshot: OutlineSnapshotLike | null | undefined): OutlineFlow | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chestnut23/dsh-conversation-outline",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",
@@ -34,7 +34,9 @@
34
34
  "inject": [
35
35
  "@deepseek-ai/dsh-client-runtime",
36
36
  "@deepseek-ai/dsh-client-locale",
37
- "@deepseek-ai/dsh-client-ui-layout"
37
+ "@deepseek-ai/dsh-client-ui-layout",
38
+ "@deepseek-ai/dsh-client-ui-chat",
39
+ "@deepseek-ai/dsh-client-ui-conversation"
38
40
  ],
39
41
  "platform": "web"
40
42
  }