dsh-ask-from-text 0.1.13 → 0.1.20

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 (4) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +6 -6
  3. package/client.js +188 -120
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,12 +4,125 @@ All notable changes to `dsh-ask-from-text` are documented here. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
5
5
  project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.1.20] - 2026-11-13
8
+
9
+ ### Added
10
+
11
+ - Runtime UI localization for Chinese and English. The active DSH language is
12
+ read through `ctx.locale`; other languages fall back to English. Native
13
+ conversation and Plugin Manager metadata continue to follow DSH.
14
+
15
+ ## [0.1.19] - 2026-11-13
16
+
17
+ ### Fixed
18
+
19
+ - Do not pin a selected branch row above the group. Only the root/main Session
20
+ is allowed above `追问分支`; clicking a branch keeps it in the group.
21
+
22
+ ## [0.1.18] - 2026-11-13
23
+
24
+ ### Fixed
25
+
26
+ - Put the shared `追问分支` header before the topmost branch row in visual
27
+ document order, so collapsing/expanding no longer moves it below the group.
28
+
29
+ ## [0.1.17] - 2026-11-13
30
+
31
+ ### Fixed
32
+
33
+ - Keep the shared branch-group header when the group is collapsed by ignoring
34
+ rows hidden by the plugin itself, while still ignoring rows hidden inside a
35
+ folded Workspace or a stale search tree.
36
+
37
+ ## [0.1.16] - 2026-11-13
38
+
39
+ ### Fixed
40
+
41
+ - Restore the shared `追问分支` header by inserting it inside the first branch
42
+ row wrapper, where the sidebar animation layer participates in layout, and
43
+ keep only one header for all branch rows.
44
+
45
+ ## [0.1.15] - 2026-11-13
46
+
47
+ ### Fixed
48
+
49
+ - Group all branch Session rows under one shared `追问分支` header even when
50
+ the sidebar gives each row its own animated wrapper, and keep the selected
51
+ main row above that shared header.
52
+
53
+ ## [0.1.14] - 2026-11-13
54
+
55
+ ### Fixed
56
+
57
+ - Handle main and branch Sessions that the sidebar places in different
58
+ Workspace sections by moving the main section above the first branch section.
59
+
7
60
  ## [0.1.13] - 2026-11-13
8
61
 
9
62
  ### Fixed
10
63
 
11
64
  - README screenshots now use absolute CDN URLs so they render on the npm package page.
12
65
 
66
+ ## [0.1.20] - 2026-11-13
67
+
68
+ ### Added
69
+
70
+ - Runtime UI localization for Chinese and English. The active DSH language is
71
+ read through `ctx.locale`; other languages fall back to English. Native
72
+ conversation and Plugin Manager metadata continue to follow DSH.
73
+
74
+ ## [0.1.19] - 2026-11-13
75
+
76
+ ### Fixed
77
+
78
+ - Do not pin a selected branch row above the group. Only the root/main Session
79
+ is allowed above `追问分支`; clicking a branch keeps it in the group.
80
+
81
+ ## [0.1.18] - 2026-11-13
82
+
83
+ ### Fixed
84
+
85
+ - Put the shared `追问分支` header before the topmost branch row in visual
86
+ document order, so collapsing/expanding no longer moves it below the group.
87
+
88
+ ## [0.1.17] - 2026-11-13
89
+
90
+ ### Fixed
91
+
92
+ - Keep the shared branch-group header when the group is collapsed by ignoring
93
+ rows hidden by the plugin itself, while still ignoring rows hidden inside a
94
+ folded Workspace or a stale search tree.
95
+
96
+ ## [0.1.16] - 2026-11-13
97
+
98
+ ### Fixed
99
+
100
+ - Restore the shared `追问分支` header by inserting it inside the first branch
101
+ row wrapper, where the sidebar animation layer participates in layout, and
102
+ keep only one header for all branch rows.
103
+
104
+ ## [0.1.15] - 2026-11-13
105
+
106
+ ### Fixed
107
+
108
+ - Group all branch Session rows under one shared `追问分支` header even when
109
+ the sidebar gives each row its own animated wrapper, and keep the selected
110
+ main row above that shared header.
111
+
112
+ ## [0.1.14] - 2026-11-13
113
+
114
+ ### Fixed
115
+
116
+ - Handle main and branch Sessions that the sidebar places in different
117
+ Workspace sections by moving the main section above the first branch section.
118
+
119
+ ## [0.1.13] - 2026-11-13
120
+
121
+ ### Fixed
122
+
123
+ - Prefer the selected sidebar row when pinning the main conversation above its
124
+ branch group, and re-evaluate when the selected Session changes.
125
+
13
126
  ## [0.1.12] - 2026-11-13
14
127
 
15
128
  ### Changed
