dsh-sidenote 0.7.2 → 0.7.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
@@ -23,9 +23,9 @@
23
23
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-60708E" alt="MIT license"></a>
24
24
  </p>
25
25
 
26
- ![新版浅色界面:左侧保留主对话,右侧深入讨论,结论回到主输入框](docs/assets/workflow.jpg)
26
+ ![划选文字后可添加到对话或在侧边提问;同屏展示侧聊回答、待发送的批注与回流标签,以及已发送消息中的上下文气泡](docs/assets/workflow.jpg)
27
27
 
28
- <p align="center"><sub>dsh-sidenote 0.6.0 · DSH 0.2.0-rc.2 · 原生侧栏 · 浅色主题</sub></p>
28
+ <p align="center"><sub>dsh-sidenote 0.7.2 · DSH 0.2.1-rc.2 · DeepSeek V4.1 Flash · 真实交互录制</sub></p>
29
29
 
30
30
  ## 安装
31
31
 
@@ -51,7 +51,7 @@ dsh plugin --profile web add dsh-sidenote
51
51
 
52
52
  划选主对话或侧聊中的一段回复,写下问题或修改意见。**原文和注释一起保留**,可累积多条,发送前预览、移除,正文草稿照常编辑。
53
53
 
54
- 读到「没有结果时,引导用户调整搜索词」,就选中这一句,追问「这里如何设计才不会让用户卡住?」。
54
+ 读到「提示“未找到”,并给出放宽条件建议」,就选中这一句,追问「给一版可直接使用的空状态」。
55
55
 
56
56
  ### 有结论,带回正在做的事
57
57
 
@@ -61,11 +61,11 @@ dsh plugin --profile web add dsh-sidenote
61
61
 
62
62
  **划选一句 → 带着上下文追问 → 把结论带回主线。**
63
63
 
64
- ![浅色主题操作录屏:划选原文、添加问题、发送到侧边聊天、回流结论](docs/assets/demo.gif)
64
+ ![真实 Flash 演示:划选批注、悬浮查看与编辑、侧聊追问、图标回流,以及发送后的气泡内引用](docs/assets/demo.gif)
65
65
 
66
66
  [观看清晰版 MP4](docs/assets/demo.mp4) · [查看静态截图](docs/assets/workflow.jpg)
67
67
 
68
- <sub>以上素材重新录制于 dsh-sidenote 0.6.0 / DSH 0.2.0-rc.2。使用隔离环境中的示例对话与本地演示模型,展示真实界面操作;内容与回答为合成示例。</sub>
68
+ <sub>2026-10-25 录制于 dsh-sidenote 0.7.2 / DSH 0.2.1-rc.2。以笔记搜索设计为例,真实调用官方 DeepSeek V4.1 Flash 生成回答;约 43 秒的录屏展示实际操作与完整回复过程。<a href="docs/assets/README.md">素材说明</a></sub>
69
69
 
70
70
  ## 用起来顺手的细节
71
71
 
@@ -1,23 +1,38 @@
1
- # README media / 首页素材
1
+ # README and storefront media / 首页与商店素材
2
2
 
3
- The screenshots and recording used by the README were captured on **2026-10-18** with **dsh-sidenote 0.6.0** (source: `7ed85e4`) and **DSH 0.2.0-rc.2**, using the native right sidebar without better-sidebar. The host's active bundle inventory and the mounted plugin's build hash were checked before capture.
3
+ Captured on **2026-10-25** with the published **dsh-sidenote 0.7.2** package (source: `0d55fcd`) and **DSH 0.2.1-rc.2**, using native Conversation and the native right sidebar. The mounted plugin's version and bundle digest, plus the host's active conversation, composer, and sidebar packages, were checked before capture.
4
4
 
5
- 首页截图与录屏于 **2026-10-18** 重新采集,使用 **dsh-sidenote 0.6.0**(源码 `7ed85e4`)与 **DSH 0.2.0-rc.2** 原生右侧栏,不安装 better-sidebar。采集前核对了宿主实际激活的组件版本与插件构建摘要。
5
+ 素材于 **2026-10-25** 重新录制,使用已发布的 **dsh-sidenote 0.7.2**(源码 `0d55fcd`)与 **DSH 0.2.1-rc.2** 原生对话和右侧栏。录制前核对了实际挂载的插件版本与构建摘要,以及宿主正在运行的对话、输入框和侧栏组件版本。
6
6
 
7
- - `workflow.jpg`: the main and side conversations with a returned answer / 主、侧对话与回流结论。
8
- - `selection.jpg`: a selected passage and its note editor / 划选与注释编辑器。
9
- - `sidechat.jpg`: a completed side-chat response / 侧聊问答。
10
- - `demo.gif`, `demo.mp4`: the same continuous selection → annotation → side reply → reflow sequence / 同一段连续操作的 GIF 与 MP4。
11
- - `hero.svg`, `hero-en.svg` and their `-compact.svg` variants: original vector covers; regenerate with `python3 scripts/build-brand-assets.py` / 原创矢量封面,可用该命令重新生成。
7
+ ## Real model responses / 真实模型回答
12
8
 
