dsh-select-quote 0.3.0 → 0.3.1

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.
Files changed (3) hide show
  1. package/README.md +52 -54
  2. package/lib/client.js +224 -157
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # dsh-select-quote
2
2
 
3
- [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/guide/quickstart) 的 Web 客户端插件:在对话里**划词加批注**——把选中的原文连同你的评论一起交给模型,并让它在回答里明确指出说的是哪一段。
3
+ [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/guide/quickstart) 的客户端插件:在对话里**划词加批注**——选中的原文连同你的评论一起交给模型,并让它在回答里明确指出说的是哪一段。
4
4
 
5
5
  - **划词工具条**:选中对话区任意文本,浮出「复制 / 添加到任务」;拖拽结束才出现,不挡操作
6
- - **批注卡片**:添加后输入框内出现一张摘要卡片(可多条),**草稿里不会出现任何协议文本**
7
- - **正文标记**:被划中的文字在原消息里留下虚线下划线 + 序号角标;点卡片里的条目会滚回那段文字
8
- - **模型回指**:模型回答时以内联指令标出它处理的每一条批注,插件把这些指令渲染成可点击的 chip
9
- - **对话记录**:已发送消息里的批注渲染为卡片,用户气泡只剩你打的问题
6
+ - **批注卡片**:添加后输入框内出现摘要卡片(可多条),草稿里不会出现任何协议文本
7
+ - **正文标记**:被划中的文字在原消息里留下虚线下划线 + 序号角标,点卡片条目会滚回那段文字
8
+ - **模型回指**:模型回答里指向某条批注的位置,渲染成可点击的 chip
9
+ - **对话记录**:已发送消息里的批注渲染成卡片,用户气泡只剩你打的问题
10
+ - **界面语言**:跟随 dsh 自己的语言设置,简体中文 / English / Русский
10
11
 