@@ -91,3 +204,18 @@ project follows [Semantic Versioning](https://semver.org/).
91
204
  [0.1.11]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.11
92
205
  [0.1.12]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.12
93
206
  [0.1.13]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.13
207
+ [0.1.14]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.14
208
+ [0.1.15]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.15
209
+ [0.1.16]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.16
210
+ [0.1.17]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.17
211
+ [0.1.18]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.18
212
+ [0.1.19]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.19
213
+ [0.1.20]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.20
214
+ [0.1.13]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.13
215
+ [0.1.14]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.14
216
+ [0.1.15]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.15
217
+ [0.1.16]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.16
218
+ [0.1.17]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.17
219
+ [0.1.18]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.18
220
+ [0.1.19]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.19
221
+ [0.1.20]: https://github.com/gbn0219/dsh-ask-from-text/releases/tag/v0.1.20
package/README.md CHANGED
@@ -10,13 +10,13 @@
10
10
 
11
11
  需要 DeepSeek Harness 0.2.2-rc.x。
12
12
 
13
- ### 从 GitHub 安装(当前可用)
13
+ ### 从 GitHub 安装
14
14
 
15
15
  ```sh
16
16
  dsh plugin --profile web add github:gbn0219/dsh-ask-from-text
17
17
  ```
18
18
 
19
- ### 从 npm 安装(发布后可用)
19
+ ### 从 npm 安装
20
20
 
21
21
  ```sh
22
22
  dsh plugin --profile web add dsh-ask-from-text
@@ -103,7 +103,7 @@ dsh plugin --profile web add /absolute/path/to/dsh-ask-from-text
103
103
  - 追问回答是真实的 DSH Session,可以使用思考、工具调用、Stop 和原生输入框。
104
104
  - 分支只看到父级和祖先内容,不看到兄弟分支,也不会自动获得父级后来新增的内容。
105
105
  - 分支对话会出现在普通 Session 列表中,这是正常现象。
106
- - 当前浮窗和舞台的运行时按钮文案为中文;插件管理器的标题/描述提供中英文。
106
+ - 界面文案跟随 DSH 语言:中文/英文;其他语言自动回退英文。原生对话区域继续跟随 DSH 自身的语言。
107
107
  - 目前主要面向 DSH Desktop;Web 共用同一套 UI,窄屏体验尚未专门优化。
108
108
 
109
109
  ## 常见问题
@@ -132,13 +132,13 @@ dsh plugin --profile web add /absolute/path/to/dsh-ask-from-text
132
132
 
133
133
  Requires DeepSeek Harness 0.2.2-rc.x.
134
134
 
135
- #### From GitHub (available now)
135
+ #### From GitHub
136
136
 
137
137
  ```sh
138
138
  dsh plugin --profile web add github:gbn0219/dsh-ask-from-text
139
139
  ```
140
140
 
141
- #### From npm (after the npm release)
141
+ #### From npm
142
142
 
143
143
  ```sh
144
144
  dsh plugin --profile web add dsh-ask-from-text
@@ -225,7 +225,7 @@ These sessions are grouped under a `追问分支` header. Click the header to co
225
225
  - A follow-up is a real DSH Session. It can use reasoning, tool calls, Stop, and the native composer.
226
226
  - A branch sees its parent and ancestors, not siblings, and does not automatically receive later turns added to its parent.
227
227
  - Branch conversations also appear in the normal Session list. This is expected.
228
- - Runtime UI labels are currently Chinese; Plugin Manager metadata is available in Chinese and English.
228
+ - Runtime UI follows the DSH language: Chinese and English are bundled; other languages fall back to English. The native conversation follows DSH.
229
229
  - DSH Desktop is the primary target. The Web client shares the same UI, but narrow-screen behavior has not been specifically polished.
230
230
 
231
231
  ### FAQ
package/client.js CHANGED
@@ -78,6 +78,13 @@ window.__ModuleLoader__.load({
78
78
  stages: '个节点',
79
79
  collapseDock: '收起列表',
80
80
  expandDock: '展开列表',
81
+ sidebarGroup: '追问分支',
82
+ context: '上下文',
83
+ contextStart: '上下文起点',
84
+ waiting: '待提问',
85
+ treeNote: '当前模型上下文:{context}。兄弟分支不在其中。',
86
+ anchorMissing: '的原文当前不可见,可能已被折叠或尚未加载',
87
+ draftWait: '在下方输入浮窗中提问',
81
88
  },
82
89
  en: {
83
90
  chip: 'Ask',
@@ -126,6 +133,13 @@ window.__ModuleLoader__.load({
126
133
  stages: 'nodes',
127
134
  collapseDock: 'Collapse list',
128
135
  expandDock: 'Expand list',
136
+ sidebarGroup: 'Branches',
137
+ context: 'Context',
138
+ contextStart: 'Context root',
139
+ waiting: 'Waiting for question',
140
+ treeNote: 'Current model context: {context}. Sibling branches are not included.',
141
+ anchorMissing: 'source text is not visible; it may be folded or not loaded yet',
142
+ draftWait: 'Ask in the input window below',
129
143
  },
130
144
  }
131
145
 
@@ -342,21 +356,21 @@ window.__ModuleLoader__.load({
342
356
  }
343
357
 
344
358
  function pathLabel(state, branch) {
345
- return `${DICT.zh.main}${pathOf(state, branch).map(item => ` › #${item.number}`).join('')}`
359
+ return `${app.t('main')}${pathOf(state, branch).map(item => ` › #${item.number}`).join('')}`
346
360
  }
347
361
 
348
362
  function branchBasis(state, branch) {
349
- if (!branch.parentBranchId) return `${DICT.zh.basedOn} ${DICT.zh.main}`
363
+ if (!branch.parentBranchId) return `${app.t('basedOn')} ${app.t('main')}`
350
364
  const parent = state.branches[branch.parentBranchId]
351
- return parent ? `${DICT.zh.basedOn} #${parent.number}` : `${DICT.zh.basedOn} ${DICT.zh.main}`
365
+ return parent ? `${app.t('basedOn')} #${parent.number}` : `${app.t('basedOn')} ${app.t('main')}`
352
366
  }
353
367
 
354
368
  function statusLabel(branch) {
355
- if (branch.status === 'sending') return DICT.zh.sending
356
- if (branch.status === 'running') return DICT.zh.running
357
- if (branch.status === 'error') return DICT.zh.failed
358
- if (branch.prompted) return DICT.zh.ready
359
- return DICT.zh.draft
369
+ if (branch.status === 'sending') return app.t('sending')
370
+ if (branch.status === 'running') return app.t('running')
371
+ if (branch.status === 'error') return app.t('failed')
372
+ if (branch.prompted) return app.t('ready')
373
+ return app.t('draft')
360
374
  }
361
375
 
362
376
  function colorVars(color) {
@@ -422,7 +436,15 @@ window.__ModuleLoader__.load({
422
436
  refs,
423
437
  chats,
424
438
  get main() { return main },
425
- t(key) { return (ctx.locale && ctx.locale.bind(NS)(key)) || DICT.zh[key] || key },
439
+ t(key, params) {
440
+ if (ctx.locale) {
441
+ const value = ctx.locale.bind(NS)(key, params)
442
+ if (value && value !== key) return value
443
+ }
444
+ const template = DICT.en[key] ?? key
445
+ if (!params) return template
446
+ return template.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match)
447
+ },
426
448
  subscribe(listener) {
427
449
  listeners.add(listener)
428
450
  return () => { listeners.delete(listener) }
@@ -902,7 +924,7 @@ window.__ModuleLoader__.load({
902
924
  const quote = ranges.map(part => part.quote).join('\n').trim()
903
925
  if (!quote) { clearChip(); return }
904
926
  if (quote.length > MAX_QUOTE) {
905
- notify(DICT.zh.tooLong)
927
+ notify(app.t('tooLong'))
906
928
  clearChip()
907
929
  return
908
930
  }
@@ -1013,7 +1035,13 @@ window.__ModuleLoader__.load({
1013
1035
  })
1014
1036
  if (!own) scheduleProject()
1015
1037
  })
1016
- mutationObserver.observe(document.body, { childList: true, subtree: true, characterData: true })
1038
+ mutationObserver.observe(document.body, {
1039
+ childList: true,
1040
+ subtree: true,
1041
+ characterData: true,
1042
+ attributes: true,
1043
+ attributeFilter: ['aria-selected'],
1044
+ })
1017
1045
  }
1018
1046
  scheduleProject()
1019
1047
  return () => {
@@ -1096,7 +1124,7 @@ window.__ModuleLoader__.load({
1096
1124
 
1097
1125
  function serialize(quote, question) {
1098
1126
  const body = String(quote || '').trim().split(/\r?\n/).map(line => `> ${line}`).join('\n')
1099
- const text = String(question || '').trim() || DICT.zh.defaultQuestion
1127
+ const text = String(question || '').trim() || app.t('defaultQuestion')
1100
1128
  return `${body}\n\n${text}`
1101
1129
  }
1102
1130
 
@@ -1154,7 +1182,7 @@ window.__ModuleLoader__.load({
1154
1182
  await ensureReference(branch)
1155
1183
  }
1156
1184
  const reference = refs.get(branch.id)
1157
- if (!reference) throw new Error(DICT.zh.noSession)
1185
+ if (!reference) throw new Error(app.t('noSession'))
1158
1186
  const text = serialize(branch.anchor.quote, branch.question)
1159
1187
  const echo = reference.binding.session.beginSubmission({
1160
1188
  mode: 'queue',
@@ -1169,7 +1197,7 @@ window.__ModuleLoader__.load({
1169
1197
  )
1170
1198
  if (!result.ok) {
1171
1199
  branch.status = 'error'
1172
- branch.error = result.error?.message || DICT.zh.error
1200
+ branch.error = result.error?.message || app.t('error')
1173
1201
  emit()
1174
1202
  return false
1175
1203
  }
@@ -1198,7 +1226,7 @@ window.__ModuleLoader__.load({
1198
1226
  try {
1199
1227
  await ensureReference(branch)
1200
1228
  const reference = refs.get(branch.id)
1201
- if (!reference) throw new Error(DICT.zh.noSession)
1229
+ if (!reference) throw new Error(app.t('noSession'))
1202
1230
  const echo = reference.binding.session.beginSubmission({
1203
1231
  mode: 'queue',
1204
1232
  text,
@@ -1212,7 +1240,7 @@ window.__ModuleLoader__.load({
1212
1240
  )
1213
1241
  if (!result.ok) {
1214
1242
  branch.status = 'error'
1215
- branch.error = result.error?.message || DICT.zh.error
1243
+ branch.error = result.error?.message || app.t('error')
1216
1244
  emit()
1217
1245
  return false
1218
1246
  }
@@ -1263,57 +1291,96 @@ window.__ModuleLoader__.load({
1263
1291
  const ids = new Set(state.order
1264
1292
  .map(id => state.branches[id]?.sessionId)
1265
1293
  .filter(Boolean))
1266
- const parents = new Map()
1294
+ const rows = []
1267
1295
  for (const id of ids) {
1268
1296
  for (const row of document.querySelectorAll(`[data-row-key="session:${cssEscape(id)}"]`)) {
1269
1297
  if (row.closest('[data-ifu-root]')) continue
1270
- const parent = row.parentElement
1271
- if (!parent) continue
1272
- const list = parents.get(parent) || []
1273
- list.push(row)
1274
- parents.set(parent, list)
1298
+ // Keep rows hidden by our own collapsed state in the group so the
1299
+ // header stays clickable; skip rows hidden inside a folded
1300
+ // Workspace or a stale search tree.
1301
+ if (row.getClientRects().length === 0 && !row.classList.contains('ifu-sidebar-hidden')) continue
1302
+ rows.push(row)
1275
1303
  }
1276
1304
  }
1277
- const activeParents = new Set(parents.keys())
1278
- for (const header of document.querySelectorAll('[data-ifu-sidebar-group]')) {
1279
- if (!activeParents.has(header.parentElement)) header.remove()
1280
- }
1281
- for (const [parent, rows] of parents) {
1282
- let header = parent.querySelector(':scope > [data-ifu-sidebar-group]')
1283
- if (!header) {
1284
- header = document.createElement('button')
1285
- header.type = 'button'
1286
- header.className = 'ifu-sidebar-group'
1287
- header.setAttribute('data-ifu-sidebar-group', '')
1288
- header.addEventListener('click', (event) => {
1289
- event.preventDefault()
1290
- event.stopPropagation()
1291
- toggleSidebarBranches()
1292
- })
1293
- }
1294
- const first = rows[0]
1295
- if (header.nextSibling !== first) parent.insertBefore(header, first)
1296
- const label = `${state.sidebarCollapsed ? '▸' : '▾'} 追问分支 · ${rows.length}`
1297
- if (header.textContent !== label) header.textContent = label
1298
- for (const row of rows) row.classList.toggle('ifu-sidebar-hidden', state.sidebarCollapsed)
1299
- // Keep the main conversation row above its branch rows even when the
1300
- // host sort places a recently used branch first. Moving the row at
1301
- // the React-owned DOM level is deliberate: the Session list is not a
1302
- // flex/order container, and the collapsible group header sits here.
1303
- const mainId = state.mainSessionId
1304
- const mainRow = mainId
1305
- ? parent.querySelector(`:scope > [data-row-key="session:${cssEscape(mainId)}"]`)
1306
- : null
1307
- if (mainRow && mainRow !== header
1305
+ rows.sort((a, b) => a === b ? 0
1306
+ : (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1)
1307
+ if (rows.length === 0) {
1308
+ for (const header of document.querySelectorAll('[data-ifu-sidebar-group]')) header.remove()
1309
+ for (const row of document.querySelectorAll('.ifu-sidebar-hidden')) row.classList.remove('ifu-sidebar-hidden')
1310
+ return
1311
+ }
1312
+ const first = rows[0]
1313
+ const firstParent = first.parentElement
1314
+ if (!firstParent) return
1315
+ let header = firstParent.querySelector(':scope > [data-ifu-sidebar-group]')
1316
+ if (!header) {
1317
+ header = document.createElement('button')
1318
+ header.type = 'button'
1319
+ header.className = 'ifu-sidebar-group'
1320
+ header.setAttribute('data-ifu-sidebar-group', '')
1321
+ header.addEventListener('click', (event) => {
1322
+ event.preventDefault()
1323
+ event.stopPropagation()
1324
+ toggleSidebarBranches()
1325
+ })
1326
+ }
1327
+ // Keep one shared header, inside the first branch wrapper. Putting it
1328
+ // at the animated tree root would be clipped or ignored by the row
1329
+ // animation layer; a wrapper child participates in the same layout as
1330
+ // the original per-row header did.
1331
+ for (const other of document.querySelectorAll('[data-ifu-sidebar-group]')) {
1332
+ if (other !== header) other.remove()
1333
+ }
1334
+ if (header.nextSibling !== first) firstParent.insertBefore(header, first)
1335
+ const label = `${state.sidebarCollapsed ? '▸' : '▾'} ${app.t('sidebarGroup')} · ${rows.length}`
1336
+ if (header.textContent !== label) header.textContent = label
1337
+ for (const row of rows) row.classList.toggle('ifu-sidebar-hidden', state.sidebarCollapsed)
1338
+
1339
+ const branchSessionIds = ids
1340
+ const rootSessionIds = new Set(state.order
1341
+ .map(id => state.branches[id]?.rootSessionId)
1342
+ .filter(Boolean))
1343
+ const sessionIdOf = (row) => {
1344
+ const key = row.getAttribute('data-row-key') || ''
1345
+ return key.startsWith('session:') ? key.slice('session:'.length) : null
1346
+ }
1347
+ const selectedRow = Array.from(document.querySelectorAll('[data-row-key^="session:"][aria-selected="true"]'))
1348
+ .find(row => row.getClientRects().length > 0) || null
1349
+ // A branch row can be selected too (opening it as the main view). Only a
1350
+ // root/main Session may be pinned above the branch group; otherwise
1351
+ // clicking a branch would make that branch jump above its siblings.
1352
+ const selectedMain = selectedRow && rootSessionIds.has(sessionIdOf(selectedRow))
1353
+ ? selectedRow
1354
+ : null
1355
+ let mainId = state.mainSessionId
1356
+ if (mainId && branchSessionIds.has(mainId)) {
1357
+ mainId = branchBySession(mainId)?.rootSessionId || null
1358
+ }
1359
+ const mainRow = selectedMain || (mainId
1360
+ ? Array.from(document.querySelectorAll(`[data-row-key="session:${cssEscape(mainId)}"]`))
1361
+ .find(row => row.getClientRects().length > 0) || null
1362
+ : null)
1363
+ if (!mainRow) return
1364
+ if (mainRow.parentElement === firstParent) {
1365
+ if (mainRow !== header
1308
1366
  && (mainRow.compareDocumentPosition(header) & Node.DOCUMENT_POSITION_FOLLOWING) === 0) {
1309
- parent.insertBefore(mainRow, header)
1367
+ firstParent.insertBefore(mainRow, header)
1310
1368
  }
1369
+ return
1311
1370
  }
1312
- // Keep row classes correct even when no parent could be found this pass.
1313
- for (const id of ids) {
1314
- for (const row of document.querySelectorAll(`[data-row-key="session:${cssEscape(id)}"]`)) {
1315
- if (!row.closest('[data-ifu-root]')) row.classList.toggle('ifu-sidebar-hidden', state.sidebarCollapsed)
1316
- }
1371
+ let common = mainRow.parentElement
1372
+ while (common && !common.contains(firstParent)) common = common.parentElement
1373
+ if (!common || common === document.body || common === document.documentElement) return
1374
+ const directChild = (node) => {
1375
+ let element = node
1376
+ while (element && element.parentElement !== common) element = element.parentElement
1377
+ return element && element.parentElement === common ? element : null
1378
+ }
1379
+ const mainChild = directChild(mainRow)
1380
+ const branchChild = directChild(firstParent)
1381
+ if (mainChild && branchChild && mainChild !== branchChild
1382
+ && (mainChild.compareDocumentPosition(branchChild) & Node.DOCUMENT_POSITION_FOLLOWING) === 0) {
1383
+ common.insertBefore(mainChild, branchChild)
1317
1384
  }
1318
1385
  }
1319
1386
 
@@ -1379,7 +1446,7 @@ window.__ModuleLoader__.load({
1379
1446
  // giving up on this attempt.
1380
1447
  if (navigator && attempt === 0) navigator.click()
1381
1448
  if (attempt < 14) setTimeout(() => scrollBranchIntoView(branch, attempt + 1), 100)
1382
- else notify(`#${branch.number} 的原文当前不可见,可能已被折叠或尚未加载`)
1449
+ else notify(`#${branch.number} ${app.t('anchorMissing')}`)
1383
1450
  return
1384
1451
  }
1385
1452
  if (navigator && attempt === 0) navigator.click()
@@ -1807,14 +1874,14 @@ window.__ModuleLoader__.load({
1807
1874
  }
1808
1875
  return h('div', { className: 'ifu-compose' },
1809
1876
  h('div', { className: 'ifu-quote', title: branch.anchor.quote },
1810
- h('div', { className: 'ifu-quote-label' }, `${DICT.zh.quote} · ${DICT.zh.basedOn} ${branch.parentBranchId ? `#${app.state.branches[branch.parentBranchId]?.number ?? ''}` : DICT.zh.main}`),
1877
+ h('div', { className: 'ifu-quote-label' }, `${app.t('quote')} · ${app.t('basedOn')} ${branch.parentBranchId ? `#${app.state.branches[branch.parentBranchId]?.number ?? ''}` : app.t('main')}`),
1811
1878
  h('div', { className: 'ifu-quote-text' }, branch.anchor.quote.length > 220 ? `${branch.anchor.quote.slice(0, 219)}…` : branch.anchor.quote),
1812
1879
  ),
1813
1880
  h('textarea', {
1814
1881
  ref: areaRef,
1815
1882
  className: 'ifu-textarea',
1816
1883
  value: text,
1817
- placeholder: DICT.zh.empty,
1884
+ placeholder: app.t('empty'),
1818
1885
  rows: 3,
1819
1886
  onChange: event => { setText(event.target.value); app.setDraftText(branch.id, event.target.value) },
1820
1887
  onKeyDown: event => {
@@ -1826,14 +1893,14 @@ window.__ModuleLoader__.load({
1826
1893
  }),
1827
1894
  branch.error ? h('div', { className: 'ifu-error', role: 'alert' }, branch.error) : null,
1828
1895
  h('div', { className: 'ifu-compose-foot' },
1829
- h('span', { className: 'ifu-hint' }, DICT.zh.sendHint),
1896
+ h('span', { className: 'ifu-hint' }, app.t('sendHint')),
1830
1897
  h('div', { className: 'ifu-grow' }),
1831
1898
  h('button', {
1832
1899
  type: 'button',
1833
1900
  className: 'ifu-send',
1834
1901
  disabled: sending,
1835
1902
  onClick: submit,
1836
- }, sending ? DICT.zh.sendingAction : (branch.error ? DICT.zh.retry : DICT.zh.send)),
1903
+ }, sending ? app.t('sendingAction') : (branch.error ? app.t('retry') : app.t('send'))),
1837
1904
  ),
1838
1905
  )
1839
1906
  }
@@ -1842,7 +1909,7 @@ window.__ModuleLoader__.load({
1842
1909
  const { branch, SessionProvider, renderSlot } = props
1843
1910
  const reference = app.refs.get(branch.id)
1844
1911
  if (!reference) {
1845
- return h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending)
1912
+ return h('div', { className: 'ifu-loading' }, branch.error || app.t('sending'))
1846
1913
  }
1847
1914
  return h(SessionProvider, { session: reference },
1848
1915
  h('div', {
@@ -1890,18 +1957,18 @@ window.__ModuleLoader__.load({
1890
1957
  ...colorVars(branch.color),
1891
1958
  },
1892
1959
  role: 'dialog',
1893
- 'aria-label': `${DICT.zh.branchN} #${branch.number} ${DICT.zh.input}`,
1960
+ 'aria-label': `${app.t('branchN')} #${branch.number} ${app.t('input')}`,
1894
1961
  },
1895
1962
  h('header', { className: 'ifu-head', onPointerDown: event => app.startPanelDrag(branch.id, event, 'input') },
1896
1963
  h('span', { className: 'ifu-dot' }),
1897
1964
  h('div', { className: 'ifu-title-wrap' },
1898
- h('div', { className: 'ifu-title' }, `${DICT.zh.input} · #${branch.number}`),
1965
+ h('div', { className: 'ifu-title' }, `${app.t('input')} · #${branch.number}`),
1899
1966
  h('div', { className: 'ifu-sub' }, statusLabel(branch)),
1900
1967
  ),
1901
1968
  h('div', { className: 'ifu-grow' }),
1902
- h(IconButton, { title: DICT.zh.collapse, onClick: () => app.collapse(branch.id) }, '—'),
1969
+ h(IconButton, { title: app.t('collapse'), onClick: () => app.collapse(branch.id) }, '—'),
1903
1970
  branch.sessionId ? null : h(IconButton, {
1904
- title: DICT.zh.deleteDraft,
1971
+ title: app.t('deleteDraft'),
1905
1972
  className: 'ifu-danger',
1906
1973
  onClick: () => app.deleteDraft(branch.id),
1907
1974
  }, '⌫'),
@@ -1911,7 +1978,7 @@ window.__ModuleLoader__.load({
1911
1978
  ref: areaRef,
1912
1979
  className: 'ifu-textarea',
1913
1980
  value: text,
1914
- placeholder: DICT.zh.empty,
1981
+ placeholder: app.t('empty'),
1915
1982
  rows: 3,
1916
1983
  onChange: event => { setText(event.target.value); app.setInputText(branch.id, event.target.value) },
1917
1984
  onKeyDown: event => {
@@ -1923,14 +1990,14 @@ window.__ModuleLoader__.load({
1923
1990
  }),
1924
1991
  branch.error ? h('div', { className: 'ifu-error', role: 'alert' }, branch.error) : null,
1925
1992
  h('div', { className: 'ifu-compose-foot' },
1926
- h('span', { className: 'ifu-hint' }, DICT.zh.sendHint),
1993
+ h('span', { className: 'ifu-hint' }, app.t('sendHint')),
1927
1994
  h('div', { className: 'ifu-grow' }),
1928
1995
  h('button', {
1929
1996
  type: 'button',
1930
1997
  className: 'ifu-send',
1931
1998
  disabled: sending,
1932
1999
  onClick: submit,
1933
- }, sending ? DICT.zh.sendingAction : DICT.zh.send),
2000
+ }, sending ? app.t('sendingAction') : app.t('send')),
1934
2001
  ),
1935
2002
  ),
1936
2003
  ...['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'].map(direction => h('div', {
@@ -1946,7 +2013,7 @@ window.__ModuleLoader__.load({
1946
2013
  useVersion()
1947
2014
  if (!branch.prompted) return h(InputWindow, { key: `${branch.id}-input`, branch })
1948
2015
  const parent = branch.parentBranchId ? app.state.branches[branch.parentBranchId] : null
1949
- const basis = parent ? `${DICT.zh.basedOn} #${parent.number}` : `${DICT.zh.basedOn} ${DICT.zh.main}`
2016
+ const basis = parent ? `${app.t('basedOn')} #${parent.number}` : `${app.t('basedOn')} ${app.t('main')}`
1950
2017
  const reference = app.refs.get(branch.id)
1951
2018
  const showThread = branch.prompted && reference && !branch.loading
1952
2019
  const showDraft = !branch.prompted
@@ -1967,20 +2034,20 @@ window.__ModuleLoader__.load({
1967
2034
  ...colorVars(branch.color),
1968
2035
  },
1969
2036
  role: 'dialog',
1970
- 'aria-label': `${DICT.zh.branchN} #${branch.number}`,
2037
+ 'aria-label': `${app.t('branchN')} #${branch.number}`,
1971
2038
  },
1972
2039
  h('header', { className: 'ifu-head', onPointerDown: event => app.startPanelDrag(branch.id, event) },
1973
2040
  h('span', { className: 'ifu-dot' }),
1974
2041
  h('div', { className: 'ifu-title-wrap' },
1975
- h('div', { className: 'ifu-title' }, `${DICT.zh.branchN} #${branch.number}`),
2042
+ h('div', { className: 'ifu-title' }, `${app.t('branchN')} #${branch.number}`),
1976
2043
  h('div', { className: 'ifu-sub' }, `${basis} · ${statusLabel(branch)}`),
1977
2044
  ),
1978
2045
  h('div', { className: 'ifu-grow' }),
1979
- h(IconButton, { title: DICT.zh.expand, onClick: () => app.promote(branch.id), disabled: !branch.prompted || !branch.sessionId }, '↗'),
1980
- h(IconButton, { title: DICT.zh.collapse, onClick: () => app.collapse(branch.id) }, '—'),
2046
+ h(IconButton, { title: app.t('expand'), onClick: () => app.promote(branch.id), disabled: !branch.prompted || !branch.sessionId }, '↗'),
2047
+ h(IconButton, { title: app.t('collapse'), onClick: () => app.collapse(branch.id) }, '—'),
1981
2048
  branch.sessionId || branch.prompted
1982
- ? h(IconButton, { title: DICT.zh.close, onClick: () => app.collapse(branch.id) }, '✕')
1983
- : h(IconButton, { title: DICT.zh.deleteDraft, className: 'ifu-danger', onClick: () => app.deleteDraft(branch.id) }, '⌫'),
2049
+ ? h(IconButton, { title: app.t('close'), onClick: () => app.collapse(branch.id) }, '✕')
2050
+ : h(IconButton, { title: app.t('deleteDraft'), className: 'ifu-danger', onClick: () => app.deleteDraft(branch.id) }, '⌫'),
1984
2051
  ),
1985
2052
  h('div', { className: 'ifu-panel-body' },
1986
2053
  showThread
@@ -1988,12 +2055,12 @@ window.__ModuleLoader__.load({
1988
2055
  : showDraft
1989
2056
  ? h('div', { className: 'ifu-compose-center' },
1990
2057
  h('div', { className: 'ifu-draft-quote' },
1991
- h('div', { className: 'ifu-quote-label' }, `${DICT.zh.quote} · ${basis}`),
2058
+ h('div', { className: 'ifu-quote-label' }, `${app.t('quote')} · ${basis}`),
1992
2059
  h('div', { className: 'ifu-quote-text' }, branch.anchor.quote),
1993
- h('div', { className: 'ifu-draft-wait' }, '在下方输入浮窗中提问'),
2060
+ h('div', { className: 'ifu-draft-wait' }, app.t('draftWait')),
1994
2061
  ),
1995
2062
  )
1996
- : h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending),
2063
+ : h('div', { className: 'ifu-loading' }, branch.error || app.t('sending')),
1997
2064
  ),
1998
2065
  ...['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'].map(direction => h('div', {
1999
2066
  key: direction,
@@ -2008,11 +2075,11 @@ window.__ModuleLoader__.load({
2008
2075
  function StageQuote(props) {
2009
2076
  const { branch } = props
2010
2077
  const parent = branch.parentBranchId ? app.state.branches[branch.parentBranchId] : null
2011
- const basis = parent ? `#${parent.number}` : DICT.zh.main
2078
+ const basis = parent ? `#${parent.number}` : app.t('main')
2012
2079
  return h('div', { className: 'ifu-stage-quote', title: branch.anchor.quote },
2013
2080
  h('div', { className: 'ifu-stage-quote-label' },
2014
2081
  h('span', { className: 'ifu-dot' }),
2015
- `${DICT.zh.quote} · ${DICT.zh.basedOn} ${basis}`,
2082
+ `${app.t('quote')} · ${app.t('basedOn')} ${basis}`,
2016
2083
  ),
2017
2084
  h('div', { className: 'ifu-stage-quote-text' }, branch.anchor.quote),
2018
2085
  )
@@ -2034,7 +2101,7 @@ window.__ModuleLoader__.load({
2034
2101
  observer.observe(host, { childList: true, subtree: true, characterData: true })
2035
2102
  return () => observer.disconnect()
2036
2103
  }, [branch.id])
2037
- if (!reference) return h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending)
2104
+ if (!reference) return h('div', { className: 'ifu-loading' }, branch.error || app.t('sending'))
2038
2105
  return h(SessionProvider, { session: reference },
2039
2106
  h('div', {
2040
2107
  ref: hostRef,
@@ -2061,24 +2128,24 @@ window.__ModuleLoader__.load({
2061
2128
  ? rows.filter(row => pathIds.has(row.branch.id))
2062
2129
  : rows
2063
2130
  const contextText = path.length > 0
2064
- ? `${DICT.zh.main}${path.map(branch => ` → #${branch.number}`).join('')}`
2065
- : DICT.zh.main
2131
+ ? `${app.t('main')}${path.map(branch => ` → #${branch.number}`).join('')}`
2132
+ : app.t('main')
2066
2133
  return h('aside', { className: 'ifu-tree', 'data-ifu-no-select': '' },
2067
2134
  h('div', { className: 'ifu-tree-head' },
2068
- h('span', null, DICT.zh.tree),
2135
+ h('span', null, app.t('tree')),
2069
2136
  h('div', { className: 'ifu-tree-tabs' },
2070
2137
  h('button', {
2071
2138
  type: 'button',
2072
2139
  className: `ifu-tree-tab ${scope === 'path' ? 'is-active' : ''}`,
2073
2140
  onClick: () => setScope('path'),
2074
- }, '路径'),
2141
+ }, app.t('path')),
2075
2142
  h('button', {
2076
2143
  type: 'button',
2077
2144
  className: `ifu-tree-tab ${scope === 'all' ? 'is-active' : ''}`,
2078
2145
  onClick: () => setScope('all'),
2079
- }, '全部'),
2146
+ }, app.t('all')),
2080
2147
  ),
2081
- h('button', { type: 'button', className: 'ifu-icon', title: DICT.zh.close, onClick: () => app.toggleTree() }, '✕'),
2148
+ h('button', { type: 'button', className: 'ifu-icon', title: app.t('close'), onClick: () => app.toggleTree() }, '✕'),
2082
2149
  ),
2083
2150
  h('div', { className: 'ifu-tree-list' },
2084
2151
  h('div', {
@@ -2093,15 +2160,15 @@ window.__ModuleLoader__.load({
2093
2160
  },
2094
2161
  },
2095
2162
  h('span', { className: 'ifu-dot' }),
2096
- h('span', { className: 'ifu-tree-root-name' }, DICT.zh.main),
2163
+ h('span', { className: 'ifu-tree-root-name' }, app.t('main')),
2097
2164
  h('span', { className: 'ifu-tree-root-tag' }, 'ROOT'),
2098
- h('span', { className: 'ifu-tree-root-meta' }, '上下文起点'),
2165
+ h('span', { className: 'ifu-tree-root-meta' }, app.t('contextStart')),
2099
2166
  ),
2100
2167
  visibleRows.map(({ branch, depth }) => {
2101
2168
  const isDraft = !branch.sessionId && !branch.prompted
2102
2169
  const parent = branch.parentBranchId ? state.branches[branch.parentBranchId] : null
2103
- const parentLabel = parent ? `${DICT.zh.main} · #${parent.number}` : DICT.zh.main
2104
- const title = branch.question || branch.anchor.quote || '待提问'
2170
+ const parentLabel = parent ? `${app.t('main')} · #${parent.number}` : app.t('main')
2171
+ const title = branch.question || branch.anchor.quote || app.t('waiting')
2105
2172
  const inPath = pathIds.has(branch.id)
2106
2173
  return h('div', {
2107
2174
  key: branch.id,
@@ -2127,34 +2194,34 @@ window.__ModuleLoader__.load({
2127
2194
  } else if (branch.prompted && branch.sessionId) app.promote(branch.id)
2128
2195
  else app.openPanel(branch.id)
2129
2196
  },
2130
- title: `${DICT.zh.switchBranch} #${branch.number}`,
2197
+ title: `${app.t('switchBranch')} #${branch.number}`,
2131
2198
  },
2132
2199
  h('div', { className: 'ifu-tree-top' },
2133
2200
  h('span', { className: 'ifu-dot' }),
2134
2201
  h('span', { className: 'ifu-tree-num' }, `#${branch.number}`),
2135
2202
  h('span', { className: 'ifu-tree-title' }, snippet(title, 36)),
2136
2203
  branch.id === activeId
2137
- ? h('span', { className: 'ifu-tree-tag' }, DICT.zh.current)
2204
+ ? h('span', { className: 'ifu-tree-tag' }, app.t('current'))
2138
2205
  : inPath
2139
- ? h('span', { className: 'ifu-tree-tag is-context' }, '上下文')
2206
+ ? h('span', { className: 'ifu-tree-tag is-context' }, app.t('context'))
2140
2207
  : null,
2141
2208
  isDraft ? h('button', {
2142
2209
  type: 'button',
2143
2210
  className: 'ifu-tree-delete',
2144
- title: DICT.zh.deleteDraft,
2211
+ title: app.t('deleteDraft'),
2145
2212
  onClick: (event) => { event.stopPropagation(); app.deleteDraft(branch.id) },
2146
2213
  }, '⌫') : null,
2147
2214
  ),
2148
2215
  h('div', { className: 'ifu-tree-quote' }, snippet(branch.anchor.quote, 72)),
2149
2216
  h('div', { className: 'ifu-tree-meta' },
2150
- h('span', null, `${DICT.zh.basedOn} ${parentLabel}`),
2217
+ h('span', null, `${app.t('basedOn')} ${parentLabel}`),
2151
2218
  h('span', null, statusLabel(branch)),
2152
2219
  ),
2153
2220
  )
2154
2221
  }),
2155
2222
  ),
2156
2223
  h('div', { className: 'ifu-tree-note' },
2157
- `当前模型上下文:${contextText}。兄弟分支不在其中。`,
2224
+ app.t('treeNote', { context: contextText }),
2158
2225
  ),
2159
2226
  )
2160
2227
  }
@@ -2178,23 +2245,23 @@ window.__ModuleLoader__.load({
2178
2245
  type: 'button',
2179
2246
  className: 'ifu-stage-back',
2180
2247
  onClick: () => index <= 0 ? app.stageClose() : app.stageGo(-1),
2181
- title: index <= 0 ? DICT.zh.main : DICT.zh.back,
2182
- }, `← ${DICT.zh.back}`),
2248
+ title: index <= 0 ? app.t('main') : app.t('back'),
2249
+ }, `← ${app.t('back')}`),
2183
2250
  h('span', { className: 'ifu-stage-count' }, `${index + 1}/${total}`),
2184
2251
  h('button', {
2185
2252
  type: 'button',
2186
2253
  className: 'ifu-stage-back',
2187
2254
  disabled: index >= total - 1,
2188
2255
  onClick: () => app.stageGo(1),
2189
- }, DICT.zh.forward, ' →'),
2256
+ }, app.t('forward'), ' →'),
2190
2257
  h('span', { className: 'ifu-stage-divider' }),
2191
2258
  h('div', { className: 'ifu-crumbs' },
2192
2259
  h('button', {
2193
2260
  type: 'button',
2194
2261
  className: 'ifu-crumb-btn',
2195
2262
  onClick: () => app.stageClose(),
2196
- title: DICT.zh.main,
2197
- }, DICT.zh.main),
2263
+ title: app.t('main'),
2264
+ }, app.t('main')),
2198
2265
  ...path.flatMap(item => [
2199
2266
  h('span', { key: `sep-${item.id}`, className: 'ifu-crumb-sep' }, '›'),
2200
2267
  h('button', {
@@ -2205,23 +2272,23 @@ window.__ModuleLoader__.load({
2205
2272
  if (item.id === branch.id) return
2206
2273
  app.promote(item.id)
2207
2274
  },
2208
- title: `${DICT.zh.switchBranch} #${item.number}`,
2275
+ title: `${app.t('switchBranch')} #${item.number}`,
2209
2276
  }, `#${item.number}`),
2210
2277
  ]),
2211
2278
  ),
2212
- h('span', { className: 'ifu-context-chip' }, `上下文 · ${path.length} ${DICT.zh.stages}`),
2279
+ h('span', { className: 'ifu-context-chip' }, `${app.t('context')} · ${path.length} ${app.t('stages')}`),
2213
2280
  h('div', { className: 'ifu-grow' }),
2214
2281
  h('span', { className: 'ifu-stage-status' }, statusLabel(branch)),
2215
- h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.toggleTree() }, '▤ ', DICT.zh.tree),
2216
- h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageRestore() }, '↘ ', DICT.zh.restore),
2217
- h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageClose() }, '✕ ', DICT.zh.close),
2282
+ h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.toggleTree() }, '▤ ', app.t('tree')),
2283
+ h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageRestore() }, '↘ ', app.t('restore')),
2284
+ h('button', { type: 'button', className: 'ifu-stage-action', onClick: () => app.stageClose() }, '✕ ', app.t('close')),
2218
2285
  ),
2219
- h('div', { className: 'ifu-stage-tip' }, DICT.zh.stageHint),
2286
+ h('div', { className: 'ifu-stage-tip' }, app.t('stageHint')),
2220
2287
  h('div', { className: 'ifu-stage-body' },
2221
2288
  h(StageQuote, { branch }),
2222
2289
  reference && !branch.loading
2223
2290
  ? h(StageThread, { key: branch.id, branch, SessionProvider, renderSlot })
2224
- : h('div', { className: 'ifu-loading' }, branch.error || DICT.zh.sending),
2291
+ : h('div', { className: 'ifu-loading' }, branch.error || app.t('sending')),
2225
2292
  ),
2226
2293
  h(BranchTree),
2227
2294
  )
@@ -2358,7 +2425,7 @@ window.__ModuleLoader__.load({
2358
2425
  className: 'ifu-dock-collapsed',
2359
2426
  'data-ifu-dock': '',
2360
2427
  onClick: () => app.toggleDock(),
2361
- }, `◉ ${DICT.zh.dock} · ${branches.length}`)
2428
+ }, `◉ ${app.t('dock')} · ${branches.length}`)
2362
2429
  }
2363
2430
  const dockStyle = state.dock
2364
2431
  ? {
@@ -2372,11 +2439,11 @@ window.__ModuleLoader__.load({
2372
2439
  : {}
2373
2440
  return h('aside', { className: 'ifu-dock', 'data-ifu-dock': '', style: dockStyle },
2374
2441
  h('div', { className: 'ifu-dock-head', onPointerDown: event => app.startDockDrag(event) },
2375
- h('span', null, `${DICT.zh.dock} · ${branches.length}`),
2442
+ h('span', null, `${app.t('dock')} · ${branches.length}`),
2376
2443
  h('button', {
2377
2444
  type: 'button',
2378
2445
  className: 'ifu-dock-toggle',
2379
- title: DICT.zh.collapseDock,
2446
+ title: app.t('collapseDock'),
2380
2447
  onClick: () => app.closeDock(),
2381
2448
  }, '—'),
2382
2449
  ),
@@ -2395,7 +2462,7 @@ window.__ModuleLoader__.load({
2395
2462
  event.preventDefault()
2396
2463
  app.revealBranch(branch.id)
2397
2464
  },
2398
- title: `${DICT.zh.switchBranch}: #${branch.number}`,
2465
+ title: `${app.t('switchBranch')}: #${branch.number}`,
2399
2466
  },
2400
2467
  h('span', { className: 'ifu-dot', style: colorVars(branch.color) }),
2401
2468
  h('span', { className: 'ifu-dock-num' }, `#${branch.number}`),
@@ -2404,7 +2471,7 @@ window.__ModuleLoader__.load({
2404
2471
  isDraft ? h('button', {
2405
2472
  type: 'button',
2406
2473
  className: 'ifu-dock-delete',
2407
- title: DICT.zh.deleteDraft,
2474
+ title: app.t('deleteDraft'),
2408
2475
  onClick: (event) => { event.stopPropagation(); app.deleteDraft(branch.id) },
2409
2476
  }, '⌫') : null,
2410
2477
  )
@@ -2426,14 +2493,14 @@ window.__ModuleLoader__.load({
2426
2493
  className: 'ifu-chip',
2427
2494
  'data-ifu-chip': '',
2428
2495
  style: { left: `${chip.x}px`, top: `${chip.y}px` },
2429
- title: DICT.zh.chipHint,
2496
+ title: app.t('chipHint'),
2430
2497
  onMouseDown: event => event.preventDefault(),
2431
2498
  onClick: () => {
2432
2499
  const x = clamp(chip.x - 120, 12, window.innerWidth - PANEL_WIDTH - 12)
2433
2500
  const y = clamp(chip.y + 38, 12, window.innerHeight - 320)
2434
2501
  app.openDraft(chip.anchor, x, y)
2435
2502
  },
2436
- }, '✦ ', DICT.zh.chip)
2503
+ }, '✦ ', app.t('chip'))
2437
2504
  }
2438
2505
 
2439
2506
  function Notice() {
@@ -2662,6 +2729,7 @@ window.__ModuleLoader__.load({
2662
2729
  app = createApp(ctx)
2663
2730
  app.load()
2664
2731
  ctx.effect(() => ctx.locale.register(NS, { zh: DICT.zh, en: DICT.en }), 'inline-followup: locale')
2732
+ ctx.effect(() => ctx.on('locale/change', () => app.emit()), 'inline-followup: locale-change')
2665
2733
  ctx.effect(() => app.install(), 'inline-followup: lifetime')
2666
2734
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
2667
2735
  name: 'shell.overlay',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ask-from-text",
3
- "version": "0.1.13",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "description": "Ask a follow-up from selected text in a DeepSeek Harness answer, in a real Session branch with a tree of context.",
6
6
  "exports": {