dsh-trivium 0.4.7 → 0.4.8

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/lib/client.js CHANGED
@@ -9,69 +9,333 @@ window.__ModuleLoader__.load({
9
9
  const { jsx, jsxs } = require("react/jsx-runtime");
10
10
  const { Button } = require("@deepseek-ai/dsh-client-ui-primitives");
11
11
 
12
- const L = {
13
- nav: "Trivium 记忆",
14
- sub: "按节点和边记。默认不自动灌全文。归档后 find 不再返回;删除从 .tdb 去掉且不可恢复。",
15
- loading: "加载中…",
16
- empty: "还没有打开过工作区,或当前库为空。",
17
- path: "当前 .tdb",
18
- nodes: "节点数",
19
- tokens: "上次短地图 token",
20
- recall: "注入策略(三选一,默认关)",
21
- recallHint: "与 autoRecall、实体名折中互斥。改完点右上角保存。",
22
- recallOff: "关(默认)",
23
- recallOffHint: "仅 session-start 短地图(≤400 token)。模型自行 ctx_find / ctx_read。",
24
- recallAuto: "autoRecall",
25
- recallAutoHint: "本步含直接用户文本时注入最多 3 条 L0(≤300 token)。不调用 ctx_find 工具。",
26
- recallAnchor: "实体名折中",
27
- recallAnchorHint: "用户文本命中已有 entity 的 name/aliases 时,注入该实体 1 跳 about/decided/broke/fixed 邻居(≤300 token)。",
28
- extract: "compaction/end 抽取",
29
- extractHint: "规则 + 一次小 prompt;失败只记日志。turn 结束后约 12 秒空闲会再抽一次,下次 session-start 也会重放 pending。每轮写入正文合计 ≤3000 字、最多 24 条。",
30
- markdown: "Markdown 投影",
31
- markdownHint: "只读导出,带边(- 鉴权走 header X —about→ AuthGateway)。给编辑器和 git 看。改完回写用 JSON 导入或本页编辑,不解析自由散文。",
32
- exportMd: "导出 Markdown",
33
- workbuddy: "WorkBuddy 一次性导入",
34
- workbuddyHint: "只读 ~/.workbuddy/MEMORY.md 与工作区 .workbuddy/memory/MEMORY.md。偏严宁漏:闲聊、一次性改文件、密钥、无规则线索的段落跳过。不持续同步。每批同样 ≤3000 字 / 24 条。",
35
- workbuddyImport: "导入找到的 MEMORY.md",
36
- workbuddyNone: "当前没有找到这些文件。",
37
- workbuddyFound: "找到",
38
- embed: "远程 embedding(OpenAI 兼容,默认关)",
39
- embedHint: "失败退回关键词 + 图检索。需单独的 embeddings endpoint;官方 DeepSeek chat API 不含 embedding。",
40
- embedReady: "已就绪",
41
- embedWait: "未就绪:打开开关并填写 http(s) URL 后保存。",
42
- embedUrl: "Embedding URL",
43
- embedModel: "模型",
44
- embedKey: "API Key",
45
- embedKeyHint: "已保存则留空不改。也可设环境变量 DSH_TRIVIUM_EMBED_KEY。",
46
- embedBackfill: "回填已有节点向量",
47
- list: "记忆条目",
48
- filter: "类型",
49
- search: "搜索",
50
- all: "全部",
51
- archive: "归档",
52
- remove: "删除",
53
- saveNode: "保存条目",
54
- merge: "合并进来",
55
- mergePick: "并入本条…",
56
- exportJson: "导出 JSON",
57
- importJson: "导入 JSON",
58
- archiveHint: "归档:软删除,find / 短地图不再出现。删除:从库里去掉,不可恢复。合并:同类型节点并入本条,对方 same_as 后归档。",
59
- save: "保存设置",
60
- saved: "已保存",
61
- refresh: "刷新",
62
- noMatch: "没有匹配的节点。换个关键词,或清空搜索。",
63
- showStale: "显示过期决策",
64
- neighbors: "邻居(业务边)",
65
- aboutThis: "只看挂在这上面的",
66
- aboutOn: "正在看挂在",
67
- clearAbout: "返回全部",
68
- stale: "过期",
69
- until: "until",
70
- name: "名称",
71
- text: "正文",
72
- aliases: "别名",
12
+ const I18N = {
13
+ zh: {
14
+ nav: "Trivium 记忆",
15
+ sub: "按节点和边记。默认不自动灌全文。归档后 find 不再返回;删除从 .tdb 去掉且不可恢复。",
16
+ loading: "加载中…",
17
+ empty: "还没有打开过工作区,或当前库为空。",
18
+ path: "当前 .tdb",
19
+ nodes: "节点数",
20
+ tokens: "上次短地图 token",
21
+ recall: "注入策略(三选一,默认关)",
22
+ recallHint: "与 autoRecall、实体名折中互斥。改完点右上角保存。",
23
+ recallOff: "关(默认)",
24
+ recallOffHint: "仅 session-start 短地图(≤400 token)。模型自行 ctx_find / ctx_read。",
25
+ recallAuto: "autoRecall",
26
+ recallAutoHint: "本步含直接用户文本时注入最多 3 条 L0(≤300 token)。不调用 ctx_find 工具。",
27
+ recallAnchor: "实体名折中",
28
+ recallAnchorHint: "用户文本命中已有 entity 的 name/aliases 时,注入该实体 1 跳 about/decided/broke/fixed 邻居(≤300 token)。",
29
+ extract: "compaction/end 抽取",
30
+ extractHint: "规则 + 一次小 prompt;失败只记日志。turn 结束后约 12 秒空闲会再抽一次,下次 session-start 也会重放 pending。每轮写入正文合计 ≤3000 字、最多 24 条。",
31
+ markdown: "Markdown 投影",
32
+ markdownHint: "只读导出,带边(- 鉴权走 header X —about→ AuthGateway)。给编辑器和 git 看。改完回写用 JSON 导入或本页编辑,不解析自由散文。",
33
+ exportMd: "导出 Markdown",
34
+ external: "外部记忆一次性导入",
35
+ externalHint: "发现 WorkBuddy MEMORY.md、Claude Code CLAUDE.md、Codex AGENTS.md。偏严宁漏:闲聊、一次性改文件、密钥、乱码、工具 dump、无规则线索的段落跳过。不持续同步,不导入会话 jsonl。每批同样 ≤3000 字 / 24 条。",
36
+ externalImport: "导入找到的文件",
37
+ externalNone: "当前没有找到这些文件。",
38
+ externalFound: "找到",
39
+ update: "检查更新",
40
+ updateHint: "对比本机安装版本和 npm 上的 latest。有新版时给出安装命令,不会在后台自动升级。",
41
+ updateChecking: "正在检查…",
42
+ updateCurrent: "已是最新",
43
+ updateNewer: "有新版本",
44
+ updateInstalled: "已安装",
45
+ updateLatest: "npm latest",
46
+ updateFailed: "暂时查不到 registry",
47
+ embed: "远程 embedding(OpenAI 兼容,默认关)",
48
+ embedHint: "失败退回关键词 + 图检索。需单独的 embeddings endpoint;官方 DeepSeek chat API 不含 embedding。",
49
+ embedReady: "已就绪",
50
+ embedWait: "未就绪:打开开关并填写 http(s) URL 后保存。",
51
+ embedUrl: "Embedding URL",
52
+ embedModel: "模型",
53
+ embedKey: "API Key",
54
+ embedKeyHint: "已保存则留空不改。也可设环境变量 DSH_TRIVIUM_EMBED_KEY。",
55
+ embedBackfill: "回填已有节点向量",
56
+ list: "记忆条目",
57
+ filter: "类型",
58
+ search: "搜索",
59
+ all: "全部",
60
+ archive: "归档",
61
+ remove: "删除",
62
+ saveNode: "保存条目",
63
+ merge: "合并进来",
64
+ mergePick: "并入本条…",
65
+ exportJson: "导出 JSON",
66
+ importJson: "导入 JSON",
67
+ archiveHint: "归档:软删除,find / 短地图不再出现。删除:从库里去掉,不可恢复。合并:同类型节点并入本条,对方 same_as 后归档。",
68
+ save: "保存设置",
69
+ saved: "已保存",
70
+ savedKey: "已保存",
71
+ refresh: "刷新",
72
+ noMatch: "没有匹配的节点。换个关键词,或清空搜索。",
73
+ showStale: "显示过期决策",
74
+ neighbors: "邻居(业务边)",
75
+ aboutThis: "只看挂在这上面的",
76
+ aboutOn: "正在看挂在",
77
+ clearAbout: "返回全部",
78
+ stale: "过期",
79
+ dirty: "脏",
80
+ until: "until",
81
+ name: "名称",
82
+ text: "正文",
83
+ aliases: "别名",
84
+ aliasesPh: "逗号分隔",
85
+ untilPh: "周五 / 下周 / ISO",
86
+ searchPh: "鉴权 / until / about",
87
+ importOk: "导入 {created} 新建 / {merged} 合并,跳过 {skipped}",
88
+ importFail: "导入失败",
89
+ justNow: "刚刚",
90
+ minutesAgo: "{n} 分钟前",
91
+ hoursAgo: "{n} 小时前",
92
+ daysAgo: "{n} 天前",
93
+ boxNext: "后续",
94
+ boxFork: "分支",
95
+ boxCheck: "检查点",
96
+ forkDefault: "分叉 (1)",
97
+ forkName: "分叉",
98
+ trajectory: "轨迹",
99
+ fork: "分叉",
100
+ openChat: "打开对话",
101
+ chatShort: "对",
102
+ boxTitle: "方框标题",
103
+ renameHint: "双击改名",
104
+ rename: "改名",
105
+ noSummary: "(尚无摘要)",
106
+ backfillNone: "没有发现压缩标记或分叉。没压过的会话本来就只有「后续」。要分段请点「生成检查点」。",
107
+ backfillOk: "已同步 {checks} 个检查点、{forks} 条分叉。",
108
+ cutNeedTurn: "当前对话还没有可切的轮次。先在对话里发一条,再生成检查点。",
109
+ cutOk: "已把当前「后续」收成左边一格(seq {seq}),右边仍是后续。这不压缩 DSH 上下文。",
110
+ cutDup: "这个位置已经切过了(seq {seq})。再聊几轮后再生成。",
111
+ forkOpenFail: "无法打开分叉会话:会话图没有拿到 session.open",
112
+ chatViewFail: "无法切换到对话页:会话图没有拿到同一 chat store 的 setView",
113
+ trajViewFail: "无法切换到轨迹页:会话图没有拿到同一 chat store 的 setView",
114
+ noFork: "当前壳没有 session.fork",
115
+ renameFail: "会话已分叉,但侧边栏改名失败:{err}",
116
+ deleteConfirm: "删除会从 .tdb 里拿掉这 {n} 条,不可恢复。归档只是藏起来,find / 短地图 / 芯片都不再出现。确定删除?",
117
+ sessionMap: "会话图",
118
+ chips: "记忆芯片",
119
+ chipsHint: "共 {n} 条,已钉 {pins}。默认不勾选:不钉就不会按芯片白名单注入,但仍可能被开场短地图点名带上,模型也能 find 到。",
120
+ cutBtn: "生成检查点",
121
+ cutting: "生成中…",
122
+ cutTitle: "把当前「后续」收成左边一格,右边继续当后续。只切情节图,不触发 DSH 压缩。要压上下文请在对话里输入 /compact。",
123
+ updateBtn: "更新检查点",
124
+ updating: "更新中…",
125
+ updateTitle: "读取当前对话里 DSH 已有的压缩标记和侧边栏分叉,补进图。没压过就补不出历史方框。",
126
+ collapse: "收起",
127
+ expand: "展开勾选",
128
+ searchChips: "搜索芯片",
129
+ inheritPins: "分叉时继承钉选",
130
+ addChip: "新增",
131
+ cancelAdd: "取消新增",
132
+ select: "选择",
133
+ cancelSelect: "取消选择",
134
+ chipSelectHint: "点芯片多选。归档:软删除,find / 短地图 / 芯片不再出现,节点还在库里。删除:从 .tdb 去掉,不可恢复。",
135
+ chipClipped: "已截到预算(≤300 token)",
136
+ chipPinHint: "勾选后从当前这段的下一轮注入。空白处拖动画布,滚轮缩放。",
137
+ chipDraftPh: "粘贴或输入要记住的内容。点一次写入,整段就是一条芯片。",
138
+ writePin: "写入并钉选",
139
+ suggested: "建议",
140
+ noChipMatch: "没有匹配的芯片。",
141
+ noChips: "还没有可钉选的 preference / decision / entity。用「新增」写一条。",
142
+ forkDialog: "分叉为新会话",
143
+ forkDialogHint: "这个名字会出现在左侧会话列表里。",
144
+ newSessionName: "新会话名称",
145
+ cancel: "取消",
146
+ forking: "分叉中…",
147
+ noRename: "当前壳没有 session.rename",
148
+ },
149
+ en: {
150
+ nav: "Trivium memory",
151
+ sub: "Stored as nodes and edges. Full text is not auto-injected. Archive hides from find; delete removes from .tdb and cannot be undone.",
152
+ loading: "Loading…",
153
+ empty: "No workspace is open, or the store is empty.",
154
+ path: "Current .tdb",
155
+ nodes: "Nodes",
156
+ tokens: "Last short-map tokens",
157
+ recall: "Injection (pick one, default off)",
158
+ recallHint: "autoRecall and entity-name path are mutually exclusive. Save in the top-right when done.",
159
+ recallOff: "Off (default)",
160
+ recallOffHint: "Session-start short map only (≤400 tokens). The model calls ctx_find / ctx_read itself.",
161
+ recallAuto: "autoRecall",
162
+ recallAutoHint: "When the step has user text, inject at most 3 L0 hits (≤300 tokens). Does not call ctx_find.",
163
+ recallAnchor: "Entity-name path",
164
+ recallAnchorHint: "When user text names an existing entity, inject 1-hop about/decided/broke/fixed neighbors (≤300 tokens).",
165
+ extract: "Extract on compaction/end",
166
+ extractHint: "Rules + one small prompt; failures are logged. Idle ~12s after turn/end also extracts; pending replays on next session-start. Per batch: body ≤3000 chars, at most 24 items.",
167
+ markdown: "Markdown projection",
168
+ markdownHint: "Read-only export with edges (e.g. - auth in header X —about→ AuthGateway). For editors and git. Write back with JSON import or this page, not free prose.",
169
+ exportMd: "Export Markdown",
170
+ external: "One-shot external import",
171
+ externalHint: "Discovers WorkBuddy MEMORY.md, Claude Code CLAUDE.md, and Codex AGENTS.md. Strict: skip chitchat, one-off file edits, secrets, mojibake, tool dumps, and lines with no rule cue. No watch, no session jsonl. Same cap: ≤3000 chars / 24 items.",
172
+ externalImport: "Import found files",
173
+ externalNone: "None of these files were found.",
174
+ externalFound: "Found",
175
+ update: "Check for updates",
176
+ updateHint: "Compare the installed version with npm latest. If a newer version exists, the install command is shown — it does not auto-upgrade.",
177
+ updateChecking: "Checking…",
178
+ updateCurrent: "Up to date",
179
+ updateNewer: "Update available",
180
+ updateInstalled: "Installed",
181
+ updateLatest: "npm latest",
182
+ updateFailed: "Registry unavailable",
183
+ embed: "Remote embedding (OpenAI-compatible, off by default)",
184
+ embedHint: "Failures fall back to keyword + graph search. Needs a separate embeddings endpoint; the official DeepSeek chat API has none.",
185
+ embedReady: "Ready",
186
+ embedWait: "Not ready: turn on and fill an http(s) URL, then save.",
187
+ embedUrl: "Embedding URL",
188
+ embedModel: "Model",
189
+ embedKey: "API Key",
190
+ embedKeyHint: "Leave blank to keep the saved key. Or set DSH_TRIVIUM_EMBED_KEY.",
191
+ embedBackfill: "Backfill vectors",
192
+ list: "Memory entries",
193
+ filter: "Type",
194
+ search: "Search",
195
+ all: "All",
196
+ archive: "Archive",
197
+ remove: "Delete",
198
+ saveNode: "Save entry",
199
+ merge: "Merge in",
200
+ mergePick: "Merge into this…",
201
+ exportJson: "Export JSON",
202
+ importJson: "Import JSON",
203
+ archiveHint: "Archive: soft delete, gone from find / short map. Delete: removed from the store, not recoverable. Merge: same-type node into this one; the other is same_as then archived.",
204
+ save: "Save settings",
205
+ saved: "Saved",
206
+ savedKey: "Saved",
207
+ refresh: "Refresh",
208
+ noMatch: "No matching nodes. Try another keyword, or clear the search.",
209
+ showStale: "Show expired decisions",
210
+ neighbors: "Neighbors (business edges)",
211
+ aboutThis: "Only nodes hanging on this",
212
+ aboutOn: "Showing nodes on",
213
+ clearAbout: "Back to all",
214
+ stale: "expired",
215
+ dirty: "dirty",
216
+ until: "until",
217
+ name: "Name",
218
+ text: "Body",
219
+ aliases: "Aliases",
220
+ aliasesPh: "comma-separated",
221
+ untilPh: "Friday / next week / ISO",
222
+ searchPh: "auth / until / about",
223
+ importOk: "Imported {created} new / {merged} merged, skipped {skipped}",
224
+ importFail: "Import failed",
225
+ justNow: "just now",
226
+ minutesAgo: "{n} min ago",
227
+ hoursAgo: "{n} h ago",
228
+ daysAgo: "{n} d ago",
229
+ boxNext: "Next",
230
+ boxFork: "Branch",
231
+ boxCheck: "Checkpoint",
232
+ forkDefault: "Fork (1)",
233
+ forkName: "Fork",
234
+ trajectory: "Trajectory",
235
+ fork: "Fork",
236
+ openChat: "Open conversation",
237
+ chatShort: "C",
238
+ boxTitle: "Box title",
239
+ renameHint: "Double-click to rename",
240
+ rename: "Rename",
241
+ noSummary: "(no summary yet)",
242
+ backfillNone: "No compaction markers or forks. A never-compacted session only has Next. Split with Create checkpoint.",
243
+ backfillOk: "Synced {checks} checkpoints, {forks} forks.",
244
+ cutNeedTurn: "This conversation has no turn to cut yet. Send a message, then create a checkpoint.",
245
+ cutOk: "Folded current Next into a left box (seq {seq}). Right side is still Next. This does not compact DSH context.",
246
+ cutDup: "Already cut at seq {seq}. Chat a few more turns, then create another.",
247
+ forkOpenFail: "Cannot open the forked session: session map has no session.open",
248
+ chatViewFail: "Cannot switch to Conversation: session map has no shared chat store setView",
249
+ trajViewFail: "Cannot switch to Trajectory: session map has no shared chat store setView",
250
+ noFork: "This host has no session.fork",
251
+ renameFail: "Session forked, but sidebar rename failed: {err}",
252
+ deleteConfirm: "Delete removes these {n} nodes from .tdb and cannot be undone. Archive only hides them from find / short map / chips. Delete anyway?",
253
+ sessionMap: "Session graph",
254
+ chips: "Memory chips",
255
+ chipsHint: "{n} chips, {pins} pinned. Unchecked by default: pinning is a whitelist, but the short map may still name them and the model can still find them.",
256
+ cutBtn: "Create checkpoint",
257
+ cutting: "Creating…",
258
+ cutTitle: "Fold current Next into a left box; the right side stays Next. Plot only — does not compact DSH. Type /compact in the conversation to compact context.",
259
+ updateBtn: "Update checkpoints",
260
+ updating: "Updating…",
261
+ updateTitle: "Read DSH compression markers and sidebar forks in this conversation and backfill the graph. Never compacted still means only Next.",
262
+ collapse: "Collapse",
263
+ expand: "Expand to pin",
264
+ searchChips: "Search chips",
265
+ inheritPins: "Inherit pins on fork",
266
+ addChip: "Add",
267
+ cancelAdd: "Cancel add",
268
+ select: "Select",
269
+ cancelSelect: "Cancel select",
270
+ chipSelectHint: "Click chips to multi-select. Archive: soft delete, gone from find / short map / chips, node stays in .tdb. Delete: removed from .tdb, not recoverable.",
271
+ chipClipped: "Clipped to budget (≤300 tokens)",
272
+ chipPinHint: "Checked chips inject from the next turn of this segment. Drag empty canvas to pan, scroll to zoom.",
273
+ chipDraftPh: "Paste or type a fact. One click writes one chip; a whole paste is still one node.",
274
+ writePin: "Write and pin",
275
+ suggested: "suggested",
276
+ noChipMatch: "No matching chips.",
277
+ noChips: "No preference / decision / entity chips yet. Use Add to write one.",
278
+ forkDialog: "Fork as new session",
279
+ forkDialogHint: "This name appears in the left session list.",
280
+ newSessionName: "New session name",
281
+ cancel: "Cancel",
282
+ forking: "Forking…",
283
+ noRename: "This host has no session.rename",
284
+ },
73
285
  };
74
286
 
287
+ let uiLocale = "zh";
288
+ const localeListeners = new Set();
289
+
290
+ function normalizeLocale(value) {
291
+ const s = String(value || "").toLowerCase();
292
+ if (s.startsWith("en")) return "en";
293
+ if (s.startsWith("zh")) return "zh";
294
+ return "";
295
+ }
296
+
297
+ function setUiLocale(next) {
298
+ const loc = normalizeLocale(next) || "zh";
299
+ if (loc === uiLocale) return;
300
+ uiLocale = loc;
301
+ localeListeners.forEach((fn) => {
302
+ try {
303
+ fn();
304
+ } catch {
305
+ // ignore
306
+ }
307
+ });
308
+ }
309
+
310
+ function t(key, vars) {
311
+ let s = (I18N[uiLocale] && I18N[uiLocale][key]) || I18N.en[key] || I18N.zh[key] || key;
312
+ if (vars) {
313
+ Object.keys(vars).forEach((k) => {
314
+ s = s.split("{" + k + "}").join(String(vars[k]));
315
+ });
316
+ }
317
+ return s;
318
+ }
319
+
320
+ function useT() {
321
+ const [, bump] = react.useState(0);
322
+ react.useEffect(() => {
323
+ const fn = () => bump((n) => n + 1);
324
+ localeListeners.add(fn);
325
+ return () => localeListeners.delete(fn);
326
+ }, []);
327
+ return t;
328
+ }
329
+
330
+ const L = new Proxy(
331
+ {},
332
+ {
333
+ get(_target, key) {
334
+ return t(key);
335
+ },
336
+ },
337
+ );
338
+
75
339
  const TYPES = ["", "entity", "preference", "decision", "experience", "workspace"];
76
340
 
77
341
  const ui = {
@@ -131,6 +395,7 @@ window.__ModuleLoader__.load({
131
395
  }
132
396
 
133
397
  function TriviumCard() {
398
+ useT();
134
399
  const [status, setStatus] = react.useState(null);
135
400
  const [nodes, setNodes] = react.useState([]);
136
401
  const [type, setType] = react.useState("");
@@ -153,6 +418,8 @@ window.__ModuleLoader__.load({
153
418
  const fileRef = react.useRef(null);
154
419
  const [buddyFiles, setBuddyFiles] = react.useState([]);
155
420
  const [buddyNote, setBuddyNote] = react.useState("");
421
+ const [updateInfo, setUpdateInfo] = react.useState(null);
422
+ const [updateBusy, setUpdateBusy] = react.useState(false);
156
423
 
157
424
  const load = react.useCallback(async () => {
158
425
  setError("");
@@ -173,7 +440,7 @@ window.__ModuleLoader__.load({
173
440
  const qs = params.toString();
174
441
  const list = await api("/api/dsh-trivium/nodes" + (qs ? "?" + qs : ""));
175
442
  setNodes(list.nodes || []);
176
- const wb = await api("/api/dsh-trivium/workbuddy");
443
+ const wb = await api("/api/dsh-trivium/external");
177
444
  setBuddyFiles(wb.files || []);
178
445
  } catch (err) {
179
446
  setError(String(err.message || err));
@@ -317,19 +584,18 @@ window.__ModuleLoader__.load({
317
584
  setBusy(true);
318
585
  setBuddyNote("");
319
586
  try {
320
- const r = await api("/api/dsh-trivium/import-workbuddy", {
587
+ const r = await api("/api/dsh-trivium/import-external", {
321
588
  method: "POST",
322
589
  headers: { "content-type": "application/json" },
323
590
  body: "{}",
324
591
  });
325
592
  setSaved(true);
326
593
  setBuddyNote(
327
- "导入 " +
328
- (r.created || 0) +
329
- " 新建 / " +
330
- (r.merged || 0) +
331
- " 合并,跳过 " +
332
- (r.skipped || 0),
594
+ t("importOk", {
595
+ created: r.created || 0,
596
+ merged: r.merged || 0,
597
+ skipped: r.skipped || 0,
598
+ }),
333
599
  );
334
600
  await load();
335
601
  } catch (err) {
@@ -339,6 +605,17 @@ window.__ModuleLoader__.load({
339
605
  }
340
606
  };
341
607
 
608
+ const checkUpdate = async () => {
609
+ setUpdateBusy(true);
610
+ try {
611
+ setUpdateInfo(await api("/api/dsh-trivium/update?force=1"));
612
+ } catch (err) {
613
+ setUpdateInfo({ ok: false, error: String(err.message || err) });
614
+ } finally {
615
+ setUpdateBusy(false);
616
+ }
617
+ };
618
+
342
619
  const importJson = async (file) => {
343
620
  if (!file) return;
344
621
  setBusy(true);
@@ -354,11 +631,11 @@ window.__ModuleLoader__.load({
354
631
  setError(
355
632
  r.ok
356
633
  ? ""
357
- : String(r.message || "导入失败"),
634
+ : String(r.message || t("importFail")),
358
635
  );
359
636
  await load();
360
637
  } catch (err) {
361
- setError("导入失败:" + String(err.message || err));
638
+ setError(t("importFail") + ":" + String(err.message || err));
362
639
  } finally {
363
640
  setBusy(false);
364
641
  if (fileRef.current) fileRef.current.value = "";
@@ -461,6 +738,42 @@ window.__ModuleLoader__.load({
461
738
  ],
462
739
  })
463
740
  : null,
741
+ jsxs("div", {
742
+ style: ui.card,
743
+ children: [
744
+ jsx("div", { style: ui.cardTitle, children: L.update }),
745
+ jsx("div", { style: ui.hint, children: L.updateHint }),
746
+ jsx("div", {
747
+ style: ui.hint,
748
+ children: updateBusy
749
+ ? L.updateChecking
750
+ : updateInfo
751
+ ? [
752
+ L.updateInstalled + " " + (updateInfo.installed || status?.version || "—"),
753
+ L.updateLatest + " " + (updateInfo.latest || "—"),
754
+ updateInfo.error
755
+ ? L.updateFailed
756
+ : updateInfo.newer
757
+ ? L.updateNewer
758
+ : L.updateCurrent,
759
+ ].join(" · ")
760
+ : L.updateInstalled + " " + (status?.version || "—"),
761
+ }),
762
+ updateInfo && updateInfo.newer && updateInfo.command
763
+ ? jsx("code", { style: { fontSize: 12 }, children: updateInfo.command })
764
+ : null,
765
+ jsx("div", {
766
+ style: { display: "flex", flexWrap: "wrap", gap: 8 },
767
+ children: jsx(Button, {
768
+ variant: "outline",
769
+ size: "sm",
770
+ disabled: updateBusy,
771
+ onClick: checkUpdate,
772
+ children: updateBusy ? L.updateChecking : L.update,
773
+ }),
774
+ }),
775
+ ],
776
+ }),
464
777
  jsxs("div", {
465
778
  style: ui.card,
466
779
  children: [
@@ -493,18 +806,18 @@ window.__ModuleLoader__.load({
493
806
  children: [
494
807
  jsx("div", { style: ui.cardTitle, children: L.markdown }),
495
808
  jsx("div", { style: ui.hint, children: L.markdownHint }),
496
- jsx("div", { style: ui.cardTitle, children: L.workbuddy }),
497
- jsx("div", { style: ui.hint, children: L.workbuddyHint }),
809
+ jsx("div", { style: ui.cardTitle, children: L.external }),
810
+ jsx("div", { style: ui.hint, children: L.externalHint }),
498
811
  jsx("div", {
499
812
  style: { ...ui.hint, wordBreak: "break-all" },
500
813
  children: (buddyFiles || []).some((f) => f.exists)
501
- ? L.workbuddyFound +
814
+ ? L.externalFound +
502
815
  " " +
503
816
  buddyFiles
504
817
  .filter((f) => f.exists)
505
- .map((f) => f.path)
818
+ .map((f) => (f.family ? f.family + " · " : "") + f.path)
506
819
  .join(" · ")
507
- : L.workbuddyNone,
820
+ : L.externalNone,
508
821
  }),
509
822
  buddyNote ? jsx("div", { style: ui.saved, children: buddyNote }) : null,
510
823
  jsx("div", {
@@ -514,7 +827,7 @@ window.__ModuleLoader__.load({
514
827
  size: "sm",
515
828
  disabled: busy || !(buddyFiles || []).some((f) => f.exists),
516
829
  onClick: importBuddy,
517
- children: L.workbuddyImport,
830
+ children: L.externalImport,
518
831
  }),
519
832
  }),
520
833
  ],
@@ -581,7 +894,7 @@ window.__ModuleLoader__.load({
581
894
  type: "password",
582
895
  value: embeddingApiKey,
583
896
  onChange: (e) => setEmbeddingApiKey(e.target.value),
584
- placeholder: embeddingApiKeySet ? "已保存" : "",
897
+ placeholder: embeddingApiKeySet ? L.savedKey : "",
585
898
  style: ui.input,
586
899
  }),
587
900
  ),
@@ -614,7 +927,7 @@ window.__ModuleLoader__.load({
614
927
  jsx("input", {
615
928
  value: q,
616
929
  onChange: (e) => setQ(e.target.value),
617
- placeholder: L.search + " 鉴权 / until / about",
930
+ placeholder: L.search + " " + L.searchPh,
618
931
  style: { ...ui.input, flex: 1, minWidth: 160 },
619
932
  }),
620
933
  jsxs("label", {
@@ -682,6 +995,7 @@ window.__ModuleLoader__.load({
682
995
  n.name ? " · " + n.name : "",
683
996
  n.until ? " · " + L.until + " " + n.until : "",
684
997
  n.stale ? " · " + L.stale : "",
998
+ n.dirty ? " · " + L.dirty : "",
685
999
  ],
686
1000
  }),
687
1001
  jsx("div", { style: { fontSize: 13, opacity: 0.9, lineHeight: 1.4 }, children: n.text }),
@@ -730,7 +1044,7 @@ window.__ModuleLoader__.load({
730
1044
  ...prev,
731
1045
  [n.id]: { ...editOf(n), aliases: ev.target.value },
732
1046
  })),
733
- placeholder: "逗号分隔",
1047
+ placeholder: L.aliasesPh,
734
1048
  style: ui.input,
735
1049
  }),
736
1050
  )
@@ -745,7 +1059,7 @@ window.__ModuleLoader__.load({
745
1059
  ...prev,
746
1060
  [n.id]: { ...editOf(n), until: ev.target.value },
747
1061
  })),
748
- placeholder: "周五 / 下周 / ISO",
1062
+ placeholder: L.untilPh,
749
1063
  style: ui.input,
750
1064
  }),
751
1065
  )
@@ -874,22 +1188,35 @@ window.__ModuleLoader__.load({
874
1188
  }
875
1189
 
876
1190
  function relTime(iso) {
877
- const t = Date.parse(iso);
878
- if (!Number.isFinite(t)) return "";
879
- const sec = Math.max(0, Math.round((Date.now() - t) / 1000));
880
- if (sec < 60) return "刚刚";
881
- if (sec < 3600) return Math.floor(sec / 60) + " 分钟前";
882
- if (sec < 86400) return Math.floor(sec / 3600) + " 小时前";
883
- return Math.floor(sec / 86400) + " 天前";
1191
+ const at = Date.parse(iso);
1192
+ if (!Number.isFinite(at)) return "";
1193
+ const sec = Math.max(0, Math.round((Date.now() - at) / 1000));
1194
+ if (sec < 60) return t("justNow");
1195
+ if (sec < 3600) return t("minutesAgo", { n: Math.floor(sec / 60) });
1196
+ if (sec < 86400) return t("hoursAgo", { n: Math.floor(sec / 3600) });
1197
+ return t("daysAgo", { n: Math.floor(sec / 86400) });
884
1198
  }
885
1199
 
886
1200
  function kindLabel(kind, current) {
887
- if (current || kind === "tail") return "后续";
888
- if (kind === "fork") return "分支";
889
- return "检查点";
1201
+ if (current || kind === "tail") return t("boxNext");
1202
+ if (kind === "fork") return t("boxFork");
1203
+ return t("boxCheck");
890
1204
  }
891
1205
 
892
- const GENERIC_BOX_NAMES = { "当前": 1, "已压缩": 1, "分叉": 1, "分叉点": 1, "后续": 1, "分支": 1 };
1206
+ const GENERIC_BOX_NAMES = {
1207
+ 当前: 1,
1208
+ 已压缩: 1,
1209
+ 分叉: 1,
1210
+ 分叉点: 1,
1211
+ 后续: 1,
1212
+ 分支: 1,
1213
+ Current: 1,
1214
+ Compacted: 1,
1215
+ Fork: 1,
1216
+ Checkpoint: 1,
1217
+ Next: 1,
1218
+ Branch: 1,
1219
+ };
893
1220
  const MAP_BOX_W = 220;
894
1221
  const MAP_ARROW_W = 96;
895
1222
  const MAP_PAD = 40;
@@ -1115,7 +1442,7 @@ window.__ModuleLoader__.load({
1115
1442
 
1116
1443
  function increasedForkTitle(title) {
1117
1444
  const text = String(title || "").trim();
1118
- if (!text) return "分叉 (1)";
1445
+ if (!text) return t("forkDefault");
1119
1446
  const ascii = /^(.*?)\((\d+)\)$/u.exec(text);
1120
1447
  if (ascii?.[1] != null && ascii[2] != null) return ascii[1] + "(" + (Number(ascii[2]) + 1) + ")";
1121
1448
  const fullWidth = /^(.*?)((\d+))$/u.exec(text);
@@ -1129,8 +1456,8 @@ window.__ModuleLoader__.load({
1129
1456
  }
1130
1457
 
1131
1458
  function boxKicker(node, { current, child, sessionTitle }) {
1132
- if (child) return sessionTitle || "分叉";
1133
- if (current || node?.kind === "tail") return "后续";
1459
+ if (child) return sessionTitle || t("forkName");
1460
+ if (current || node?.kind === "tail") return t("boxNext");
1134
1461
  if (node?.atSeq != null && Number(node.atSeq) > 0) return "seq " + node.atSeq;
1135
1462
  return kindLabel(node?.kind, current);
1136
1463
  }
@@ -1604,8 +1931,8 @@ window.__ModuleLoader__.load({
1604
1931
  };
1605
1932
  const inspectBtn = jsx("button", {
1606
1933
  type: "button",
1607
- title: "轨迹",
1608
- "aria-label": "轨迹",
1934
+ title: t("trajectory"),
1935
+ "aria-label": t("trajectory"),
1609
1936
  "data-map-inspect": "1",
1610
1937
  style: mapUi.iconBtn,
1611
1938
  onPointerDown: stopIcon,
@@ -1618,8 +1945,8 @@ window.__ModuleLoader__.load({
1618
1945
  });
1619
1946
  const forkBtn = jsx("button", {
1620
1947
  type: "button",
1621
- title: "分叉",
1622
- "aria-label": "分叉",
1948
+ title: t("fork"),
1949
+ "aria-label": t("fork"),
1623
1950
  "data-map-fork": "1",
1624
1951
  disabled: !!busy,
1625
1952
  style: { ...mapUi.iconBtn, ...(busy ? { opacity: 0.4 } : {}) },
@@ -1635,8 +1962,8 @@ window.__ModuleLoader__.load({
1635
1962
  !canSetView
1636
1963
  ? jsx("button", {
1637
1964
  type: "button",
1638
- title: "打开对话",
1639
- "aria-label": "打开对话",
1965
+ title: t("openChat"),
1966
+ "aria-label": t("openChat"),
1640
1967
  "data-map-open": "1",
1641
1968
  style: mapUi.iconBtn,
1642
1969
  onPointerDown: stopIcon,
@@ -1645,14 +1972,14 @@ window.__ModuleLoader__.load({
1645
1972
  stopIcon(e);
1646
1973
  onJump(node);
1647
1974
  },
1648
- children: "对",
1975
+ children: t("chatShort"),
1649
1976
  })
1650
1977
  : null;
1651
1978
  const titleNode = editing
1652
1979
  ? jsx("input", {
1653
1980
  value: draft,
1654
1981
  autoFocus: true,
1655
- "aria-label": "方框标题",
1982
+ "aria-label": t("boxTitle"),
1656
1983
  style: mapUi.titleInput,
1657
1984
  onPointerDown: (e) => e.stopPropagation(),
1658
1985
  onClick: (e) => e.stopPropagation(),
@@ -1672,7 +1999,7 @@ window.__ModuleLoader__.load({
1672
1999
  })
1673
2000
  : jsx("div", {
1674
2001
  style: mapUi.title,
1675
- title: "双击改名",
2002
+ title: t("renameHint"),
1676
2003
  onClick: (e) => e.stopPropagation(),
1677
2004
  onDoubleClick: startEdit,
1678
2005
  children: customTitle,
@@ -1714,7 +2041,7 @@ window.__ModuleLoader__.load({
1714
2041
  children: [
1715
2042
  jsxs("div", {
1716
2043
  style: { ...mapUi.kicker, flex: 1, minWidth: 0 },
1717
- title: canRename ? "双击改名" : undefined,
2044
+ title: canRename ? t("renameHint") : undefined,
1718
2045
  onDoubleClick: startEdit,
1719
2046
  children: [
1720
2047
  boxKicker(node, { current, child, sessionTitle }),
@@ -1726,13 +2053,13 @@ window.__ModuleLoader__.load({
1726
2053
  style: mapUi.ghostBtn,
1727
2054
  onPointerDown: stopIcon,
1728
2055
  onClick: startEdit,
1729
- children: "改名",
2056
+ children: t("rename"),
1730
2057
  })
1731
2058
  : null,
1732
2059
  ],
1733
2060
  }),
1734
2061
  customTitle || editing ? titleNode : null,
1735
- jsx("div", { style: mapUi.summary, children: body || "(尚无摘要)" }),
2062
+ jsx("div", { style: mapUi.summary, children: body || t("noSummary") }),
1736
2063
  jsx("div", { style: mapUi.hint, children: relTime(node.createdAt) }),
1737
2064
  ],
1738
2065
  }),
@@ -1749,6 +2076,7 @@ window.__ModuleLoader__.load({
1749
2076
  let pendingSessionNav = null;
1750
2077
 
1751
2078
  function SessionMapView({ sessionId, useSessions, useSession, fork, open, renameSession, actions, seedLengthOf }) {
2079
+ useT();
1752
2080
  const canSetView = typeof actions?.setView === "function";
1753
2081
  const chat = useSession((s) => s.chat);
1754
2082
  const cwd = useSessions((s) => (sessionId == null ? "" : s.byId[sessionId]?.cwd || ""));
@@ -1860,9 +2188,9 @@ window.__ModuleLoader__.load({
1860
2188
  const checks = Number(resp.checkpoints) || 0;
1861
2189
  const forkN = Number(resp.forks) || 0;
1862
2190
  if (!checks && !forkN) {
1863
- setBackfillNote("没有发现压缩标记或分叉。没压过的会话本来就只有「后续」。要分段请点「生成检查点」。");
2191
+ setBackfillNote(t("backfillNone"));
1864
2192
  } else {
1865
- setBackfillNote("已同步 " + checks + " 个检查点、" + forkN + " 条分叉。");
2193
+ setBackfillNote(t("backfillOk", { checks, forks: forkN }));
1866
2194
  }
1867
2195
  } catch (err) {
1868
2196
  setError(String(err.message || err));
@@ -1875,7 +2203,7 @@ window.__ModuleLoader__.load({
1875
2203
  if (!sessionId || busy) return;
1876
2204
  const atSeq = lastChatSeq(chat);
1877
2205
  if (atSeq == null) {
1878
- setError("当前对话还没有可切的轮次。先在对话里发一条,再生成检查点。");
2206
+ setError(t("cutNeedTurn"));
1879
2207
  return;
1880
2208
  }
1881
2209
  setBusy(true);
@@ -1892,9 +2220,9 @@ window.__ModuleLoader__.load({
1892
2220
  });
1893
2221
  applyMap(resp);
1894
2222
  if (resp.created) {
1895
- setBackfillNote("已把当前「后续」收成左边一格(seq " + atSeq + "),右边仍是后续。这不压缩 DSH 上下文。");
2223
+ setBackfillNote(t("cutOk", { seq: atSeq }));
1896
2224
  } else {
1897
- setBackfillNote("这个位置已经切过了(seq " + atSeq + ")。再聊几轮后再生成。");
2225
+ setBackfillNote(t("cutDup", { seq: atSeq }));
1898
2226
  }
1899
2227
  } catch (err) {
1900
2228
  setError(String(err.message || err));
@@ -1974,7 +2302,7 @@ window.__ModuleLoader__.load({
1974
2302
 
1975
2303
  const openSession = (id, nav) => {
1976
2304
  if (typeof open !== "function") {
1977
- setError("无法打开分叉会话:会话图没有拿到 session.open");
2305
+ setError(t("forkOpenFail"));
1978
2306
  return false;
1979
2307
  }
1980
2308
  try {
@@ -1994,7 +2322,7 @@ window.__ModuleLoader__.load({
1994
2322
  return;
1995
2323
  }
1996
2324
  if (typeof actions?.setView !== "function") {
1997
- setError("无法切换到对话页:会话图没有拿到同一 chat store 的 setView");
2325
+ setError(t("chatViewFail"));
1998
2326
  return;
1999
2327
  }
2000
2328
  const toEnd = node.kind === "tail" || !!node.current || !Number(node.atSeq);
@@ -2017,7 +2345,7 @@ window.__ModuleLoader__.load({
2017
2345
  if (typeof actions?.setView === "function") {
2018
2346
  actions.setView("trajectory");
2019
2347
  } else {
2020
- setError("无法切换到轨迹页:会话图没有拿到同一 chat store 的 setView");
2348
+ setError(t("trajViewFail"));
2021
2349
  }
2022
2350
  };
2023
2351
 
@@ -2040,7 +2368,7 @@ window.__ModuleLoader__.load({
2040
2368
  setBusy(true);
2041
2369
  setError("");
2042
2370
  try {
2043
- if (typeof fork !== "function") throw new Error("当前壳没有 session.fork");
2371
+ if (typeof fork !== "function") throw new Error(t("noFork"));
2044
2372
  const sourceId = String(node.sessionId || sessionId);
2045
2373
  const opts = { sessionId: sourceId, increaseTitle: true };
2046
2374
  if (!node.current && node.kind !== "tail" && Number.isFinite(Number(node.atSeq))) {
@@ -2052,7 +2380,7 @@ window.__ModuleLoader__.load({
2052
2380
  try {
2053
2381
  await renameSession(childId, wanted);
2054
2382
  } catch (err) {
2055
- setError("会话已分叉,但侧边栏改名失败:" + String(err.message || err));
2383
+ setError(t("renameFail", { err: String(err.message || err) }));
2056
2384
  }
2057
2385
  }
2058
2386
  if (inheritPins && childId) {
@@ -2123,12 +2451,7 @@ window.__ModuleLoader__.load({
2123
2451
  const runChipBatch = async (action) => {
2124
2452
  if (!pickedIds.length || busy) return;
2125
2453
  if (action === "delete") {
2126
- const ok = window.confirm(
2127
- "删除会从 .tdb 里拿掉这 " +
2128
- pickedIds.length +
2129
- " 条,不可恢复。归档只是藏起来,find / 短地图 / 芯片都不再出现。确定删除?",
2130
- );
2131
- if (!ok) return;
2454
+ if (!window.confirm(t("deleteConfirm", { n: pickedIds.length }))) return;
2132
2455
  }
2133
2456
  setBusy(true);
2134
2457
  setError("");
@@ -2195,16 +2518,12 @@ window.__ModuleLoader__.load({
2195
2518
  jsxs("div", {
2196
2519
  style: mapUi.chipsHead,
2197
2520
  children: [
2198
- jsx("div", { style: mapUi.chipsTitle, children: "记忆芯片" }),
2521
+ jsx("div", { style: mapUi.chipsTitle, children: t("chips") }),
2199
2522
  jsx("div", {
2200
2523
  style: mapUi.hint,
2201
2524
  children:
2202
2525
  backfillNote ||
2203
- "共 " +
2204
- chips.length +
2205
- " 条,已钉 " +
2206
- pinIds.length +
2207
- "。默认不勾选:不钉就不会按芯片白名单注入,但仍可能被开场短地图点名带上,模型也能 find 到。",
2526
+ t("chipsHint", { n: chips.length, pins: pinIds.length }),
2208
2527
  }),
2209
2528
  jsxs("div", {
2210
2529
  style: mapUi.chipsActions,
@@ -2213,23 +2532,23 @@ window.__ModuleLoader__.load({
2213
2532
  type: "button",
2214
2533
  style: mapUi.actionBtn,
2215
2534
  disabled: busy,
2216
- title: "把当前「后续」收成左边一格,右边继续当后续。只切情节图,不触发 DSH 压缩。要压上下文请在对话里输入 /compact。",
2535
+ title: t("cutTitle"),
2217
2536
  onClick: cutCheckpoint,
2218
- children: busy ? "生成中…" : "生成检查点",
2537
+ children: busy ? t("cutting") : t("cutBtn"),
2219
2538
  }),
2220
2539
  jsx("button", {
2221
2540
  type: "button",
2222
2541
  style: mapUi.actionBtn,
2223
2542
  disabled: busy,
2224
- title: "读取当前对话里 DSH 已有的压缩标记和侧边栏分叉,补进图。没压过就补不出历史方框。",
2543
+ title: t("updateTitle"),
2225
2544
  onClick: syncCheckpoints,
2226
- children: busy ? "更新中…" : "更新检查点",
2545
+ children: busy ? t("updating") : t("updateBtn"),
2227
2546
  }),
2228
2547
  jsx("button", {
2229
2548
  type: "button",
2230
2549
  style: mapUi.actionBtn,
2231
2550
  onClick: () => setChipsOpen((v) => !v),
2232
- children: chipsOpen ? "收起" : "展开勾选",
2551
+ children: chipsOpen ? t("collapse") : t("expand"),
2233
2552
  }),
2234
2553
  ],
2235
2554
  }),
@@ -2244,7 +2563,7 @@ window.__ModuleLoader__.load({
2244
2563
  children: [
2245
2564
  jsx("input", {
2246
2565
  type: "search",
2247
- placeholder: "搜索芯片",
2566
+ placeholder: t("searchChips"),
2248
2567
  value: chipQuery,
2249
2568
  onChange: (e) => setChipQuery(e.target.value),
2250
2569
  style: mapUi.search,
@@ -2257,7 +2576,7 @@ window.__ModuleLoader__.load({
2257
2576
  checked: inheritPins,
2258
2577
  onChange: (e) => setInheritPins(e.target.checked),
2259
2578
  }),
2260
- "分叉时继承钉选",
2579
+ t("inheritPins"),
2261
2580
  ],
2262
2581
  }),
2263
2582
  jsxs("div", {
@@ -2271,7 +2590,7 @@ window.__ModuleLoader__.load({
2271
2590
  setChipSelect(false);
2272
2591
  setPickedIds([]);
2273
2592
  },
2274
- children: draftOpen ? "取消新增" : "新增",
2593
+ children: draftOpen ? t("cancelAdd") : t("addChip"),
2275
2594
  }),
2276
2595
  jsx("button", {
2277
2596
  type: "button",
@@ -2281,7 +2600,7 @@ window.__ModuleLoader__.load({
2281
2600
  setPickedIds([]);
2282
2601
  setDraftOpen(false);
2283
2602
  },
2284
- children: chipSelect ? "取消选择" : "选择",
2603
+ children: chipSelect ? t("cancelSelect") : t("select"),
2285
2604
  }),
2286
2605
  chipSelect
2287
2606
  ? jsx("button", {
@@ -2289,7 +2608,7 @@ window.__ModuleLoader__.load({
2289
2608
  style: mapUi.actionBtn,
2290
2609
  disabled: !pickedIds.length || busy,
2291
2610
  onClick: () => runChipBatch("archive"),
2292
- children: "归档" + (pickedIds.length ? " " + pickedIds.length : ""),
2611
+ children: t("archive") + (pickedIds.length ? " " + pickedIds.length : ""),
2293
2612
  })
2294
2613
  : null,
2295
2614
  chipSelect
@@ -2298,7 +2617,7 @@ window.__ModuleLoader__.load({
2298
2617
  style: mapUi.dangerBtn,
2299
2618
  disabled: !pickedIds.length || busy,
2300
2619
  onClick: () => runChipBatch("delete"),
2301
- children: "删除" + (pickedIds.length ? " " + pickedIds.length : ""),
2620
+ children: t("remove") + (pickedIds.length ? " " + pickedIds.length : ""),
2302
2621
  })
2303
2622
  : null,
2304
2623
  ],
@@ -2308,10 +2627,10 @@ window.__ModuleLoader__.load({
2308
2627
  jsx("div", {
2309
2628
  style: mapUi.hint,
2310
2629
  children: chipSelect
2311
- ? "点芯片多选。归档:软删除,find / 短地图 / 芯片不再出现,节点还在库里。删除:从 .tdb 去掉,不可恢复。"
2630
+ ? t("chipSelectHint")
2312
2631
  : clipped
2313
- ? "已截到预算(≤300 token)"
2314
- : "勾选后从当前这段的下一轮注入。空白处拖动画布,滚轮缩放。",
2632
+ ? t("chipClipped")
2633
+ : t("chipPinHint"),
2315
2634
  }),
2316
2635
  draftOpen
2317
2636
  ? jsxs("div", {
@@ -2320,7 +2639,7 @@ window.__ModuleLoader__.load({
2320
2639
  jsx("textarea", {
2321
2640
  value: draftText,
2322
2641
  onChange: (e) => setDraftText(e.target.value),
2323
- placeholder: "粘贴或输入要记住的内容。点一次写入,整段就是一条芯片。",
2642
+ placeholder: t("chipDraftPh"),
2324
2643
  style: mapUi.draftBox,
2325
2644
  }),
2326
2645
  jsx("button", {
@@ -2328,7 +2647,7 @@ window.__ModuleLoader__.load({
2328
2647
  style: { ...mapUi.actionBtn, alignSelf: "flex-start" },
2329
2648
  disabled: busy || !draftText.trim(),
2330
2649
  onClick: submitChipDraft,
2331
- children: "写入并钉选",
2650
+ children: t("writePin"),
2332
2651
  }),
2333
2652
  ],
2334
2653
  })
@@ -2367,7 +2686,7 @@ window.__ModuleLoader__.load({
2367
2686
  checked: pinSet.has(c.id),
2368
2687
  onChange: (e) => togglePin(c.id, e.target.checked),
2369
2688
  }),
2370
- c.suggested ? jsx("span", { style: mapUi.suggest, children: "建议" }) : null,
2689
+ c.suggested ? jsx("span", { style: mapUi.suggest, children: t("suggested") }) : null,
2371
2690
  jsx("span", {
2372
2691
  style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
2373
2692
  children: (c.type || "") + " · " + (c.l0 || c.name || "#" + c.id),
@@ -2381,8 +2700,8 @@ window.__ModuleLoader__.load({
2381
2700
  : jsx("div", {
2382
2701
  style: mapUi.hint,
2383
2702
  children: chips.length
2384
- ? "没有匹配的芯片。"
2385
- : "还没有可钉选的 preference / decision / entity。用「新增」写一条。",
2703
+ ? t("noChipMatch")
2704
+ : t("noChips"),
2386
2705
  }),
2387
2706
  ],
2388
2707
  })
@@ -2503,15 +2822,15 @@ window.__ModuleLoader__.load({
2503
2822
  if (forkDraft?.node) forkBox(forkDraft.node, forkDraft.title);
2504
2823
  },
2505
2824
  children: [
2506
- jsx("div", { style: mapUi.dialogTitle, children: "分叉为新会话" }),
2825
+ jsx("div", { style: mapUi.dialogTitle, children: t("forkDialog") }),
2507
2826
  jsx("div", {
2508
2827
  style: mapUi.hint,
2509
- children: "这个名字会出现在左侧会话列表里。",
2828
+ children: t("forkDialogHint"),
2510
2829
  }),
2511
2830
  jsx("input", {
2512
2831
  autoFocus: true,
2513
2832
  value: forkDraft.title,
2514
- "aria-label": "新会话名称",
2833
+ "aria-label": t("newSessionName"),
2515
2834
  onChange: (e) => setForkDraft({ ...forkDraft, title: e.target.value }),
2516
2835
  style: { ...mapUi.titleInput, flex: "none", width: "100%" },
2517
2836
  }),
@@ -2522,13 +2841,13 @@ window.__ModuleLoader__.load({
2522
2841
  type: "button",
2523
2842
  style: mapUi.foldBtn,
2524
2843
  onClick: () => setForkDraft(null),
2525
- children: "取消",
2844
+ children: t("cancel"),
2526
2845
  }),
2527
2846
  jsx("button", {
2528
2847
  type: "submit",
2529
2848
  style: mapUi.foldBtn,
2530
2849
  disabled: busy,
2531
- children: busy ? "分叉中…" : "分叉",
2850
+ children: busy ? t("forking") : t("fork"),
2532
2851
  }),
2533
2852
  ],
2534
2853
  }),
@@ -2543,6 +2862,19 @@ window.__ModuleLoader__.load({
2543
2862
  function apply(ctx) {
2544
2863
  workspacesList = ctx.workspaces?.list || null;
2545
2864
  const sessions = ctx.sessions;
2865
+ try {
2866
+ const snap = ctx.locale && typeof ctx.locale.getLocale === "function" ? ctx.locale.getLocale() : null;
2867
+ if (snap && snap.active) setUiLocale(snap.active);
2868
+ } catch {
2869
+ // host may not expose locale
2870
+ }
2871
+ try {
2872
+ ctx.on("locale/change", (snap) => {
2873
+ if (snap && snap.active) setUiLocale(snap.active);
2874
+ });
2875
+ } catch {
2876
+ // ignore
2877
+ }
2546
2878
  ctx.slots.inject(
2547
2879
  "settings.section",
2548
2880
  () =>
@@ -2567,7 +2899,7 @@ window.__ModuleLoader__.load({
2567
2899
  name: "conversation.view",
2568
2900
  id: "session-map",
2569
2901
  order: 20,
2570
- label: () => "会话图",
2902
+ label: () => t("sessionMap"),
2571
2903
  ...(chatStore ? { store: chatStore } : {}),
2572
2904
  inject: () => ({
2573
2905
  fork: (opts) => sessions.fork(opts),
@@ -2584,7 +2916,7 @@ window.__ModuleLoader__.load({
2584
2916
  renameSession: async (id, title) => {
2585
2917
  const face = sessions.binding?.(id)?.session;
2586
2918
  if (!face || typeof face.rename !== "function") {
2587
- throw new Error("当前壳没有 session.rename");
2919
+ throw new Error(t("noRename"));
2588
2920
  }
2589
2921
  const result = await face.rename(title);
2590
2922
  if (result && result.ok === false) {