11
12
  ![screenshot](https://raw.githubusercontent.com/latte03/dsh-select-quote/main/assets/screenshot-1.png)
12
13
 
@@ -14,88 +15,85 @@
14
15
 
15
16
  ---
16
17
 
17
- ## 兼容性
18
-
19
- | 项 | 值 |
20
- |---|---|
21
- | dsh | `0.1.7-rc.1`(2026-09-24 实测:划词、批注卡片、正文标记、对话记录渲染全通) |
22
- | profile | web(`dsh web`)与桌面端(desktop)都可用;桌面端在 0.2.0 及更早版本会楔死渲染进程,本仓库已修复,详见下节 |
23
- | 运行时 | 只有浏览器半有业务;Node 半仅用于让包被 Loader 挂载 |
24
-
25
- 实现依赖若干产品内部约定(槽位名、composer DOM、深色主题属性等),升级 dsh 后请先跑 `npm run check`,再照 [`docs/internals.md`](docs/internals.md) 末尾的复核清单人工回归一遍。
26
-
27
- ### 桌面端(desktop profile)
28
-
29
- 桌面端是 Electron 外壳 + 同一个 Web 客户端,插件声明的 `dsh.client.platform: "web"` 在桌面端同样加载,槽位与 DOM 契约都在,功能本身可用(实测批注随消息送达模型、模型回指渲染为 chip 均正常)。
30
-
31
- > ⚠️ **0.2.0 及更早版本在桌面端会楔死渲染进程**,见 [#1](https://github.com/latte03/dsh-select-quote/issues/1):App 能启动、界面完整渲染,但整个输入区无法点击,renderer 子进程单核 100% 持续旋转。根因在插件自己的装饰器里——协议头部逐字写出了 `:dsh-annotation{index="N"}`,当文本含有这个字面量而严格正则一条都没匹配上时,装饰器仍然替换了一次内容完全相同的 Text 节点,于是被自己的 `MutationObserver` 反复触发,微任务队列永远排不空,宏任务和定时器再也得不到执行。触发条件是文本内容,与 Electron 或 `dsh-app://` 无关,桌面端只是恰好把那段文本送进了 Text 节点。完整机制与两道防线记录在 [`docs/internals.md`](docs/internals.md) 关键机制 8。
32
-
33
- > **现在的状态**:修复已进主干,Windows 桌面端 0.2.0-rc.2 实测重新启用插件后批注正常送达、Turn 正常结束、输入框可点击、没有新的崩溃日志。npm 上可安装的仍是 `0.2.0`,也就是带缺陷的那一版,因此桌面端用户在修复版发布之前请在 `dsh.profile.bundles` 中摘除本插件,或改用浏览器访问同一个端口。[#1](https://github.com/latte03/dsh-select-quote/issues/1) 保持 open,等报告人复验后关闭。
34
-
35
18
  ## 安装
36
19
 
37
- ### 方式一:从 npm 安装(bundle)
20
+ 网页版:
38
21
 
39
22
  ```bash
40
23
  dsh plugin --profile web add dsh-select-quote
41
24
  dsh --profile web
42
25
  ```
43
26
 
44
- > npm 上目前最新是 `0.2.0`(2026-09-24 发布):带批注卡片、正文标记与模型回指,但界面只有简体中文,桌面端还会楔死渲染进程(见兼容性一节)。本仓库的 `0.3.0` 修掉了楔死、加了简体中文 / English / Русский 三语,并把对话记录里的批注卡片改到用户气泡内,**还没发布到 npm**。要用 `0.3.0` 的行为,先按方式二从本地 checkout 加载;发布完成后这一节需要更新。仓库里另有 `0.1.0`,那是旧版「引用块」,不要装错。
27
+ 桌面版:在应用内输入`dsh-select-quote`安装,或者插件页搜索 `dsh-select-quote` 安装,重启客户端生效。
45
28
 
46
- ### 方式二:本地 checkout 加载(`--patch`)
29
+ 改了代码想立刻验证,用本地 checkout 加载:
47
30
 
48
31
  ```bash
49
- git clone <repo> && cd dsh-select-quote
32
+ git clone https://github.com/latte03/dsh-select-quote && cd dsh-select-quote
50
33
  npm install && npm run build
51
34
  dsh web --patch "$PWD/dev.patch.yml"
52
35
  ```
53
36
 
54
- 打开 `http://127.0.0.1:3080`,在对话消息里划词即可看到工具条。`dev.patch.yml` 里的插件路径按本机写死(必须是绝对路径),换机器或换目录时改那一行;该文件不随 npm 包发布。
37
+ 打开 `http://127.0.0.1:3080`,在对话消息里划词即可看到工具条。`dev.patch.yml` 里的插件路径必须是绝对路径,换机器或换目录时改那一行;该文件不随 npm 包发布。
55
38
 
56
39
  ## 用法
57
40
 
58
- 1. 在对话区选中一段文本 → 点「添加到任务」
59
- 2. 弹出评论框,写一句想说的话(可留空,只划不评)→ `⌘/Ctrl + Enter` 确认
60
- 3. 输入框上方出现批注卡片,悬停可展开列表:逐条编辑、删除,或一键清空;点条目会跳到正文里对应的那段文字
61
- 4. 正常打字、按 Enter 发送。批注随消息一起交给模型,卡片随之消失
62
- 5. 模型回答里指向某条批注的位置,会显示成可点的 chip
41
+ 1. 在对话区选中一段文本,点「添加到任务」
42
+ 2. 在弹出的评论框里写一句想说的话(可留空,只划不评),`⌘/Ctrl + Enter` 确认
43
+ 3. 输入框上方出现批注卡片:悬停展开列表,可逐条编辑、删除或一键清空,点条目跳回正文对应文字
44
+ 4. 正常打字发送,批注随消息一起交给模型,卡片随之消失
45
+ 5. 模型回答里引用某条批注的位置显示为可点击 chip
63
46
 
64
- ## 语言 / Localization
47
+ ## 语言
65
48
 
66
- 界面文案支持 **简体中文 / English / Русский**,默认跟随浏览器语言:
49
+ 界面语言由 dsh 决定,插件不再自己判断浏览器语言。在 **设置 → 通用 → 语言** 里切换。
67
50
 
68
- | 语言 | 默认值 |
69
- |---|---|
70
- | `zh` | 原文案,同时作为缺失键的兜底词典 |
71
- | `en` | 非中文环境 |
72
- | `ru` | `ru` / `ru-RU` 等标签 |
73
51
 
74
- 想固定语言,在 Web GUI 控制台里设置后刷新页面:
52
+ ## 已知限制
75
53
 
76
- ```js
77
- localStorage.setItem('dsq-locale', 'ru') // 'ru' | 'en' | 'zh'
78
- localStorage.removeItem('dsq-locale') // 恢复跟随浏览器
79
- ```
54
+ - **待发批注只在内存里**:刷新页面或切换会话后会丢,卡片不持久化;已发送消息里的批注不受影响。
55
+ - **占用了用户气泡的渲染**:本插件以最低优先级替换内置 `user` 节点。其它插件若替换同一个键(例如 `dsh-easyrewrite` 用 `-1`),会被本插件屏蔽。
56
+ - **卡片宽度有限**:输入框内每条摘要最宽约为输入框的 1/4,超长标题走省略号。
57
+ - **划词只在对话区生效**:不介入编辑器、终端等面板。
80
58
 
81
- **刻意不翻译的部分**:`> [选中文本]`、`<response-annotations>`、`:dsh-annotation{index="N"}` 这些是模型与解析器约定的传输格式;`# Response annotations:` 那段是给模型的指令正文,不是界面文案。只有面向人的 chrome 走 `t()`。
59
+ ## 常见问题
82
60
 
83
- 新增文案:在 `src/client/i18n.ts` 的三份词典里各加一条,再在组件里 `t('key', { ... })` 调用。带 `{count}` 的键可以写成复数形式对象(中文用 `other`,俄语用 `one` / `few` / `many`)。
61
+ **升级 dsh 之后功能不对?** 先 `npm run check`;产物没问题就对照 [`docs/internals.md`](docs/internals.md) 的「依赖的产品内部约定」逐条复核,那里列了所有产品内部名。
84
62
 
85
- ## 已知限制
63
+ **划词没有工具条?** 确认选区在对话区而不是编辑器里;刷新页面(样式与观察者是幂等注入的);宿主 boot 日志里应出现 `dsh-select-quote`。
86
64
 
87
- - **待发批注只在内存里**:刷新页面或切换会话后会丢,卡片不持久化(已发送消息里的批注不受影响)。
88
- - **占用了用户气泡的渲染**:本插件以最低优先级替换内置 `user` 节点。若其它插件也替换同一个键(例如 `dsh-easyrewrite` 用 `-1`),会被本插件屏蔽。
89
- - **卡片宽度有限**:输入框内每条摘要最宽约为输入框的 1/4,超长标题走省略号。
90
- - 划词只在对话区生效,不介入编辑器、终端等面板。
65
+ **输入框里出现了协议原文?** 发送前的注入没生效,一般是 composer 结构变了——检查 `[data-composer-card]` 是否还在。
66
+
67
+ **对话记录里没有批注卡片?** 卡片渲染在用户气泡内部,若 `user` 节点被别的接管者赢走,或那条消息里确实没有批注块,都不会出现。
68
+
69
+ **界面语言不跟着 dsh 变?** 文案取自宿主的语言服务;宿主没有该服务或语言未注册时回退到 English。此时确认 dsh 版本带 `dsh-client-locale`,以及设置里确有语言选项。
70
+
71
+ **改了 CSS 不生效?** 样式只注入一次,刷新页面。
91
72
 
92
73
  ## 开发
93
74
 
94
75
  ```bash
95
- npm run check # typecheck + build + verify,提交前跑这个
76
+ npm install
77
+ npm run check # typecheck + build + verify
78
+ npm run watch # 增量重建
96
79
  ```
97
80
 
98
- 改 CSS 后要刷新页面(样式只注入一次)。协议格式、槽位注册、以及几处非显而易见的坑(U+200B 草稿标记、portal 到 body 的浮层、`--dsq-quote-pad` 高度补偿)记录在 **[docs/internals.md](docs/internals.md)**。
81
+ `npm run verify` 无头执行产物,断言 8 项贡献都注册上,并检查三份词典的键与 `{placeholder}` 在每种语言里都齐备(复数消息必须有 `.other` 兜底)。
82
+
83
+ 发布:先把源码与产物 `lib/` 一起提交,再跑
84
+
85
+ ```bash
86
+ npm run release # check → 追加 CHANGELOG.md → 提升版本号并打 tag
87
+ ```
88
+
89
+ ## 贡献
90
+
91
+ fork 后开分支,提交前跑 `npm run check`。一个 PR 只讲一件事,功能与文档分开,方便追溯。PR 里写清复现方式(环境、dsh 版本、操作步骤)。
92
+
93
+ ## 致谢
94
+
95
+ - [@imallakov](https://github.com/imallakov):多语言文案、跨平台构建、装饰器楔死修复与批注卡片归属(PR #2 / #3 / #4)
96
+ - [@EliteOtaku](https://github.com/EliteOtaku):桌面端渲染进程楔死的定位报告与 CDP 探针方法([#1](https://github.com/latte03/dsh-select-quote/issues/1))
99
97
 
100
98
  ## License
101
99
 
package/lib/client.js CHANGED
@@ -1765,175 +1765,204 @@ function LucideIcon({ name, size = 15 }) {
1765
1765
  }
1766
1766
  //#endregion
1767
1767
  //#region src/client/i18n.ts
1768
- const LOCALES = [
1769
- "ru",
1770
- "en",
1771
- "zh"
1772
- ];
1773
- const STORAGE_KEY = "dsq-locale";
1774
- const MESSAGES = {
1775
- ru: {
1776
- "common.selectedText": "Выделенный текст",
1777
- "common.commentPlaceholder": "Добавить необязательный комментарий…",
1778
- "common.cancel": "Отмена",
1779
- "common.save": "Сохранить",
1780
- "toolbar.aria": "Действия с выделенным текстом",
1781
- "toolbar.commentHint": "Добавить аннотацию…",
1782
- "toolbar.copy": "Копировать",
1783
- "toolbar.copyFailed": "Не удалось скопировать",
1784
- "toolbar.addToTask": "Добавить к задаче",
1785
- "toolbar.commentDialogAria": "Добавить необязательный комментарий",
1786
- "toolbar.skipComment": "Без комментария",
1787
- "toolbar.addComment": "Добавить комментарий",
1788
- "card.commentSubtitle": "Комментарий · {comment}",
1789
- "card.ariaIndexed": "Аннотация {index}",
1790
- "card.ariaPlain": "Аннотация к выделенному тексту",
1791
- "card.editComment": "Изменить комментарий",
1792
- "card.remove": "Удалить аннотацию",
1793
- "summary.hint": "Наведите, чтобы увидеть аннотации",
1794
- "summary.dialogAria": "Детали аннотаций: {count}",
1795
- "summary.edit": "Изменить аннотацию {index}",
1796
- "summary.remove": "Удалить аннотацию {index}",
1797
- "summary.userComment": "Комментарий пользователя",
1798
- "summary.noComment": "Комментарий не добавлен",
1799
- "summary.aria": {
1800
- one: "{count} аннотация. Наведите, чтобы увидеть детали.",
1801
- few: "{count} аннотации. Наведите, чтобы увидеть детали.",
1802
- many: "{count} аннотаций. Наведите, чтобы увидеть детали."
1803
- },
1804
- "summary.title": {
1805
- one: "{count} аннотация",
1806
- few: "{count} аннотации",
1807
- many: "{count} аннотаций"
1808
- },
1809
- "summary.removeAll": "Удалить все аннотации",
1810
- "chip.annotation": "Аннотация {index}"
1811
- },
1812
- en: {
1813
- "common.selectedText": "Selected text",
1814
- "common.commentPlaceholder": "Add an optional comment…",
1815
- "common.cancel": "Cancel",
1816
- "common.save": "Save",
1817
- "toolbar.aria": "Selection actions",
1818
- "toolbar.commentHint": "Add annotation…",
1819
- "toolbar.copy": "Copy",
1820
- "toolbar.copyFailed": "Copy failed",
1821
- "toolbar.addToTask": "Add to task",
1822
- "toolbar.commentDialogAria": "Add an optional comment",
1823
- "toolbar.skipComment": "Skip comment",
1824
- "toolbar.addComment": "Add comment",
1825
- "card.commentSubtitle": "Comment · {comment}",
1826
- "card.ariaIndexed": "Annotation {index}",
1827
- "card.ariaPlain": "Selection annotation",
1828
- "card.editComment": "Edit comment",
1829
- "card.remove": "Remove annotation",
1830
- "summary.hint": "Hover to view annotations",
1831
- "summary.dialogAria": "Annotation details: {count}",
1832
- "summary.edit": "Edit annotation {index}",
1833
- "summary.remove": "Remove annotation {index}",
1834
- "summary.userComment": "User comment",
1835
- "summary.noComment": "No comment added",
1836
- "summary.aria": {
1837
- one: "{count} annotation. Hover to view details.",
1838
- other: "{count} annotations. Hover to view details."
1839
- },
1840
- "summary.title": {
1841
- one: "{count} annotation",
1842
- other: "{count} annotations"
1843
- },
1844
- "summary.removeAll": "Remove all annotations",
1845
- "chip.annotation": "Annotation {index}"
1846
- },
1847
- zh: {
1848
- "common.selectedText": "选中的文本",
1849
- "common.commentPlaceholder": "添加可选评论…",
1850
- "common.cancel": "取消",
1851
- "common.save": "保存",
1852
- "toolbar.aria": "划词操作",
1853
- "toolbar.commentHint": "添加批注…",
1854
- "toolbar.copy": "复制",
1855
- "toolbar.copyFailed": "复制失败",
1856
- "toolbar.addToTask": "添加到任务",
1857
- "toolbar.commentDialogAria": "添加可选评论",
1858
- "toolbar.skipComment": "暂不评论",
1859
- "toolbar.addComment": "添加评论",
1860
- "card.commentSubtitle": "评论 · {comment}",
1861
- "card.ariaIndexed": "批注 {index}",
1862
- "card.ariaPlain": "划词批注",
1863
- "card.editComment": "编辑评论",
1864
- "card.remove": "移除批注",
1865
- "summary.hint": "悬停查看批注",
1866
- "summary.dialogAria": "{count} 条划词批注详情",
1867
- "summary.edit": "编辑批注 {index}",
1868
- "summary.remove": "移除批注 {index}",
1869
- "summary.userComment": "用户评论",
1870
- "summary.noComment": "未添加评论",
1871
- "summary.aria": { other: "{count} 条划词批注。悬停查看详情。" },
1872
- "summary.title": { other: "{count} 条批注" },
1873
- "summary.removeAll": "移除全部划词批注",
1874
- "chip.annotation": "批注 {index}"
1875
- }
1768
+ /**
1769
+ * Copy for dsh-select-quote, served through the host locale service.
1770
+ *
1771
+ * The active language is owned by `@deepseek-ai/dsh-client-locale` (Settings →
1772
+ * General → Language, persisted as `locale.preference`). This module owns only
1773
+ * the dictionaries and the plural form selection that the host's plain-template
1774
+ * lookup cannot express; it never resolves a language on its own.
1775
+ *
1776
+ * NOT localized on purpose:
1777
+ * - wire markers (`> [选中文本]`, `<response-annotations>`,
1778
+ * `:dsh-annotation{index="N"}`) — they are the message format the model
1779
+ * and the transcript parser agree on;
1780
+ * - the `# Response annotations:` header — model-facing instruction prose,
1781
+ * not UI text.
1782
+ */
1783
+ const LOCALE_NS = "select-quote";
1784
+ const zh = {
1785
+ "common.selectedText": "选中的文本",
1786
+ "common.commentPlaceholder": "添加可选评论…",
1787
+ "common.cancel": "取消",
1788
+ "common.save": "保存",
1789
+ "toolbar.aria": "划词操作",
1790
+ "toolbar.commentHint": "添加批注…",
1791
+ "toolbar.copy": "复制",
1792
+ "toolbar.copyFailed": "复制失败",
1793
+ "toolbar.addToTask": "添加到任务",
1794
+ "toolbar.commentDialogAria": "添加可选评论",
1795
+ "toolbar.skipComment": "暂不评论",
1796
+ "toolbar.addComment": "添加评论",
1797
+ "card.commentSubtitle": "评论 · {comment}",
1798
+ "card.ariaIndexed": "批注 {index}",
1799
+ "card.ariaPlain": "划词批注",
1800
+ "card.editComment": "编辑评论",
1801
+ "card.remove": "移除批注",
1802
+ "summary.hint": "悬停查看批注",
1803
+ "summary.dialogAria": "{count} 条划词批注详情",
1804
+ "summary.edit": "编辑批注 {index}",
1805
+ "summary.remove": "移除批注 {index}",
1806
+ "summary.userComment": "用户评论",
1807
+ "summary.noComment": "未添加评论",
1808
+ "summary.aria.other": "{count} 条划词批注。悬停查看详情。",
1809
+ "summary.title.other": "{count} 条批注",
1810
+ "summary.removeAll": "移除全部划词批注",
1811
+ "chip.annotation": "批注 {index}"
1876
1812
  };
1877
- let cached = null;
1878
- function matchLocale(tag) {
1879
- const lower = tag.toLowerCase();
1880
- for (const locale of LOCALES) if (lower === locale || lower.startsWith(`${locale}-`) || lower.startsWith(`${locale}_`)) return locale;
1881
- return null;
1882
- }
1883
- function localStore() {
1884
- return globalThis.localStorage;
1885
- }
1886
- function readOverride() {
1887
- try {
1888
- const raw = localStore()?.getItem(STORAGE_KEY);
1889
- return raw ? matchLocale(raw) : null;
1890
- } catch {
1891
- return null;
1892
- }
1893
- }
1894
- function detectLocale() {
1895
- const override = readOverride();
1896
- if (override) return override;
1897
- const nav = globalThis.navigator;
1898
- const tags = nav ? [...nav.languages ?? [], nav.language].filter(Boolean) : [];
1899
- for (const tag of tags) {
1900
- const hit = matchLocale(tag);
1901
- if (hit) return hit;
1902
- }
1903
- return "en";
1813
+ const en = {
1814
+ "common.selectedText": "Selected text",
1815
+ "common.commentPlaceholder": "Add an optional comment…",
1816
+ "common.cancel": "Cancel",
1817
+ "common.save": "Save",
1818
+ "toolbar.aria": "Selection actions",
1819
+ "toolbar.commentHint": "Add annotation…",
1820
+ "toolbar.copy": "Copy",
1821
+ "toolbar.copyFailed": "Copy failed",
1822
+ "toolbar.addToTask": "Add to task",
1823
+ "toolbar.commentDialogAria": "Add an optional comment",
1824
+ "toolbar.skipComment": "Skip comment",
1825
+ "toolbar.addComment": "Add comment",
1826
+ "card.commentSubtitle": "Comment · {comment}",
1827
+ "card.ariaIndexed": "Annotation {index}",
1828
+ "card.ariaPlain": "Selection annotation",
1829
+ "card.editComment": "Edit comment",
1830
+ "card.remove": "Remove annotation",
1831
+ "summary.hint": "Hover to view annotations",
1832
+ "summary.dialogAria": "Annotation details: {count}",
1833
+ "summary.edit": "Edit annotation {index}",
1834
+ "summary.remove": "Remove annotation {index}",
1835
+ "summary.userComment": "User comment",
1836
+ "summary.noComment": "No comment added",
1837
+ "summary.aria.one": "{count} annotation. Hover to view details.",
1838
+ "summary.aria.other": "{count} annotations. Hover to view details.",
1839
+ "summary.title.one": "{count} annotation",
1840
+ "summary.title.other": "{count} annotations",
1841
+ "summary.removeAll": "Remove all annotations",
1842
+ "chip.annotation": "Annotation {index}"
1843
+ };
1844
+ /**
1845
+ * Russian is not a shipped host locale: it is added through
1846
+ * `ctx.locale.addLanguage()` and its dictionary through the three-argument
1847
+ * `register()`. Its fallback chain ends at `en`, as the host requires.
1848
+ */
1849
+ const ru = {
1850
+ "common.selectedText": "Выделенный текст",
1851
+ "common.commentPlaceholder": "Добавить необязательный комментарий…",
1852
+ "common.cancel": "Отмена",
1853
+ "common.save": "Сохранить",
1854
+ "toolbar.aria": "Действия с выделенным текстом",
1855
+ "toolbar.commentHint": "Добавить аннотацию…",
1856
+ "toolbar.copy": "Копировать",
1857
+ "toolbar.copyFailed": "Не удалось скопировать",
1858
+ "toolbar.addToTask": "Добавить к задаче",
1859
+ "toolbar.commentDialogAria": "Добавить необязательный комментарий",
1860
+ "toolbar.skipComment": "Без комментария",
1861
+ "toolbar.addComment": "Добавить комментарий",
1862
+ "card.commentSubtitle": "Комментарий · {comment}",
1863
+ "card.ariaIndexed": "Аннотация {index}",
1864
+ "card.ariaPlain": "Аннотация к выделенному тексту",
1865
+ "card.editComment": "Изменить комментарий",
1866
+ "card.remove": "Удалить аннотацию",
1867
+ "summary.hint": "Наведите, чтобы увидеть аннотации",
1868
+ "summary.dialogAria": "Детали аннотаций: {count}",
1869
+ "summary.edit": "Изменить аннотацию {index}",
1870
+ "summary.remove": "Удалить аннотацию {index}",
1871
+ "summary.userComment": "Комментарий пользователя",
1872
+ "summary.noComment": "Комментарий не добавлен",
1873
+ "summary.aria.one": "{count} аннотация. Наведите, чтобы увидеть детали.",
1874
+ "summary.aria.few": "{count} аннотации. Наведите, чтобы увидеть детали.",
1875
+ "summary.aria.many": "{count} аннотаций. Наведите, чтобы увидеть детали.",
1876
+ "summary.aria.other": "{count} аннотаций. Наведите, чтобы увидеть детали.",
1877
+ "summary.title.one": "{count} аннотация",
1878
+ "summary.title.few": "{count} аннотации",
1879
+ "summary.title.many": "{count} аннотаций",
1880
+ "summary.title.other": "{count} аннотаций",
1881
+ "summary.removeAll": "Удалить все аннотации",
1882
+ "chip.annotation": "Аннотация {index}"
1883
+ };
1884
+ /** Both shipped host locales, required together by the typed `register()`. */
1885
+ const SHIPPED_DICTS = Object.freeze({
1886
+ zh,
1887
+ en
1888
+ });
1889
+ /** Dictionary for the language registered through {@link LANGUAGE_RU}. */
1890
+ const DICTIONARIES = Object.freeze({
1891
+ zh,
1892
+ en,
1893
+ ru
1894
+ });
1895
+ const LANGUAGE_RU = Object.freeze({
1896
+ id: "ru",
1897
+ label: "Русский",
1898
+ fallback: "en"
1899
+ });
1900
+ let api;
1901
+ /**
1902
+ * Install the host-bound translation entry point. Called once from `apply()`
1903
+ * with `ctx.locale.bind(LOCALE_NS)` and the live snapshot accessors.
1904
+ */
1905
+ function bindLocale(next) {
1906
+ api = next;
1904
1907
  }
1905
- /** Active locale (resolved once, then cached). */
1906
- function getLocale() {
1907
- cached ??= detectLocale();
1908
- return cached;
1908
+ /** Active language id, or `en` before the host service is bound. */
1909
+ function activeLocale() {
1910
+ return api?.getActive() ?? "en";
1909
1911
  }
1910
- function pluralize(forms, locale, count) {
1912
+ function pluralForm(count) {
1913
+ const locale = activeLocale();
1911
1914
  if (locale === "ru") {
1912
1915
  const mod10 = count % 10;
1913
1916
  const mod100 = count % 100;
1914
- if (mod10 === 1 && mod100 !== 11) return forms.one ?? forms.other ?? "";
1915
- if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return forms.few ?? forms.other ?? "";
1916
- return forms.many ?? forms.other ?? "";
1917
+ if (mod10 === 1 && mod100 !== 11) return "one";
1918
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
1919
+ return "many";
1917
1920
  }
1918
- if (locale === "en") return (count === 1 ? forms.one : void 0) ?? forms.other ?? "";
1919
- return forms.other ?? forms.many ?? "";
1920
- }
1921
- function interpolate(template, params) {
1922
- return template.replace(/\{(\w+)\}/g, (whole, name) => name in params ? String(params[name]) : whole);
1921
+ if (locale === "en") return count === 1 ? "one" : "other";
1922
+ return "other";
1923
1923
  }
1924
1924
  /**
1925
- * Translate a key for the active locale.
1926
- * `{name}` placeholders are filled from `params`; a numeric `count` also
1927
- * selects the right plural form when the message is a `PluralForms` object.
1925
+ * Translate a `select-quote` key. A numeric `count` selects a plural form and
1926
+ * the matching `key.form` entry is used, falling back to `key.other`.
1928
1927
  */
1929
1928
  function t(key, params = {}) {
1930
- const locale = getLocale();
1931
- const value = MESSAGES[locale][key] ?? MESSAGES.zh[key] ?? key;
1932
- return interpolate(typeof value === "string" ? value : pluralize(value, locale, typeof params.count === "number" ? params.count : 0), params);
1929
+ if (!api) return key;
1930
+ if (typeof params.count === "number") {
1931
+ const form = pluralForm(params.count);
1932
+ const exact = api.translate(`${key}.${form}`, params);
1933
+ if (exact !== `${key}.${form}`) return exact;
1934
+ const other = api.translate(`${key}.other`, params);
1935
+ if (other !== `${key}.other`) return other;
1936
+ }
1937
+ return api.translate(key, params);
1938
+ }
1939
+ /**
1940
+ * Subscribe to language or dictionary changes. Components go through
1941
+ * `useLocale()` instead; this is the non-React entry point (DOM decorations).
1942
+ * Subscribing before the host service is bound is a no-op, so callers subscribe
1943
+ * from `apply()`, after `bindLocale()`.
1944
+ */
1945
+ function subscribeLocale(listener) {
1946
+ return api ? api.subscribe(listener) : () => {};
1947
+ }
1948
+ /** Monotonic host revision counter, bumped on locale and dictionary changes. */
1949
+ function localeRevision() {
1950
+ return api?.getRevision() ?? 0;
1951
+ }
1952
+ //#endregion
1953
+ //#region src/client/useLocale.ts
1954
+ /**
1955
+ * Re-render the calling component when the host language changes, so slot copy
1956
+ * switches without a page reload. Subscriptions happen after mount, by which
1957
+ * time `apply()` has bound the locale service.
1958
+ */
1959
+ function useLocale() {
1960
+ (0, react.useSyncExternalStore)(subscribeLocale, localeRevision, localeRevision);
1933
1961
  }
1934
1962
  //#endregion
1935
1963
  //#region src/client/ui/AnnotationCardView.tsx
1936
1964
  function AnnotationCardView({ title, text, comment, variant, index, onRemove, onEditComment }) {
1965
+ useLocale();
1937
1966
  const composer = variant === "composer";
1938
1967
  const subtitle = comment ? composer ? t("card.commentSubtitle", { comment }) : comment : composer ? t("common.selectedText") : t("summary.noComment");
1939
1968
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -1993,7 +2022,8 @@ const SETTLE_DELAY = 600;
1993
2022
  * other cancels. Comment editing happens inline in the list — the panel stays
1994
2023
  * put and auto-close is suppressed while an edit is open.
1995
2024
  */
1996
- function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemove, onItemEdit, hint = t("summary.hint") }) {
2025
+ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemove, onItemEdit }) {
2026
+ useLocale();
1997
2027
  const rootRef = (0, react.useRef)(null);
1998
2028
  const popRef = (0, react.useRef)(null);
1999
2029
  const closeTimerRef = (0, react.useRef)(null);
@@ -2285,7 +2315,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
2285
2315
  children: t("summary.title", { count })
2286
2316
  }), (0, react_jsx_runtime.jsx)("div", {
2287
2317
  className: styles.summaryHint,
2288
- children: hint
2318
+ children: t("summary.hint")
2289
2319
  })]
2290
2320
  }),
2291
2321
  onRemoveAll ? (0, react_jsx_runtime.jsx)("button", {
@@ -2586,6 +2616,7 @@ async function copyText(text) {
2586
2616
  /** Cap for the auto-growing comment box; beyond this it scrolls internally. */
2587
2617
  const COMMENT_MAX_HEIGHT = 220;
2588
2618
  function SelectionToolbar({ sessionId }) {
2619
+ useLocale();
2589
2620
  const toolbarRef = (0, react.useRef)(null);
2590
2621
  const commentWrapRef = (0, react.useRef)(null);
2591
2622
  const commentRef = (0, react.useRef)(null);
@@ -2913,6 +2944,22 @@ function decorateAnnotationDirectives(root) {
2913
2944
  }
2914
2945
  for (const node of hits) decorateTextNode(node);
2915
2946
  }
2947
+ /**
2948
+ * Re-label chips that are already on screen after the language changed.
2949
+ *
2950
+ * Chip text is plain DOM, not React state, so a switch would otherwise leave
2951
+ * old-language chips next to new-language popovers. Writing `textContent` here
2952
+ * is safe for the observer: the new text node never contains the directive
2953
+ * literal, so `decorateTextNode()` returns before it can touch the DOM.
2954
+ */
2955
+ function refreshDirectiveChips(root) {
2956
+ const el = root ?? (typeof document === "undefined" ? void 0 : document.body);
2957
+ if (!el) return;
2958
+ for (const chip of el.querySelectorAll(`[data-${DECO}]`)) {
2959
+ const index = Number(chip.dataset[DECO]);
2960
+ if (Number.isFinite(index)) chip.textContent = t("chip.annotation", { index });
2961
+ }
2962
+ }
2916
2963
  /** Observe chat DOM and decorate new directive text. Returns disposer. */
2917
2964
  function watchAnnotationDirectives(root) {
2918
2965
  const el = root ?? (typeof document === "undefined" ? void 0 : document.body);
@@ -3064,10 +3111,30 @@ function UserMessageDisplay({ node, renderMessageImages }) {
3064
3111
  * kind is classified as a turn-process member and `hidden` when the completed
3065
3112
  * turn's process disclosure is folded — mounted, but invisible. `user` is on
3066
3113
  * that allowlist, so the bubble is the only placement that is reliably visible.
3114
+ *
3115
+ * Language is the host's decision, never this plugin's: dictionaries go to
3116
+ * `ctx.locale`, and `t()` resolves against whatever language the user picked in
3117
+ * Settings. Self-detecting from `navigator` would disagree with the host on the
3118
+ * desktop shell, where Electron pins the document language (`--lang`) to its own
3119
+ * value while the host preference comes through `__DSH_LOCALE__`.
3067
3120
  */
3068
- const inject = ["slots", "sessions"];
3121
+ const inject = [
3122
+ "slots",
3123
+ "sessions",
3124
+ "locale"
3125
+ ];
3069
3126
  function apply(ctx) {
3070
3127
  ensureToolbarStyles();
3128
+ ctx.effect(() => ctx.locale.addLanguage(LANGUAGE_RU), "dsh-select-quote: ru language");
3129
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, SHIPPED_DICTS), "dsh-select-quote: dictionaries (zh, en)");
3130
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, LANGUAGE_RU.id, DICTIONARIES.ru), "dsh-select-quote: dictionary (ru)");
3131
+ bindLocale({
3132
+ translate: ctx.locale.bind(LOCALE_NS),
3133
+ getActive: () => ctx.locale.getLocale().active,
3134
+ getRevision: () => ctx.locale.getSnapshot().revision,
3135
+ subscribe: (listener) => ctx.locale.subscribe(listener)
3136
+ });
3137
+ ctx.effect(() => subscribeLocale(() => refreshDirectiveChips()), "dsh-select-quote: chip language");
3071
3138
  ctx.effect(() => watchAnnotationDirectives(), "dsh-select-quote: annotation directives");
3072
3139
  ctx.effect(() => watchAnnotationMarks(), "dsh-select-quote: annotation marks");
3073
3140
  ctx.effect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-select-quote",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "DeepSeek Harness web plugin: annotate text selected in the conversation — the selection rides along with your next message, is marked in the source text, and the model points back at it.",
5
5
  "keywords": [
6
6
  "deepseek",
@@ -39,7 +39,8 @@
39
39
  "watch": "node scripts/build.mjs --watch",
40
40
  "typecheck": "tsc -p tsconfig.json --noEmit",
41
41
  "verify": "node scripts/verify-bundle.mjs",
42
- "check": "npm run typecheck && npm run build && npm run verify"
42
+ "check": "npm run typecheck && npm run build && npm run verify",
43
+ "release": "npm run check && commit-and-tag-version"
43
44
  },
44
45
  "dsh": {
45
46
  "client": {
@@ -66,6 +67,7 @@
66
67
  "devDependencies": {
67
68
  "@types/react": "~18.3.1",
68
69
  "@types/react-dom": "~18.3",
70
+ "commit-and-tag-version": "^13.2.2",
69
71
  "react": "^18.2.0",
70
72
  "react-dom": "^18.2.0",
71
73
  "tsdown": "0.23.0",