13
- The host uses its **light theme** and a 16 px conversation font. Screenshots show the real interface; they are not UI mockups. The recording preserves browser frame timing. A browser translation extension's floating control was temporarily hidden during capture and restored afterwards. No DSH controls were altered.
9
+ The example task is designing a note app's search experience. All assistant replies were generated through the official **DeepSeek V4.1 Flash** API at `api.deepseek.com`, using model ID `deepseek-flash` with thinking off. This ID's mapping is documented in the [official model updates](https://api-docs.deepseek.com/updates/). Request metadata confirmed successful, completed responses. No fixed-answer adapter or prewritten assistant messages were used.
14
10
 
15
- 宿主使用**浅色主题**与 16 px 会话字号。截图来自真实界面,录屏保留浏览器帧的时间间隔。录制期间临时隐藏了浏览器翻译扩展的浮动控件,随后恢复;没有修改 DSH 控件。
11
+ 演示任务是设计笔记应用的搜索体验。全部助手回复都实际调用 `api.deepseek.com` 的 **DeepSeek V4.1 Flash** 生成,模型 ID 为 `deepseek-flash`,关闭思考模式;该 ID 的对应关系见[官方更新说明](https://api-docs.deepseek.com/updates/)。请求元数据确认调用成功并完成,没有使用固定回答的适配器或预先写入的助手消息。
16
12
 
17
- Conversation content is a synthetic note-search design example. A local demo adapter supplies fixed answers through the host's LLM interface; the fork, annotation, submission, and reflow paths are real. This illustrates the workflow, not model answer quality or latency. No private sessions or external model credentials are used.
13
+ The recording starts with the real main-chat answer already visible. It then shows selection and annotation, a real side-chat response, hover previews, editing a pending note, the clear button's reveal, the reply-toolbar handback action, sending the main draft with both kinds of context, and inspecting the sent message. The main model's final answer incorporates the returned empty-state copy and the edited annotation.
18
14
 
19
- 对话内容是合成的笔记搜索设计示例。本地演示模型通过宿主 LLM 接口提供固定回答;fork、注释、发送与回流均执行真实流程。素材用于说明操作,不代表模型回答质量或响应速度,也不使用私人会话或外部模型凭证。
15
+ 录屏从已经生成的主线回答开始,随后展示划选与批注、真实侧聊回答、悬浮预览、原位编辑、清除按钮显现、回复操作栏回流、携带两类上下文发送,以及发送后的气泡详情。主线最后的真实回答同时采纳了带回的空状态文案与修改后的批注。
20
16
 
21
- For future captures, use a fresh isolated DSH home and a verified host version, confirm the mounted bundle, select the light theme, and repeat the complete workflow. The older `capture-demo.mjs` and `capture-video.mjs` scripts target historical BS layouts and do not reproduce these native-sidebar assets unchanged.
17
+ ## Assets / 文件
22
18
 
23
- 以后重录时,使用全新隔离的 DSH home 和已验证宿主版本,核对实际挂载构建,选择浅色主题,并走完完整流程。旧 `capture-demo.mjs`、`capture-video.mjs` 面向历史 BS 布局,不能原样用于复现这组原生侧栏素材。
19
+ - `workflow.jpg`: a follow-up capture showing the selection toolbar's two destinations, a real side-chat answer, pending annotation and handback chips, and context bubbles in sent messages / 补拍主图:同屏展示划选菜单的两种去向、真实侧聊回答、待发送的批注与回流标签,以及已发送消息中的上下文气泡。
20
+ - `context-preview.jpg`: a compact preview with the selected passage, editable note, and horizontal context chips / 紧凑悬浮层中的原文、可编辑注解与横向标签。
21
+ - `selection.jpg`: selected text and the two annotation destinations, taken from the recording / 从录屏提取的划选画面与两种批注去向。
22
+ - `sidechat.jpg`: a real side-chat answer and the hovered reply-toolbar handback icon / 真实侧聊回答与悬停中的回流图标。
23
+ - `demo.gif`, `demo.mp4`: the same approximately **43-second** continuous workflow / 同一段约 **43 秒**的连续操作。
24
+ - `hero.svg`, `hero-en.svg` and their `-compact.svg` variants: vector brand covers; regenerate with `python3 scripts/build-brand-assets.py` / 品牌矢量封面,可用该命令重新生成。
25
+
26
+ ## Capture and distribution / 录制与展示
27
+
28
+ The real interface uses the host's **light theme** and **18 px** conversation font. The hero uses a **1440 × 780** viewport and is exported at 2160 × 1170 so the selection menu and both composers fit together. Other stills use a **1440 × 900** viewport, captured at 2× pixel density and exported at 2160 × 1350; the selection still is a native 1440 × 900 recording frame. MP4 is 1440 × 900 at 30 fps; GIF is 1120 × 700 at 12 fps. Encoding preserves the browser's frame timing without speeding up responses. A small pointer overlay follows actual mouse events in the recording; product controls and model replies are not redrawn or substituted.
29
+
30
+ 真实界面使用宿主的**浅色主题**和 **18 px** 会话字号。主图以 **1440 × 780** 视口补拍,导出为 2160 × 1170,让划选菜单和两侧输入框完整同屏。其余静态截图使用 **1440 × 900** 视口,按 2 倍像素密度采集后导出为 2160 × 1350;划选截图直接取自 1440 × 900 的录屏帧。MP4 为 1440 × 900 / 30 fps,GIF 为 1120 × 700 / 12 fps,均按浏览器帧时间编码,没有加速模型响应。录屏中仅添加跟随真实鼠标事件的小光标辅助层,没有重绘产品控件或替换模型回答。
31
+
32
+ [`screenshots.json`](../../screenshots.json) declares the storefront gallery. The [catalog's screenshot convention](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/contributing.md#screenshots--截图optional-recommended--可选推荐) resolves those paths from this repository, and the next catalog build refreshes the gallery. Existing screenshot paths remain available for cached catalogs.
33
+
34
+ [`screenshots.json`](../../screenshots.json) 声明商店展示顺序。[商店截图约定](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/contributing.md#screenshots--截图optional-recommended--可选推荐) 会从本仓库读取这些路径,下一次索引构建时更新图集;原有图片路径继续提供新版内容,供已缓存的索引读取。
35
+
36
+ For future captures, use a fresh isolated DSH home, install the published plugin with the official CLI, verify the active bundles, and run the complete workflow against the official model API. Keep credentials, launch tokens, private conversations, raw frames, and recording logs out of public assets. The older `capture-demo.mjs` and `capture-video.mjs` scripts target historical layouts and synthetic sessions; they do not reproduce this recording unchanged.
37
+
38
+ 后续重录应使用全新隔离的 DSH home,通过官方 CLI 安装已发布插件,核对实际加载的构建,并调用官方模型走完完整流程。凭证、启动 token、私人会话、原始帧和录制日志不进入公开素材。旧 `capture-demo.mjs`、`capture-video.mjs` 面向历史布局与合成会话,不能原样用于复现本次录制。
package/lib/client.js CHANGED
@@ -308,6 +308,17 @@ window.__ModuleLoader__.load({
308
308
  function conversationViewOf(node) {
309
309
  return node.closest("[data-conversation-session], [data-sidenote-chat-session]") ?? document.body;
310
310
  }
311
+ /** Find the last rendered member of an explicit history range, not the side-chat tail. */
312
+ function lastConversationEntry(root, keys) {
313
+ return [...root.querySelectorAll("[data-chat-flow-key]")].reverse().find((row) => keys.has(row.dataset.chatFlowKey ?? "") && row.getBoundingClientRect().height > 0);
314
+ }
315
+ /** Land above the native composer, without scrolling another pane or moving focus. */
316
+ function revealConversationEntryEnd(row) {
317
+ const scroller = row.closest("[data-conversation-scroll]");
318
+ if (!scroller) return;
319
+ const clip = quoteClipOf(row, conversationViewOf(row));
320
+ scroller.scrollTop += row.getBoundingClientRect().bottom - clip.bottom + 12;
321
+ }
311
322
  function hasConversationMessages() {
312
323
  return document.querySelector("[data-chat-flow-kind]") !== null;
313
324
  }
@@ -611,7 +622,7 @@ window.__ModuleLoader__.load({
611
622
  let dockRegistered = false;
612
623
  let report = {
613
624
  versions: {
614
- sidenote: "0.7.2",
625
+ sidenote: "0.7.4",
615
626
  dsh: "unknown",
616
627
  betterSidebar: null
617
628
  },
@@ -1262,15 +1273,15 @@ window.__ModuleLoader__.load({
1262
1273
  document.head.appendChild(tag);
1263
1274
  }
1264
1275
  var diagnostics_module_css_default = {
1265
- "versions": "tGe0LW_versions",
1266
- "trigger": "tGe0LW_trigger",
1267
- "actions": "tGe0LW_actions",
1268
- "panel": "tGe0LW_panel",
1276
+ "description": "tGe0LW_description",
1269
1277
  "settingTitle": "tGe0LW_settingTitle",
1270
1278
  "setting": "tGe0LW_setting",
1279
+ "list": "tGe0LW_list",
1280
+ "actions": "tGe0LW_actions",
1281
+ "panel": "tGe0LW_panel",
1271
1282
  "header": "tGe0LW_header",
1272
- "description": "tGe0LW_description",
1273
- "list": "tGe0LW_list"
1283
+ "versions": "tGe0LW_versions",
1284
+ "trigger": "tGe0LW_trigger"
1274
1285
  };
1275
1286
  //#endregion
1276
1287
  //#region src/client/diagnostics/index.tsx
@@ -1855,17 +1866,19 @@ window.__ModuleLoader__.load({
1855
1866
  const sideChatBridge = { current: null };
1856
1867
  //#endregion
1857
1868
  //#region src/client/chat/transcript.ts
1858
- /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
1869
+ /** Protocol readers need actual text, not attachment preview labels before it.
1870
+ * Native Conversation renders images; only an image-only message needs a label. */
1859
1871
  function contentTextOf(content) {
1860
1872
  if (!Array.isArray(content)) return "";
1861
1873
  const parts = [];
1874
+ let hasImage = false;
1862
1875
  for (const block of content) {
1863
1876
  if (typeof block !== "object" || block === null) continue;
1864
1877
  const b = block;
1865
1878
  if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
1866
- else if (b.type === "image") parts.push(t("imagePlaceholder"));
1879
+ else if (b.type === "image") hasImage = true;
1867
1880
  }
1868
- return parts.join("\n");
1881
+ return parts.join("\n") || (hasImage ? t("imagePlaceholder") : "");
1869
1882
  }
1870
1883
  function seqKey(prefix, node) {
1871
1884
  return `${prefix}:${typeof node.seq === "number" ? node.seq : "?"}`;
@@ -2321,7 +2334,7 @@ window.__ModuleLoader__.load({
2321
2334
  }
2322
2335
  //#endregion
2323
2336
  //#region \0dsh-css:/home/runner/work/dsh-sidenote/dsh-sidenote/src/client/sidechat/sidechat.module.css.mjs
2324
- const css$2 = "._5KEEsG_nativeHost,._5KEEsG_root,._5KEEsG_nativeConversation{flex-direction:column;min-height:0;display:flex}._5KEEsG_nativeHost,._5KEEsG_root{height:100%}._5KEEsG_root{color:var(--dsw-alias-label-primary)}._5KEEsG_nativeConversation{flex:1}._5KEEsG_chatView{flex-direction:column;flex:1 0 auto;min-width:0;display:flex}._5KEEsG_mainContext{flex:none;padding:12px 24px 4px}._5KEEsG_historyToggle{align-items:center;gap:6px;display:flex}._5KEEsG_contextHint{color:var(--dsw-alias-label-tertiary);font-size:12px}._5KEEsG_contextChevron{transform:rotate(-90deg)}._5KEEsG_contextChevron,._5KEEsG_contextChevronOpen{flex:none}._5KEEsG_progressEntries{max-height:240px;margin:8px 6px;overflow:auto}._5KEEsG_progressEntry{white-space:pre-wrap;overflow-wrap:anywhere;margin:10px 0;font-size:13px;line-height:1.6}._5KEEsG_root [data-sidenote-progress-snapshot],._5KEEsG_root[data-sidenote-context-pending] [data-submission-echo]{display:none}._5KEEsG_reflowAction{width:32px;height:32px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:8px;flex:none;order:1;justify-content:center;align-items:center;margin-left:auto;padding:0;display:inline-flex}._5KEEsG_reflowAction:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_reflowAction:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}._5KEEsG_reflowAction:disabled{cursor:default;color:var(--dsw-alias-state-business-primary);background:0 0}._5KEEsG_nativeToolbar{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:center;gap:8px;padding:8px 12px;display:flex}._5KEEsG_nativeToolbarActions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._5KEEsG_nativeAction,._5KEEsG_historyToggle{color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0;border-radius:6px;padding:4px 6px;font-size:13px}._5KEEsG_nativeAction:hover,._5KEEsG_nativeAction:focus-visible,._5KEEsG_historyToggle:hover,._5KEEsG_historyToggle:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_nativeAction:disabled{opacity:.5;cursor:default}._5KEEsG_historyToggle{text-align:left;margin:0}._5KEEsG_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}._5KEEsG_emptyIcon{color:var(--dsw-alias-label-tertiary)}._5KEEsG_emptyTitle{font-size:14px;font-weight:600}._5KEEsG_emptyText{color:var(--dsw-alias-label-secondary);max-width:280px;font-size:13px;line-height:1.6}._5KEEsG_stateDetail,._5KEEsG_errorRow{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:13px;line-height:1.6}._5KEEsG_errorRow{padding:8px 12px}._5KEEsG_headerSide{color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border:0;border-radius:6px;align-items:center;gap:4px;padding:4px 8px;display:inline-flex}._5KEEsG_headerSide:hover,._5KEEsG_headerSide:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_headerSide:disabled{opacity:.4;cursor:default}._5KEEsG_visibilitySetting{justify-content:space-between;align-items:center;gap:16px;padding:8px 0;display:flex}._5KEEsG_visibilitySetting p{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}";
2337
+ const css$2 = "._5KEEsG_nativeHost,._5KEEsG_root,._5KEEsG_nativeConversation{flex-direction:column;min-height:0;display:flex}._5KEEsG_nativeHost,._5KEEsG_root{height:100%}._5KEEsG_root{color:var(--dsw-alias-label-primary)}._5KEEsG_nativeConversation{flex:1}._5KEEsG_chatView{flex-direction:column;flex:1 0 auto;min-width:0;display:flex}._5KEEsG_historyView{display:contents}._5KEEsG_mainContext{flex:none;padding:12px 24px 4px}._5KEEsG_historyToggle{align-items:center;gap:6px;display:flex}._5KEEsG_contextHint{color:var(--dsw-alias-label-tertiary);font-size:12px}._5KEEsG_contextChevron{transform:rotate(-90deg)}._5KEEsG_contextChevron,._5KEEsG_contextChevronOpen{flex:none}._5KEEsG_progressEntries{max-height:240px;margin:8px 6px;overflow:auto}._5KEEsG_progressEntry{white-space:pre-wrap;overflow-wrap:anywhere;margin:10px 0;font-size:13px;line-height:1.6}._5KEEsG_root [data-sidenote-progress-snapshot],._5KEEsG_root[data-sidenote-context-pending] [data-submission-echo]{display:none}._5KEEsG_reflowAction{width:32px;height:32px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:8px;flex:none;order:1;justify-content:center;align-items:center;margin-left:auto;padding:0;display:inline-flex}._5KEEsG_reflowAction:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_reflowAction:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}._5KEEsG_reflowAction:disabled{cursor:default;color:var(--dsw-alias-state-business-primary);background:0 0}._5KEEsG_nativeToolbar{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:center;gap:8px;padding:8px 12px;display:flex}._5KEEsG_nativeToolbarActions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._5KEEsG_nativeAction,._5KEEsG_historyToggle{color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0;border-radius:6px;padding:4px 6px;font-size:13px}._5KEEsG_nativeAction:hover,._5KEEsG_nativeAction:focus-visible,._5KEEsG_historyToggle:hover,._5KEEsG_historyToggle:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_nativeAction:disabled{opacity:.5;cursor:default}._5KEEsG_historyToggle{text-align:left;margin:0}._5KEEsG_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}._5KEEsG_emptyIcon{color:var(--dsw-alias-label-tertiary)}._5KEEsG_emptyTitle{font-size:14px;font-weight:600}._5KEEsG_emptyText{color:var(--dsw-alias-label-secondary);max-width:280px;font-size:13px;line-height:1.6}._5KEEsG_stateDetail,._5KEEsG_errorRow{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:13px;line-height:1.6}._5KEEsG_errorRow{padding:8px 12px}._5KEEsG_headerSide{color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;background:0 0;border:0;border-radius:6px;align-items:center;gap:4px;padding:4px 8px;display:inline-flex}._5KEEsG_headerSide:hover,._5KEEsG_headerSide:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_headerSide:disabled{opacity:.4;cursor:default}._5KEEsG_visibilitySetting{justify-content:space-between;align-items:center;gap:16px;padding:8px 0;display:flex}._5KEEsG_visibilitySetting p{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}";
2325
2338
  const tagId$2 = "dsh-sidenote/sidechat.module.css";
2326
2339
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
2327
2340
  const tag = document.createElement("style");
@@ -2331,29 +2344,30 @@ window.__ModuleLoader__.load({
2331
2344
  document.head.appendChild(tag);
2332
2345
  }
2333
2346
  var sidechat_module_css_default = {
2334
- "emptyText": "_5KEEsG_emptyText",
2335
- "headerSide": "_5KEEsG_headerSide",
2336
2347
  "nativeConversation": "_5KEEsG_nativeConversation",
2337
- "errorRow": "_5KEEsG_errorRow",
2338
- "nativeAction": "_5KEEsG_nativeAction",
2339
- "stateDetail": "_5KEEsG_stateDetail",
2348
+ "emptyIcon": "_5KEEsG_emptyIcon",
2340
2349
  "progressEntries": "_5KEEsG_progressEntries",
2341
- "contextChevron": "_5KEEsG_contextChevron",
2342
2350
  "nativeToolbar": "_5KEEsG_nativeToolbar",
2343
- "mainContext": "_5KEEsG_mainContext",
2351
+ "nativeAction": "_5KEEsG_nativeAction",
2352
+ "emptyText": "_5KEEsG_emptyText",
2353
+ "stateDetail": "_5KEEsG_stateDetail",
2354
+ "errorRow": "_5KEEsG_errorRow",
2355
+ "chatView": "_5KEEsG_chatView",
2356
+ "contextChevronOpen": "_5KEEsG_contextChevronOpen",
2344
2357
  "root": "_5KEEsG_root",
2345
- "nativeHost": "_5KEEsG_nativeHost",
2346
- "contextHint": "_5KEEsG_contextHint",
2347
- "visibilitySetting": "_5KEEsG_visibilitySetting",
2348
- "progressEntry": "_5KEEsG_progressEntry",
2349
2358
  "reflowAction": "_5KEEsG_reflowAction",
2350
- "emptyIcon": "_5KEEsG_emptyIcon",
2359
+ "historyView": "_5KEEsG_historyView",
2360
+ "mainContext": "_5KEEsG_mainContext",
2361
+ "nativeHost": "_5KEEsG_nativeHost",
2351
2362
  "nativeToolbarActions": "_5KEEsG_nativeToolbarActions",
2352
- "contextChevronOpen": "_5KEEsG_contextChevronOpen",
2353
- "stateScreen": "_5KEEsG_stateScreen",
2354
2363
  "emptyTitle": "_5KEEsG_emptyTitle",
2364
+ "stateScreen": "_5KEEsG_stateScreen",
2365
+ "progressEntry": "_5KEEsG_progressEntry",
2366
+ "contextHint": "_5KEEsG_contextHint",
2367
+ "contextChevron": "_5KEEsG_contextChevron",
2355
2368
  "historyToggle": "_5KEEsG_historyToggle",
2356
- "chatView": "_5KEEsG_chatView"
2369
+ "headerSide": "_5KEEsG_headerSide",
2370
+ "visibilitySetting": "_5KEEsG_visibilitySetting"
2357
2371
  };
2358
2372
  //#endregion
2359
2373
  //#region src/client/sidechat/NativeConversation.tsx
@@ -2397,6 +2411,7 @@ window.__ModuleLoader__.load({
2397
2411
  ]
2398
2412
  }), expanded && side.progress && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2399
2413
  className: sidechat_module_css_default.progressEntries,
2414
+ "data-sidenote-context-progress": true,
2400
2415
  children: side.progress.entries.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2401
2416
  className: sidechat_module_css_default.progressEntry,
2402
2417
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2492,6 +2507,8 @@ window.__ModuleLoader__.load({
2492
2507
  });
2493
2508
  }
2494
2509
  function HistoryView({ Original, props, side }) {
2510
+ const root = (0, react.useRef)(null);
2511
+ const wasExpanded = (0, react.useRef)(side.contextExpanded);
2495
2512
  const nodes = props.useChat((state) => state.nodes);
2496
2513
  const snapshot = props.useConversation((state) => state);
2497
2514
  const groups = snapshot.views.grouped("chat");
@@ -2517,9 +2534,31 @@ window.__ModuleLoader__.load({
2517
2534
  }
2518
2535
  };
2519
2536
  const useProjected = (0, react.useCallback)((selector) => selector(projected), [projected]);
2520
- return (0, react.createElement)(Original, {
2521
- ...props,
2522
- useConversation: useProjected
2537
+ (0, react.useLayoutEffect)(() => {
2538
+ const opening = side.contextExpanded && !wasExpanded.current;
2539
+ wasExpanded.current = side.contextExpanded;
2540
+ if (!opening) return;
2541
+ const keys = new Set(entries.filter(isInherited).map((entry) => entry.key));
2542
+ let frame = requestAnimationFrame(() => {
2543
+ frame = requestAnimationFrame(() => {
2544
+ if (!root.current) return;
2545
+ const progress = root.current.closest("[data-sidenote-transcript]")?.querySelector("[data-sidenote-context-progress]");
2546
+ if (progress) progress.scrollTop = progress.scrollHeight;
2547
+ const target = progress ?? lastConversationEntry(root.current, keys);
2548
+ if (target) revealConversationEntryEnd(target);
2549
+ });
2550
+ });
2551
+ return () => {
2552
+ cancelAnimationFrame(frame);
2553
+ };
2554
+ }, [side.contextExpanded]);
2555
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2556
+ ref: root,
2557
+ className: sidechat_module_css_default.historyView,
2558
+ children: (0, react.createElement)(Original, {
2559
+ ...props,
2560
+ useConversation: useProjected
2561
+ })
2523
2562
  });
2524
2563
  }
2525
2564
  /** Both admitted hosts publish the native chat View; only 0.2.1 adds a Flow slot. */
@@ -5553,34 +5592,34 @@ ${entries.map((entry) => {
5553
5592
  document.head.appendChild(tag);
5554
5593
  }
5555
5594
  var annotate_module_css_default = {
5556
- "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
5557
5595
  "deleteButton": "_SVViG_deleteButton",
5558
- "editorEdit": "_SVViG_editorEdit",
5559
- "hintClose": "_SVViG_hintClose",
5560
- "editorTarget": "_SVViG_editorTarget",
5561
- "sentChipRow": "_SVViG_sentChipRow",
5562
- "editorInput": "_SVViG_editorInput",
5563
- "toolbarButton": "_SVViG_toolbarButton",
5564
- "highlight": "_SVViG_highlight",
5565
- "sentChip": "_SVViG_sentChip",
5566
- "editorTextarea": "_SVViG_editorTextarea",
5567
- "editorNew": "_SVViG_editorNew",
5568
- "toolbarBelow": "_SVViG_toolbarBelow",
5569
5596
  "badgeSent": "_SVViG_badgeSent",
5597
+ "editorSpacer": "_SVViG_editorSpacer",
5598
+ "saveButton": "_SVViG_saveButton",
5570
5599
  "toolbar": "_SVViG_toolbar",
5600
+ "sentChip": "_SVViG_sentChip",
5571
5601
  "editorFooter": "_SVViG_editorFooter",
5572
- "editorAsk": "_SVViG_editorAsk",
5573
- "confirmButton": "_SVViG_confirmButton",
5574
5602
  "hint": "_SVViG_hint",
5575
- "badge": "_SVViG_badge",
5576
- "saveButton": "_SVViG_saveButton",
5603
+ "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
5604
+ "confirmButton": "_SVViG_confirmButton",
5605
+ "toolbarBelow": "_SVViG_toolbarBelow",
5606
+ "editorInput": "_SVViG_editorInput",
5577
5607
  "sentCardNote": "_SVViG_sentCardNote",
5578
- "cancelButton": "_SVViG_cancelButton",
5579
- "editorAskRow": "_SVViG_editorAskRow",
5580
- "editorSpacer": "_SVViG_editorSpacer",
5608
+ "toolbarButton": "_SVViG_toolbarButton",
5609
+ "sentCardQuote": "_SVViG_sentCardQuote",
5610
+ "hintClose": "_SVViG_hintClose",
5611
+ "badge": "_SVViG_badge",
5581
5612
  "editorError": "_SVViG_editorError",
5613
+ "editorTarget": "_SVViG_editorTarget",
5614
+ "highlight": "_SVViG_highlight",
5582
5615
  "sentCardTitle": "_SVViG_sentCardTitle",
5583
- "sentCardQuote": "_SVViG_sentCardQuote"
5616
+ "sentChipRow": "_SVViG_sentChipRow",
5617
+ "editorAskRow": "_SVViG_editorAskRow",
5618
+ "editorNew": "_SVViG_editorNew",
5619
+ "editorEdit": "_SVViG_editorEdit",
5620
+ "editorAsk": "_SVViG_editorAsk",
5621
+ "cancelButton": "_SVViG_cancelButton",
5622
+ "editorTextarea": "_SVViG_editorTextarea"
5584
5623
  };
5585
5624
  //#endregion
5586
5625
  //#region src/client/annotate/hint.tsx
@@ -6393,33 +6432,33 @@ ${entries.map((entry) => {
6393
6432
  document.head.appendChild(tag);
6394
6433
  }
6395
6434
  var annotations_module_css_default = {
6396
- "storageNotice": "SVnebq_storageNotice",
6397
- "contextAction": "SVnebq_contextAction",
6398
- "userContextHost": "SVnebq_userContextHost",
6399
- "chipDismissShade": "SVnebq_chipDismissShade",
6400
- "sentChipGroup": "SVnebq_sentChipGroup",
6401
- "sentChip": "SVnebq_sentChip",
6402
- "contextFooter": "SVnebq_contextFooter",
6403
- "sentContext": "SVnebq_sentContext",
6404
- "sentChevronOpen": "SVnebq_sentChevronOpen",
6405
- "contextPanel": "SVnebq_contextPanel",
6406
- "contextAppear": "SVnebq_contextAppear",
6407
- "sentChevron": "SVnebq_sentChevron",
6435
+ "chipDismiss": "SVnebq_chipDismiss",
6436
+ "chipWrap": "SVnebq_chipWrap",
6437
+ "contextActions": "SVnebq_contextActions",
6408
6438
  "contextEditor": "SVnebq_contextEditor",
6409
- "contextNumber": "SVnebq_contextNumber",
6410
6439
  "contextText": "SVnebq_contextText",
6411
- "sentChips": "SVnebq_sentChips",
6412
- "contextContent": "SVnebq_contextContent",
6440
+ "contextAppear": "SVnebq_contextAppear",
6441
+ "sentContext": "SVnebq_sentContext",
6442
+ "sentChip": "SVnebq_sentChip",
6443
+ "sentChevron": "SVnebq_sentChevron",
6444
+ "sentChevronOpen": "SVnebq_sentChevronOpen",
6445
+ "storageNotice": "SVnebq_storageNotice",
6413
6446
  "sentPreview": "SVnebq_sentPreview",
6414
6447
  "chipMain": "SVnebq_chipMain",
6415
- "contextItem": "SVnebq_contextItem",
6416
- "contextLabel": "SVnebq_contextLabel",
6417
- "chipWrap": "SVnebq_chipWrap",
6418
- "contextActions": "SVnebq_contextActions",
6419
- "contextSource": "SVnebq_contextSource",
6420
6448
  "chipClearAll": "SVnebq_chipClearAll",
6449
+ "userContextHost": "SVnebq_userContextHost",
6450
+ "sentChips": "SVnebq_sentChips",
6451
+ "contextNumber": "SVnebq_contextNumber",
6421
6452
  "chip": "SVnebq_chip",
6422
- "chipDismiss": "SVnebq_chipDismiss",
6453
+ "chipDismissShade": "SVnebq_chipDismissShade",
6454
+ "contextPanel": "SVnebq_contextPanel",
6455
+ "contextAction": "SVnebq_contextAction",
6456
+ "contextItem": "SVnebq_contextItem",
6457
+ "contextFooter": "SVnebq_contextFooter",
6458
+ "sentChipGroup": "SVnebq_sentChipGroup",
6459
+ "contextSource": "SVnebq_contextSource",
6460
+ "contextContent": "SVnebq_contextContent",
6461
+ "contextLabel": "SVnebq_contextLabel",
6423
6462
  "contextList": "SVnebq_contextList"
6424
6463
  };
6425
6464
  //#endregion
@@ -21,7 +21,8 @@ export interface ChatMessage {
21
21
  /** 被打断冻结的 assistant 输出(渲染「已停止」标记)。 */
22
22
  interrupted?: boolean;
23
23
  }
24
- /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
24
+ /** Protocol readers need actual text, not attachment preview labels before it.
25
+ * Native Conversation renders images; only an image-only message needs a label. */
25
26
  export declare function contentTextOf(content: unknown): string;
26
27
  /**
27
28
  * 单个 ConversationNode → ChatMessage;返回 null = 面板不渲染
@@ -32,6 +32,10 @@ export declare function findSendButtonInCard(root?: ParentNode): HTMLButtonEleme
32
32
  export declare function composerSessionId(seat: HTMLElement, fallback: string): string;
33
33
  /** Bound message hover details to their native conversation pane. */
34
34
  export declare function conversationViewOf(node: Element): HTMLElement;
35
+ /** Find the last rendered member of an explicit history range, not the side-chat tail. */
36
+ export declare function lastConversationEntry(root: HTMLElement, keys: ReadonlySet<string>): HTMLElement | undefined;
37
+ /** Land above the native composer, without scrolling another pane or moving focus. */
38
+ export declare function revealConversationEntryEnd(row: HTMLElement): void;
35
39
  export declare function isMainComposerInput(target: EventTarget | null): boolean;
36
40
  export declare function composerHasMenu(seat: HTMLElement): boolean;
37
41
  export declare function hasConversationMessages(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidenote",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Follow the tangent. Keep the thread. Annotate, explore on the side, and bring answers back to DeepSeek Harness. / 思路可以分岔,主线始终向前。划选注释、侧边追问,再把结论带回正在推进的任务。",
5
5
  "icon": "docs/assets/logo.svg",
6
6
  "type": "module",
@@ -31,17 +31,19 @@ interface LooseBlock {
31
31
  name?: unknown
32
32
  }
33
33
 
34
- /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
34
+ /** Protocol readers need actual text, not attachment preview labels before it.
35
+ * Native Conversation renders images; only an image-only message needs a label. */
35
36
  export function contentTextOf(content: unknown): string {
36
37
  if (!Array.isArray(content)) return ''
37
38
  const parts: string[] = []
39
+ let hasImage = false
38
40
  for (const block of content) {
39
41
  if (typeof block !== 'object' || block === null) continue
40
42
  const b = block as LooseBlock
41
43
  if (b.type === 'text' && typeof b.text === 'string') parts.push(b.text)
42
- else if (b.type === 'image') parts.push(t('imagePlaceholder'))
44
+ else if (b.type === 'image') hasImage = true
43
45
  }
44
- return parts.join('\n')
46
+ return parts.join('\n') || (hasImage ? t('imagePlaceholder') : '')
45
47
  }
46
48
 
47
49
  function seqKey(prefix: string, node: Record<string, unknown>): string {
@@ -89,6 +89,20 @@ export function conversationViewOf(node: Element): HTMLElement {
89
89
  return node.closest<HTMLElement>('[data-conversation-session], [data-sidenote-chat-session]') ?? document.body
90
90
  }
91
91
 
92
+ /** Find the last rendered member of an explicit history range, not the side-chat tail. */
93
+ export function lastConversationEntry(root: HTMLElement, keys: ReadonlySet<string>): HTMLElement | undefined {
94
+ return [...root.querySelectorAll<HTMLElement>('[data-chat-flow-key]')].reverse()
95
+ .find(row => keys.has(row.dataset.chatFlowKey ?? '') && row.getBoundingClientRect().height > 0)
96
+ }
97
+
98
+ /** Land above the native composer, without scrolling another pane or moving focus. */
99
+ export function revealConversationEntryEnd(row: HTMLElement): void {
100
+ const scroller = row.closest<HTMLElement>('[data-conversation-scroll]')
101
+ if (!scroller) return
102
+ const clip = quoteClipOf(row, conversationViewOf(row))
103
+ scroller.scrollTop += row.getBoundingClientRect().bottom - clip.bottom + 12
104
+ }
105
+
92
106
 
93
107
  export function isMainComposerInput(target: EventTarget | null): boolean {
94
108
  return target instanceof HTMLElement
@@ -1,8 +1,9 @@
1
- import { createContext, createElement, useCallback, useContext, useMemo, useSyncExternalStore, type ComponentType, type ReactNode } from 'react'
1
+ import { createContext, createElement, useCallback, useContext, useLayoutEffect, useMemo, useRef, useSyncExternalStore, type ComponentType, type ReactNode } from 'react'
2
2
  import type { Context, SessionReference } from '../host/contracts.ts'
3
3
  import { transcriptOf, type ChatMessage } from '../chat/transcript.ts'
4
4
  import { createFoldStore } from '../chat/viewState.ts'
5
5
  import { completedReplySeqsOf, type ChatReplySnapshot } from '../host/chat.ts'
6
+ import { lastConversationEntry, revealConversationEntryEnd } from '../host/dom.ts'
6
7
  import { IconCheckOutline16, IconChevronDownOutline14 } from '../host/icons.ts'
7
8
  import type { SnapshotEntry } from './snapshot.ts'
8
9
  import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
@@ -45,7 +46,7 @@ function ChatView(props: { renderSlot(name: 'conversation.session', owner: { vie
45
46
  <IconChevronDownOutline14 size={12} className={expanded ? css.contextChevronOpen : css.contextChevron} />
46
47
  <span>{t('mainContext')}</span><span className={css.contextHint}>{side.contextPending ? t('contextAtSend') : side.progress?.takenAt ? t('contextAsOf', { time: side.progress.takenAt }) : t('contextInherited')}</span>
47
48
  </button>
48
- {expanded && side.progress && <div className={css.progressEntries}>{side.progress.entries.map((entry, index) => <div key={index} className={css.progressEntry}><span className={css.contextHint}>{entry.kind === 'tool' ? entry.toolName : t(`context${entry.kind}`)}</span><div>{entry.text}</div></div>)}</div>}
49
+ {expanded && side.progress && <div className={css.progressEntries} data-sidenote-context-progress>{side.progress.entries.map((entry, index) => <div key={index} className={css.progressEntry}><span className={css.contextHint}>{entry.kind === 'tool' ? entry.toolName : t(`context${entry.kind}`)}</span><div>{entry.text}</div></div>)}</div>}
49
50
  </div>
50
51
  <SideContext.Provider value={{ ...side, contextExpanded: expanded }}>{props.renderSlot('conversation.session', { view: 'chat' })}</SideContext.Provider>
51
52
  </div>
@@ -107,6 +108,8 @@ interface ViewProps {
107
108
  useConversation<T>(select: (state: ViewSnapshot) => T): T
108
109
  }
109
110
  function HistoryView({ Original, props, side }: { Original: ComponentType<ViewProps>; props: ViewProps; side: SideConversation }) {
111
+ const root = useRef<HTMLDivElement>(null)
112
+ const wasExpanded = useRef(side.contextExpanded)
110
113
  const nodes = props.useChat(state => state.nodes)
111
114
  const snapshot = props.useConversation(state => state)
112
115
  const groups = snapshot.views.grouped('chat')
@@ -125,7 +128,27 @@ function HistoryView({ Original, props, side }: { Original: ComponentType<ViewPr
125
128
  const visible = side.contextExpanded ? entries : entries.filter(entry => !isInherited(entry))
126
129
  const projected = { ...snapshot, views: { ...snapshot.views, grouped: (name: string) => name === 'chat' && groups ? { ...groups, entries: visible } : snapshot.views.grouped(name) } }
127
130
  const useProjected = useCallback(<T,>(selector: (state: ViewSnapshot) => T): T => selector(projected), [projected])
128
- return createElement(Original, { ...props, useConversation: useProjected })
131
+ // Only an explicit expansion navigates. Later stream/layout updates and
132
+ // restored fold state leave the host's reader-position policy in charge.
133
+ useLayoutEffect(() => {
134
+ const opening = side.contextExpanded && !wasExpanded.current
135
+ wasExpanded.current = side.contextExpanded
136
+ if (!opening) return
137
+ const keys = new Set(entries.filter(isInherited).map(entry => entry.key))
138
+ // Native Chat settles its content resize first; navigate in the next frame
139
+ // so its initial tail restoration cannot overwrite this explicit action.
140
+ let frame = requestAnimationFrame(() => {
141
+ frame = requestAnimationFrame(() => {
142
+ if (!root.current) return
143
+ const progress = root.current.closest('[data-sidenote-transcript]')?.querySelector<HTMLElement>('[data-sidenote-context-progress]')
144
+ if (progress) progress.scrollTop = progress.scrollHeight
145
+ const target = progress ?? lastConversationEntry(root.current, keys)
146
+ if (target) revealConversationEntryEnd(target)
147
+ })
148
+ })
149
+ return () => { cancelAnimationFrame(frame) }
150
+ }, [side.contextExpanded])
151
+ return <div ref={root} className={css.historyView}>{createElement(Original, { ...props, useConversation: useProjected })}</div>
129
152
  }
130
153
 
131
154
  /** Both admitted hosts publish the native chat View; only 0.2.1 adds a Flow slot. */
@@ -8,6 +8,7 @@
8
8
  .root { color: var(--dsw-alias-label-primary); }
9
9
  .nativeConversation { flex: 1; }
10
10
  .chatView { display: flex; flex-direction: column; flex: 1 0 auto; min-width: 0; }
11
+ .historyView { display: contents; }
11
12
  .mainContext { flex: none; padding: 12px 24px 4px; }
12
13
  .historyToggle { display: flex; align-items: center; gap: 6px; }
13
14
  .contextHint { color: var(--dsw-alias-label-tertiary); font-size: 12px; }