@anionex/dsh-turn-rewind 0.3.7 → 0.3.9

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
@@ -3,6 +3,7 @@ var module = { exports: {} }; var exports = module.exports;
3
3
  "use strict";
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.inject = void 0;
6
+ exports.resolveUiLocale = resolveUiLocale;
6
7
  exports.selectRewindMessage = selectRewindMessage;
7
8
  exports.apply = apply;
8
9
  exports.collectChatNodes = collectChatNodes;
@@ -11,6 +12,7 @@ exports.RewindMessageAction = RewindMessageAction;
11
12
  exports.TurnRewindSettingsCard = TurnRewindSettingsCard;
12
13
  exports.formatBytes = formatBytes;
13
14
  exports.selectRewindMessageTarget = selectRewindMessageTarget;
15
+ exports.openSessionWithDraft = openSessionWithDraft;
14
16
  exports.responseJson = responseJson;
15
17
  exports.fileRecoveryLabel = fileRecoveryLabel;
16
18
  exports.describeCaptureNotice = describeCaptureNotice;
@@ -88,6 +90,441 @@ const styles = `
88
90
  .dcl-trs-error{box-sizing:border-box;max-width:100%;margin:0;padding:10px 12px;overflow-wrap:anywhere;word-break:break-word;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 30%,transparent);border-radius:10px;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
89
91
  .dcl-trs-storage{margin:0;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:12px;overflow-wrap:anywhere}
90
92
  `;
93
+ /** Chinese copy — the language this plugin shipped in before English existed. */
94
+ const ZH = {
95
+ rewindTooltip: '恢复到发送这条消息之前',
96
+ dialogClose: '关闭',
97
+ dialogDescription: '查看恢复的文件,选择适合你的回退方式。当前会话不受影响。',
98
+ cancel: '取消',
99
+ applying: '正在恢复…',
100
+ done: '已完成',
101
+ actionRestoreAndRestart: '恢复并从这里继续',
102
+ actionRestoreFiles: '恢复文件',
103
+ actionMessagesOnly: '只回溯消息',
104
+ checkingFiles: '正在检查可以恢复的项目文件…',
105
+ previewPending: '这条消息发送前的文件还在保存,请稍后再试。',
106
+ previewMissing: '没有保存这条消息发送前的文件。可能是当时还没启用回退功能、记录已超过保留期限,或已关闭自动检查点。仍可只回溯消息。',
107
+ modeBothTitle: '恢复文件并从这里继续',
108
+ modeBothDescription: '创建一个从这里开始的新会话(当前对话会保留)',
109
+ modeCodeTitle: '只恢复文件',
110
+ modeCodeDescription: '恢复这条消息发送前的文件,当前对话保持不变。',
111
+ modeMessagesTitle: '只回溯消息(不动文件)',
112
+ modeMessagesDescription: '创建一个从这里开始的新会话,项目文件保持当前状态。',
113
+ filesUnchanged: '项目文件保持不变',
114
+ filesToRestore: count => `将恢复 ${String(count)} 个文件`,
115
+ summaryBoth: '恢复后在新对话里继续',
116
+ summaryCode: '当前对话保持不变',
117
+ summaryMessages: '仅创建从这里继续的新对话',
118
+ sharedBlocked: '这个项目目录还有别的对话正在运行。恢复文件会影响到它们,因此本次操作已被阻止。请等那些对话结束或停止后,再重新检查。',
119
+ branchChanged: '当前所在的 Git 分支和发送这条消息时不同。恢复不会切换分支,只会把当时的文件内容恢复到当前分支。',
120
+ headAdvanced: '这条消息之后有了新的 Git 提交。恢复只会改文件,不会撤销提交;完成后这些文件会显示为未提交修改。',
121
+ driftBlocked: 'Git 正在进行合并、变基或类似操作。请先完成或取消这次 Git 操作,再重新检查。',
122
+ planMissing: '恢复信息已经失效,请重新检查。',
123
+ stale: '项目文件在检查后又发生了变化。为避免覆盖新修改,这次恢复已失效,请重新检查。',
124
+ nothingToRestore: '项目文件已经是这条消息发送前的状态,无需恢复文件。可选择「只回溯消息」重新开始这段对话。',
125
+ loadingAllFiles: '正在读取全部文件…',
126
+ viewAllFiles: count => `查看全部 ${String(count)} 个文件`,
127
+ backupNote: '恢复前会自动备份当前文件;若恢复失败会自动还原,项目不会停留在只恢复了一部分的状态。',
128
+ recheck: '重新检查',
129
+ completedMessages: '已创建从这里开始的新对话;项目文件保持不变。',
130
+ completedCode: '项目文件已恢复;当前对话保持不变。恢复前的文件已自动备份。',
131
+ completedBoth: '项目文件已恢复,并已创建新对话。恢复前的文件已自动备份。',
132
+ openFailed: reason => `新对话已创建,但没能自动打开:${reason}`,
133
+ restoredButOpenFailed: reason => `文件已经恢复,新对话也已创建,但没能自动打开:${reason}`,
134
+ modeMismatch: mode => `服务器返回了不匹配的回退模式:${mode}`,
135
+ listChangedWhileExpanding: '项目文件在展开列表时发生了变化。',
136
+ incompleteFileList: '无法读取完整的文件列表。',
137
+ fileKinds: {
138
+ added: '移除后来新增的文件',
139
+ deleted: '找回文件',
140
+ modified: '恢复之前的版本',
141
+ 'mode-changed': '恢复文件权限',
142
+ 'type-changed': '恢复之前的文件类型',
143
+ },
144
+ errors: {
145
+ REWIND_ENDPOINT_UNAVAILABLE: '回退服务没有响应。请确认 @anionex/dsh-turn-rewind 已挂载到当前 DSH,并重启 DSH 后重试。',
146
+ REWIND_INVALID_RESPONSE: '回退服务返回了无法解析的内容。请重启 DSH 后重试。',
147
+ PLAN_STALE: '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。',
148
+ PLAN_STALE_WORKSPACE: '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。',
149
+ PLAN_STALE_REPOSITORY: 'Git 状态在检查后又发生了变化,恢复已失效。请重新检查后再试。',
150
+ WORKSPACE_IN_USE: '这个项目目录还有别的对话正在运行。请等那些对话结束或停止后,再重新检查。',
151
+ WORKSPACE_LOCKED: '另一个恢复操作正在处理这个项目目录。请等待它完成后重新检查。',
152
+ WORKSPACE_CHANGED: '这个项目目录已经不在原来的位置,旧的回退点无法再使用。',
153
+ WORKSPACE_MODE_CHANGED: '这个项目目录的工作区类型变了(例如从普通目录变成了 Git 仓库)。旧的回退点不再适用,请重新发送一条消息生成新的回退点。',
154
+ HEAD_CHANGED: '项目的提交或分支已发生变化。为避免覆盖新改动,请重新检查后再恢复。',
155
+ REPOSITORY_CHANGED: '这个项目目录已不属于原来的 Git 工作区,无法恢复。',
156
+ GIT_OPERATION_CHANGED: 'Git 正在执行其他操作。请先完成或取消该操作,再重新检查。',
157
+ RESTORE_POINT_NOT_FOUND: '没有找到对应的文件状态,可能已被清理。',
158
+ NO_CHANGES: '项目文件已经是这条消息发送前的状态,无需恢复文件。可选择「只回溯消息」重新开始这段对话。',
159
+ RESTORE_FAILED_ROLLED_BACK: '恢复未能完成,项目文件已自动还原到操作前的状态。',
160
+ CONVERSATION_REWIND_FAILED: '文件已恢复,但无法创建新对话;项目文件已自动还原。',
161
+ },
162
+ captureSnapshotLimit: '本轮要保存的文件总量超过上限,剩下的文件没有纳入检查点。恢复时不会改动它们。',
163
+ captureFileLimit: '本轮文件数量超过上限,多出来的文件没有纳入检查点。恢复时不会改动它们。',
164
+ captureSkipped: (count, example) => `有 ${String(count)} 个文件因为超过单文件大小上限或类型不受支持,没有纳入检查点${example === undefined ? '' : `(例如 ${example})`}。恢复时不会改动它们。`,
165
+ skipTimeout: '这个项目目录太大,在检查点时间上限内没保存完文件快照,所以本轮没有回退点(消息本身没有受影响)。可以在插件设置里调大「检查点时间上限」,或在目录根用 .dsh-rewindignore 排除大目录(例如 node_modules、构建产物、数据集)。仍可只回溯消息。',
166
+ skipDisabled: '自动文件检查点已在设置里关闭,本轮没有回退点。仍可只回溯消息。',
167
+ skipNewContentLimit: '这一轮新增的内容超过检查点预算,没有保存文件快照。仍可只回溯消息。',
168
+ skipOther: reason => `本轮没有保存文件检查点:${reason}仍可只回溯消息。`,
169
+ failureNotGitRepository: '这个项目目录不是 Git 仓库,回退功能无法保存文件检查点。仍可只回溯消息。',
170
+ failureGitStatus: message => `无法读取这个项目目录的 Git 状态:${message}仍可只回溯消息。`,
171
+ failureSizeOrCount: '本轮有文件超过检查点的大小或数量上限,没有保存文件检查点。仍可只回溯消息。',
172
+ failureUnsupportedType: '项目目录里有无法保存的特殊文件(如 socket、设备文件、FIFO),没有保存文件检查点。仍可只回溯消息。',
173
+ failureIgnoreFileInvalid: '项目目录里的 .dsh-rewindignore 内容无效,没有保存文件检查点。修正该文件后可重新发送消息。',
174
+ failureInvalidPath: '项目目录里有无法安全保存的路径(例如嵌套的独立 Git 仓库),没有保存文件检查点。仍可只回溯消息。',
175
+ failureOther: message => `没能保存这条消息发送前的文件:${message}仍可只回溯消息。`,
176
+ settingsCardTitle: 'Turn Rewind 回退设置',
177
+ settingsCardDescription: '自动文件检查点、信任策略与检查点保留上限;回退按钮在各条用户消息上',
178
+ autoCheckpointSection: '自动文件检查点',
179
+ settingsLoading: '正在加载设置…',
180
+ settingsUnavailable: '当前部署未提供设置服务,以下选项不可用。',
181
+ settingsReadOnly: '设置为只读(当前浏览器进程内保存),修改不可用。',
182
+ autoCheckpointLabel: '自动文件检查点',
183
+ autoCheckpointDescription: '关闭后不再为每条消息保存文件检查点;回退弹窗仍可只回溯消息',
184
+ trustLabel: '检查点信任策略',
185
+ trustDescription: '快速信任 Git/stat 元数据;严格会逐一重读文件内容',
186
+ overridden: '已覆盖',
187
+ resetToDefault: '恢复默认',
188
+ positiveInteger: field => `${field} 必须是正整数。`,
189
+ storageDir: directory => `存储目录(在 cordis.patch.yml 中配置,不可在线修改):${directory}`,
190
+ manageSection: '检查点管理',
191
+ refreshing: '正在刷新…',
192
+ refresh: '刷新',
193
+ clearing: '正在清理…',
194
+ confirmClearAll: '确认清空全部',
195
+ clearAll: '一键清空全部',
196
+ manageLoading: '正在读取检查点占用…',
197
+ manageTotal: (workspaces, points, size) => `共 ${String(workspaces)} 个工作区,${String(points)} 个检查点,约 ${size}(Git 原生检查点的实际磁盘占用以 Git 回收为准)。`,
198
+ workspacePoints: count => `${String(count)} 个检查点`,
199
+ pendingRecoveries: count => `${String(count)} 个恢复待处理`,
200
+ expand: '展开',
201
+ collapse: '收起',
202
+ clearWorkspace: '清空此项目',
203
+ fileCount: count => `${String(count)} 个文件`,
204
+ deleting: '正在删除…',
205
+ remove: '删除',
206
+ noCheckpoints: '还没有任何已保存的检查点。',
207
+ clearedAll: (deleted, retained, failures) => `已删除 ${String(deleted)} 个检查点${retained > 0 ? `,${String(retained)} 个受保护检查点未删除` : ''}${failures > 0 ? `,${String(failures)} 个工作区清理失败` : ''}。`,
208
+ cleared: (deleted, retained) => `已删除 ${String(deleted)} 个检查点${retained > 0 ? `,${String(retained)} 个受保护检查点未删除` : ''}。`,
209
+ checkpointModes: {
210
+ off: '关闭(不创建文件检查点)',
211
+ auto: '自动(推荐)',
212
+ 'git-native': 'Git 原生(大仓库)',
213
+ legacy: '完整快照(兼容模式)',
214
+ },
215
+ trustOptions: {
216
+ fast: '快速',
217
+ strict: '严格',
218
+ },
219
+ pointKinds: {
220
+ user: '手动',
221
+ rescue: '救援',
222
+ turn: '轮次',
223
+ },
224
+ numberFields: {
225
+ maxRestorePoints: { label: '用户/救援恢复点上限', description: '每个工作区保留的最大手动与救援恢复点数量' },
226
+ maxTurnCheckpointsPerSession: { label: '轮次检查点上限', description: '每个会话保留的最大自动轮次检查点数量(最旧的先清理)' },
227
+ maxFiles: { label: '单恢复点文件数上限', description: '一个恢复点最多纳入的文件数量' },
228
+ maxFileBytes: { label: '单文件大小上限', description: '读取单个普通文件的最大字节数' },
229
+ maxSnapshotBytes: { label: '快照总量上限', description: '单个恢复点读取的最大字节总量' },
230
+ planTtlMs: { label: '恢复计划有效期(毫秒)', description: '回溯计划从创建到失效的时间' },
231
+ staleLockMs: { label: '锁回收时长(毫秒)', description: '锁属主消失多久后允许回收该锁' },
232
+ turnCheckpointTimeoutMs: { label: '检查点超时(毫秒)', description: '单次自动检查点最多阻塞消息发送的时间,超时记录跳过' },
233
+ turnCheckpointMaxNewBytes: { label: '检查点读取上限', description: '单次 Git 原生检查点最多读取的未缓存字节数' },
234
+ },
235
+ manageMissingWorkspaces: '管理数据缺少 workspaces',
236
+ manageMissingRestorePoints: '管理数据缺少 restorePoints',
237
+ clearMissingReports: '清理结果缺少 reports',
238
+ previewMissingChanges: '回退预览缺少 changes',
239
+ previewMissingActiveSessionIds: '回退预览缺少 activeSessionIds',
240
+ invalidField: name => `${name} 无效`,
241
+ invalidObject: '服务器返回了无效对象',
242
+ unknownStatus: status => `未知回退状态:${status}`,
243
+ sessionNotReady: '新对话还没有准备好',
244
+ unparsableResponse: status => `回退服务返回了无法解析的内容(HTTP ${status})。`,
245
+ requestFailed: status => `请求失败:${status}`,
246
+ emptyResponse: '回退服务返回了空响应。',
247
+ };
248
+ /**
249
+ * Count one English noun, used only by the English copy.
250
+ * @param count - the quantity to render.
251
+ * @param singular - the noun in its singular form.
252
+ * @param plural - the plural form, when adding `s` is wrong.
253
+ * @returns the counted phrase.
254
+ */
255
+ function counted(count, singular, plural = `${singular}s`) {
256
+ return `${String(count)} ${count === 1 ? singular : plural}`;
257
+ }
258
+ /** English copy, used when the Host reports a non-Chinese UI language. */
259
+ const EN = {
260
+ rewindTooltip: 'Return to before sending this message',
261
+ dialogClose: 'Close',
262
+ dialogDescription: 'Review the files that would be restored and pick the rewind mode you want. The current session is unaffected.',
263
+ cancel: 'Cancel',
264
+ applying: 'Restoring…',
265
+ done: 'Done',
266
+ actionRestoreAndRestart: 'Restore and continue from here',
267
+ actionRestoreFiles: 'Restore files',
268
+ actionMessagesOnly: 'Rewind messages only',
269
+ checkingFiles: 'Checking which project files can be restored…',
270
+ previewPending: 'The files from before this message are still being saved. Try again in a moment.',
271
+ previewMissing: 'No files were saved from before this message. Rewind may not have been enabled yet, the record may have passed its retention limit, or automatic checkpoints may be turned off. You can still rewind messages only.',
272
+ modeBothTitle: 'Restore files and restart',
273
+ modeBothDescription: 'Creates a new session starting here (the current conversation is kept)',
274
+ modeCodeTitle: 'Restore files only',
275
+ modeCodeDescription: 'Restores the files from before this message and leaves the current conversation unchanged.',
276
+ modeMessagesTitle: 'Rewind messages only (files untouched)',
277
+ modeMessagesDescription: 'Creates a new session starting here and leaves the project files exactly as they are.',
278
+ filesUnchanged: 'Project files stay unchanged',
279
+ filesToRestore: count => `Will restore ${counted(count, 'file')}`,
280
+ summaryBoth: 'Continue in a new conversation after restoring',
281
+ summaryCode: 'The current conversation stays unchanged',
282
+ summaryMessages: 'Only creates a new conversation continuing from here',
283
+ sharedBlocked: 'Another conversation is still running in this project directory. Restoring files would affect it, so this operation is blocked. Wait for those conversations to finish or stop them, then check again.',
284
+ branchChanged: 'The current Git branch is not the one you were on when this message was sent. Restoring never switches branches; it only restores that file content onto the current branch.',
285
+ headAdvanced: 'There are new Git commits after this message. Restoring only changes files and never undoes commits; afterwards those files appear as uncommitted changes.',
286
+ driftBlocked: 'Git is in the middle of a merge, rebase, or similar operation. Finish or cancel that Git operation, then check again.',
287
+ planMissing: 'The restore information has expired. Check again.',
288
+ stale: 'The project files changed again after the check. To avoid overwriting the newer edits, this restore is no longer valid. Check again.',
289
+ nothingToRestore: 'The project files already match the state from before this message, so no files need restoring. Choose “Rewind messages only” to restart this conversation from here.',
290
+ loadingAllFiles: 'Loading all files…',
291
+ viewAllFiles: count => `View all ${counted(count, 'file')}`,
292
+ backupNote: 'The current files are backed up automatically before a restore; if the restore fails they are rolled back, so the project is never left partly restored.',
293
+ recheck: 'Check again',
294
+ completedMessages: 'A new conversation starting here has been created; the project files are unchanged.',
295
+ completedCode: 'The project files have been restored; the current conversation is unchanged. The files from before the restore were backed up automatically.',
296
+ completedBoth: 'The project files have been restored and a new conversation was created. The files from before the restore were backed up automatically.',
297
+ openFailed: reason => `The new conversation was created but could not be opened automatically: ${reason}`,
298
+ restoredButOpenFailed: reason => `The files were restored and the new conversation was created, but it could not be opened automatically: ${reason}`,
299
+ modeMismatch: mode => `The server returned a rewind mode that does not match: ${mode}`,
300
+ listChangedWhileExpanding: 'The project files changed while the full list was being loaded.',
301
+ incompleteFileList: 'Could not read the complete file list.',
302
+ fileKinds: {
303
+ added: 'Remove the file added later',
304
+ deleted: 'Bring the file back',
305
+ modified: 'Restore the earlier version',
306
+ 'mode-changed': 'Restore the file permissions',
307
+ 'type-changed': 'Restore the earlier file type',
308
+ },
309
+ errors: {
310
+ REWIND_ENDPOINT_UNAVAILABLE: 'The rewind service did not respond. Check that @anionex/dsh-turn-rewind is mounted in this DSH, restart DSH, and try again.',
311
+ REWIND_INVALID_RESPONSE: 'The rewind service returned content that could not be parsed. Restart DSH and try again.',
312
+ PLAN_STALE: 'The project files changed again after the check. To avoid overwriting the newer edits, check again before restoring.',
313
+ PLAN_STALE_WORKSPACE: 'The project files changed again after the check. To avoid overwriting the newer edits, check again before restoring.',
314
+ PLAN_STALE_REPOSITORY: 'The Git state changed after the check, so the restore is no longer valid. Check again and retry.',
315
+ WORKSPACE_IN_USE: 'Another conversation is still running in this project directory. Wait for those conversations to finish or stop them, then check again.',
316
+ WORKSPACE_LOCKED: 'Another restore is already working on this project directory. Wait for it to finish, then check again.',
317
+ WORKSPACE_CHANGED: 'This project directory is no longer in its original location, so the earlier restore points cannot be used.',
318
+ WORKSPACE_MODE_CHANGED: 'The workspace type of this project directory changed (for example from an ordinary directory to a Git repository). The earlier restore points no longer apply; send a new message to create a new one.',
319
+ HEAD_CHANGED: 'The commits or branch of this project changed. To avoid overwriting newer work, check again before restoring.',
320
+ REPOSITORY_CHANGED: 'This project directory no longer belongs to its original Git worktree, so it cannot be restored.',
321
+ GIT_OPERATION_CHANGED: 'Git is running another operation. Finish or cancel it, then check again.',
322
+ RESTORE_POINT_NOT_FOUND: 'No matching file state was found; it may already have been cleaned up.',
323
+ NO_CHANGES: 'The project files already match the state from before this message, so no files need restoring. Choose “Rewind messages only” to restart this conversation from here.',
324
+ RESTORE_FAILED_ROLLED_BACK: 'The restore did not finish; the project files were rolled back to their state before the operation.',
325
+ CONVERSATION_REWIND_FAILED: 'The files were restored but the new conversation could not be created; the project files were rolled back.',
326
+ },
327
+ captureSnapshotLimit: 'The files in this turn exceeded the total size limit, so the remaining files were left out of the checkpoint. A restore will not touch them.',
328
+ captureFileLimit: 'This turn had more files than the limit allows, so the extra files were left out of the checkpoint. A restore will not touch them.',
329
+ captureSkipped: (count, example) => `${counted(count, 'file')} ${count === 1 ? 'was' : 'were'} left out of the checkpoint for exceeding the per-file size limit or having an unsupported type${example === undefined ? '' : ` (for example ${example})`}. A restore will not touch them.`,
330
+ skipTimeout: 'This project directory is too large to finish a file snapshot within the checkpoint time limit, so this turn has no restore point (the message itself was unaffected). Raise “Checkpoint timeout (ms)” in the plugin settings, or exclude large directories with a .dsh-rewindignore in the directory root (for example node_modules, build output, datasets). You can still rewind messages only.',
331
+ skipDisabled: 'Automatic file checkpoints are turned off in settings, so this turn has no restore point. You can still rewind messages only.',
332
+ skipNewContentLimit: 'This turn added more content than the checkpoint budget allows, so no file snapshot was saved. You can still rewind messages only.',
333
+ skipOther: reason => `No file checkpoint was saved for this turn: ${reason} You can still rewind messages only.`,
334
+ failureNotGitRepository: 'This project directory is not a Git repository, so rewind cannot save file checkpoints. You can still rewind messages only.',
335
+ failureGitStatus: message => `Could not read the Git state of this project directory: ${message} You can still rewind messages only.`,
336
+ failureSizeOrCount: 'Files in this turn exceeded the checkpoint size or count limit, so no file checkpoint was saved. You can still rewind messages only.',
337
+ failureUnsupportedType: 'The project directory contains special files that cannot be saved (such as sockets, device files, or FIFOs), so no file checkpoint was saved. You can still rewind messages only.',
338
+ failureIgnoreFileInvalid: 'The .dsh-rewindignore in the project directory is invalid, so no file checkpoint was saved. Fix that file and send the message again.',
339
+ failureInvalidPath: 'The project directory contains paths that cannot be saved safely (for example a nested standalone Git repository), so no file checkpoint was saved. You can still rewind messages only.',
340
+ failureOther: message => `Could not save the files from before this message: ${message} You can still rewind messages only.`,
341
+ settingsCardTitle: 'Turn Rewind settings',
342
+ settingsCardDescription: 'Automatic file checkpoints, trust policy, and checkpoint retention limits; the rewind button sits on each user message',
343
+ autoCheckpointSection: 'Automatic file checkpoints',
344
+ settingsLoading: 'Loading settings…',
345
+ settingsUnavailable: 'This deployment provides no settings service, so the options below are unavailable.',
346
+ settingsReadOnly: 'Settings are read-only (kept inside this browser process), so changes are unavailable.',
347
+ autoCheckpointLabel: 'Automatic file checkpoints',
348
+ autoCheckpointDescription: 'When off, no file checkpoint is saved for each message; the rewind dialog can still rewind messages only',
349
+ trustLabel: 'Checkpoint trust policy',
350
+ trustDescription: 'Fast trusts Git/stat metadata; Strict re-reads every file’s content',
351
+ overridden: 'Overridden',
352
+ resetToDefault: 'Reset to default',
353
+ positiveInteger: field => `${field} must be a positive integer.`,
354
+ storageDir: directory => `Storage directory (configured in cordis.patch.yml, not editable here): ${directory}`,
355
+ manageSection: 'Checkpoint management',
356
+ refreshing: 'Refreshing…',
357
+ refresh: 'Refresh',
358
+ clearing: 'Clearing…',
359
+ confirmClearAll: 'Confirm clear all',
360
+ clearAll: 'Clear all',
361
+ manageLoading: 'Reading checkpoint usage…',
362
+ manageTotal: (workspaces, points, size) => `${counted(workspaces, 'workspace')}, ${counted(points, 'checkpoint')}, about ${size} (disk use for Git-native checkpoints follows Git garbage collection).`,
363
+ workspacePoints: count => counted(count, 'checkpoint'),
364
+ pendingRecoveries: count => `${counted(count, 'recovery', 'recoveries')} pending`,
365
+ expand: 'Expand',
366
+ collapse: 'Collapse',
367
+ clearWorkspace: 'Clear this project',
368
+ fileCount: count => counted(count, 'file'),
369
+ deleting: 'Deleting…',
370
+ remove: 'Delete',
371
+ noCheckpoints: 'No checkpoints have been saved yet.',
372
+ clearedAll: (deleted, retained, failures) => `Deleted ${counted(deleted, 'checkpoint')}${retained > 0 ? `; ${counted(retained, 'protected checkpoint')} kept` : ''}${failures > 0 ? `; ${counted(failures, 'workspace')} failed to clear` : ''}.`,
373
+ cleared: (deleted, retained) => `Deleted ${counted(deleted, 'checkpoint')}${retained > 0 ? `; ${counted(retained, 'protected checkpoint')} kept` : ''}.`,
374
+ checkpointModes: {
375
+ off: 'Off (no file checkpoints)',
376
+ auto: 'Auto (recommended)',
377
+ 'git-native': 'Git-native (large repositories)',
378
+ legacy: 'Full snapshot (compatibility mode)',
379
+ },
380
+ trustOptions: {
381
+ fast: 'Fast',
382
+ strict: 'Strict',
383
+ },
384
+ pointKinds: {
385
+ user: 'Manual',
386
+ rescue: 'Rescue',
387
+ turn: 'Turn',
388
+ },
389
+ numberFields: {
390
+ maxRestorePoints: { label: 'User/rescue restore point limit', description: 'Most manual and rescue restore points kept per workspace' },
391
+ maxTurnCheckpointsPerSession: { label: 'Turn checkpoint limit', description: 'Most automatic turn checkpoints kept per session (oldest pruned first)' },
392
+ maxFiles: { label: 'Files per restore point', description: 'Most files one restore point may include' },
393
+ maxFileBytes: { label: 'Maximum file size', description: 'Most bytes read from a single regular file' },
394
+ maxSnapshotBytes: { label: 'Maximum snapshot size', description: 'Most bytes read in total for one restore point' },
395
+ planTtlMs: { label: 'Restore plan lifetime (ms)', description: 'How long a rewind plan stays valid after it is created' },
396
+ staleLockMs: { label: 'Stale lock timeout (ms)', description: 'How long after its owner disappears a lock may be reclaimed' },
397
+ turnCheckpointTimeoutMs: { label: 'Checkpoint timeout (ms)', description: 'How long one automatic checkpoint may hold up sending a message before it is recorded as skipped' },
398
+ turnCheckpointMaxNewBytes: { label: 'Checkpoint read limit', description: 'Most uncached bytes one Git-native checkpoint may read' },
399
+ },
400
+ manageMissingWorkspaces: 'The management data is missing workspaces',
401
+ manageMissingRestorePoints: 'The management data is missing restorePoints',
402
+ clearMissingReports: 'The clear result is missing reports',
403
+ previewMissingChanges: 'The rewind preview is missing changes',
404
+ previewMissingActiveSessionIds: 'The rewind preview is missing activeSessionIds',
405
+ invalidField: name => `${name} is invalid`,
406
+ invalidObject: 'The server returned an invalid object',
407
+ unknownStatus: status => `Unknown rewind status: ${status}`,
408
+ sessionNotReady: 'The new conversation is not ready yet',
409
+ unparsableResponse: status => `The rewind service returned content that could not be parsed (HTTP ${status}).`,
410
+ requestFailed: status => `Request failed: ${status}`,
411
+ emptyResponse: 'The rewind service returned an empty response.',
412
+ };
413
+ const TEXT = { zh: ZH, en: EN };
414
+ /**
415
+ * Active UI language, and the listeners rendering it.
416
+ *
417
+ * One browser runs one copy of this plugin, so the active language is module
418
+ * state rather than React context: the pure helpers below (`friendlyError`,
419
+ * `explainCheckpointSkip`, and the decoders) produce user-facing sentences
420
+ * outside any render and would otherwise need their exported signatures
421
+ * changed.
422
+ */
423
+ let uiLocale = 'zh';
424
+ const localeListeners = new Set();
425
+ /**
426
+ * Resolve one Host locale id to a language this plugin ships copy for.
427
+ *
428
+ * An absent, empty, or unreadable id means the Host published no UI language —
429
+ * DSH releases without the locale plugin, and profiles that do not mount it —
430
+ * and keeps Chinese, so an existing install never changes language on upgrade.
431
+ * Any other registered language resolves to English, mirroring the Host's own
432
+ * per-key fallback chain, which terminates at English rather than Chinese.
433
+ * @param active - the Host's active locale id, when it publishes one.
434
+ * @returns the language whose copy to render.
435
+ */
436
+ function resolveUiLocale(active) {
437
+ if (typeof active !== 'string' || active === '')
438
+ return 'zh';
439
+ return /^zh(?:[-_]|$)/i.test(active) ? 'zh' : 'en';
440
+ }
441
+ /** Current UI copy. Chinese until the Host publishes another language. */
442
+ function uiText() {
443
+ return TEXT[uiLocale];
444
+ }
445
+ /** Adopt one Host locale id and re-render everything currently mounted. */
446
+ function adoptLocale(active) {
447
+ const next = resolveUiLocale(active);
448
+ if (next === uiLocale)
449
+ return;
450
+ uiLocale = next;
451
+ for (const listener of localeListeners)
452
+ listener();
453
+ }
454
+ /** `useSyncExternalStore` subscribe half for the active language. */
455
+ function subscribeLocale(listener) {
456
+ localeListeners.add(listener);
457
+ return () => { localeListeners.delete(listener); };
458
+ }
459
+ /** Read the current copy and re-render this component when the language changes. */
460
+ function useText() {
461
+ return (0, react_1.useSyncExternalStore)(subscribeLocale, uiText, uiText);
462
+ }
463
+ /**
464
+ * Follow the Host's UI language for as long as the plugin is mounted.
465
+ *
466
+ * The locale service is read through `ctx.get`, Cordis's un-injected read, and
467
+ * not declared in `inject`: an injected service the profile does not mount
468
+ * holds the fiber inactive, which would stop the rewind button appearing at all
469
+ * on a profile without the locale plugin. Late arrival is covered by the
470
+ * `locale/change` event, which is emitted on the context rather than on the
471
+ * service, so a listener registered here sees a switch even when the locale
472
+ * plugin loads after this one.
473
+ * @param ctx - the client context this plugin was applied to.
474
+ * @returns the teardown restoring the default language.
475
+ */
476
+ function followHostLocale(ctx) {
477
+ adoptLocale(hostLocale(ctx));
478
+ // Client plugin apply() order is not guaranteed, and `locale` is read
479
+ // un-injected (see the doc comment above) so this plugin never forces it
480
+ // to load first. When `dsh-client-locale` mounts after this plugin, the
481
+ // read above returns `undefined` and the surface falls back to Chinese;
482
+ // nothing then corrects it, because a stored preference that was already
483
+ // settled before this plugin mounted never fires another `locale/change`
484
+ // event. Re-checking once on the next microtask and once on the next
485
+ // macrotask catches both same-tick and deferred registration without
486
+ // adding a hard dependency; `adoptLocale` is a no-op when the language
487
+ // has not actually changed, so the extra calls are always safe.
488
+ const recheck = () => adoptLocale(hostLocale(ctx));
489
+ // `Promise` is an ECMAScript built-in present in every realm, unlike the
490
+ // host-provided `queueMicrotask`, which a constrained embedding (this
491
+ // plugin's own test sandbox included) need not supply.
492
+ void Promise.resolve().then(recheck);
493
+ const timeoutId = setTimeout(recheck, 0);
494
+ let off;
495
+ try {
496
+ off = ctx.on?.('locale/change', (snapshot) => {
497
+ adoptLocale(typeof snapshot?.active === 'string' ? snapshot.active : hostLocale(ctx));
498
+ });
499
+ }
500
+ catch {
501
+ off = undefined;
502
+ }
503
+ return () => {
504
+ clearTimeout(timeoutId);
505
+ if (typeof off === 'function')
506
+ off();
507
+ adoptLocale(undefined);
508
+ };
509
+ }
510
+ /**
511
+ * Read the Host's active locale id.
512
+ *
513
+ * The locale service belongs to the Host, so its presence and shape are not
514
+ * this plugin's to guarantee: a language that cannot be read leaves the rewind
515
+ * surface in Chinese instead of failing the effect that installs the button.
516
+ * @param ctx - the client context this plugin was applied to.
517
+ * @returns the active locale id, or `undefined` when none can be read.
518
+ */
519
+ function hostLocale(ctx) {
520
+ try {
521
+ const active = ctx.get?.('locale')?.getSnapshot().active;
522
+ return typeof active === 'string' ? active : undefined;
523
+ }
524
+ catch {
525
+ return undefined;
526
+ }
527
+ }
91
528
  /** Return the rewind anchor and editable text owned by one direct user message. */
92
529
  function selectRewindMessage(node) {
93
530
  if (node.kind !== 'user' || !Number.isSafeInteger(node.seq) || node.seq < 0)
@@ -103,11 +540,12 @@ function selectRewindMessage(node) {
103
540
  *
104
541
  * Every service read on `ctx` must be declared here: Cordis throws while reading an
105
542
  * undeclared service off the context proxy, before optional chaining can apply.
106
- * `settingsScope` is provided by `@deepseek-ai/dsh-client-ui-settings` and may be
107
- * absent, which is what `ctx.settingsScope?.bind(…)` below relies on.
543
+ * The settings transport is read through `ctx.get`: older Hosts expose
544
+ * `settingsScope`, while DSH 0.1.7 exposes `configForms`.
108
545
  */
109
- exports.inject = ['slots', 'sessions', 'conversation', 'settingsScope'];
546
+ exports.inject = ['slots', 'sessions', 'conversation'];
110
547
  function apply(ctx) {
548
+ ctx.effect(() => followHostLocale(ctx), 'turn-rewind: locale');
111
549
  ctx.effect(() => {
112
550
  if (document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) !== null)
113
551
  return () => { };
@@ -132,9 +570,25 @@ function apply(ctx) {
132
570
  name: 'settings.plugin.item',
133
571
  key: 'turn-rewind',
134
572
  inject: () => ({
135
- scope: ctx.settingsScope?.bind({ namespace: 'turn-rewind' }),
573
+ scope: settingsFormScope(ctx),
136
574
  }),
137
575
  }, TurnRewindSettingsCard));
576
+ // DSH 0.1.7 replaced the per-plugin item seat with feature-owned Settings tabs.
577
+ ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
578
+ name: 'settings.plugins.tab',
579
+ id: 'turn-rewind',
580
+ order: 80,
581
+ label: () => uiText().settingsCardTitle,
582
+ inject: () => ({ scope: settingsFormScope(ctx) }),
583
+ }, TurnRewindSettingsCard));
584
+ }
585
+ /** Read whichever settings transport this Host supplies without requiring both services. */
586
+ function settingsFormScope(ctx) {
587
+ const forms = ctx.get?.('configForms');
588
+ if (forms !== undefined)
589
+ return forms.get('turn-rewind');
590
+ const legacy = ctx.get?.('settingsScope');
591
+ return legacy?.bind({ namespace: 'turn-rewind' });
138
592
  }
139
593
  /** Narrow a chat node source to the keyed store form. */
140
594
  function isNodeStore(value) {
@@ -207,6 +661,7 @@ function RewindMessagePortals({ sessionId, openRestoredSession, useSession, useC
207
661
  }
208
662
  /** User-message action and its review-first file/conversation restore dialog. */
209
663
  function RewindMessageAction({ matched, sessionId, openRestoredSession }) {
664
+ const t = useText();
210
665
  const [open, setOpen] = (0, react_1.useState)(false);
211
666
  const [loading, setLoading] = (0, react_1.useState)(false);
212
667
  const [preview, setPreview] = (0, react_1.useState)(null);
@@ -312,7 +767,7 @@ function RewindMessageAction({ matched, sessionId, openRestoredSession }) {
312
767
  || page.checkpointId !== ready.checkpointId
313
768
  || page.totalChanges !== ready.totalChanges
314
769
  || page.offset !== offset) {
315
- throw new RewindRequestError('PLAN_STALE', '项目文件在展开列表时发生了变化。');
770
+ throw new RewindRequestError('PLAN_STALE', t.listChangedWhileExpanding);
316
771
  }
317
772
  collected.push(...page.changes);
318
773
  offset += page.changes.length;
@@ -320,7 +775,7 @@ function RewindMessageAction({ matched, sessionId, openRestoredSession }) {
320
775
  break;
321
776
  }
322
777
  if (offset !== ready.totalChanges)
323
- throw new RewindRequestError('PLAN_STALE', '无法读取完整的文件列表。');
778
+ throw new RewindRequestError('PLAN_STALE', t.incompleteFileList);
324
779
  setPreview({ ...ready, changes: collected, truncated: false });
325
780
  }
326
781
  catch (caught) {
@@ -359,33 +814,33 @@ function RewindMessageAction({ matched, sessionId, openRestoredSession }) {
359
814
  const result = recordOf(await responseJson(response));
360
815
  const resultMode = requiredString(result.mode, 'mode');
361
816
  if (resultMode !== mode)
362
- throw new Error(`服务器返回了不匹配的回退模式:${resultMode}`);
817
+ throw new Error(t.modeMismatch(resultMode));
363
818
  if (mode === 'messages') {
364
819
  const childSessionId = requiredString(result.sessionId, 'sessionId');
365
- setCompleted('已创建从这里开始的新对话;项目文件保持不变。');
820
+ setCompleted(t.completedMessages);
366
821
  try {
367
822
  await openRestoredSession(childSessionId, matched.promptText);
368
823
  setOpen(false);
369
824
  }
370
825
  catch (navigationError) {
371
- setError(`新对话已创建,但没能自动打开:${messageOf(navigationError)}`);
826
+ setError(t.openFailed(messageOf(navigationError)));
372
827
  }
373
828
  return;
374
829
  }
375
830
  if (mode === 'code') {
376
831
  requiredString(result.rescuePointId, 'rescuePointId');
377
- setCompleted('项目文件已恢复;当前对话保持不变。恢复前的文件已自动备份。');
832
+ setCompleted(t.completedCode);
378
833
  return;
379
834
  }
380
835
  const childSessionId = requiredString(result.sessionId, 'sessionId');
381
836
  requiredString(result.rescuePointId, 'rescuePointId');
382
- setCompleted('项目文件已恢复,并已创建新对话。恢复前的文件已自动备份。');
837
+ setCompleted(t.completedBoth);
383
838
  try {
384
839
  await openRestoredSession(childSessionId, matched.promptText);
385
840
  setOpen(false);
386
841
  }
387
842
  catch (navigationError) {
388
- setError(`文件已经恢复,新对话也已创建,但没能自动打开:${messageOf(navigationError)}`);
843
+ setError(t.restoredButOpenFailed(messageOf(navigationError)));
389
844
  }
390
845
  }
391
846
  catch (caught) {
@@ -400,45 +855,28 @@ function RewindMessageAction({ matched, sessionId, openRestoredSession }) {
400
855
  }
401
856
  };
402
857
  const captureNotice = describeCaptureNotice(ready);
403
- const actionLabel = mode === 'both' ? '恢复并从这里继续' : mode === 'code' ? '恢复文件' : '只回溯消息';
858
+ const actionLabel = mode === 'both' ? t.actionRestoreAndRestart : mode === 'code' ? t.actionRestoreFiles : t.actionMessagesOnly;
404
859
  const radioName = `dcl-rewind-${sessionId}-${String(matched.messageSeq)}`;
405
860
  const branchChanged = ready !== null && ready.checkpointBranch !== ready.currentBranch;
406
- return ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-tail", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Tooltip, { label: "\u6062\u590D\u5230\u53D1\u9001\u8FD9\u6761\u6D88\u606F\u4E4B\u524D", side: "bottom", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dcl-rewind-trigger", onClick: show, "aria-label": "\u6062\u590D\u5230\u53D1\u9001\u8FD9\u6761\u6D88\u606F\u4E4B\u524D", children: (0, jsx_runtime_1.jsx)(RewindIcon, { size: 16 }) }) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Modal, { open: open, onClose: close, title: "\u6062\u590D\u5230\u53D1\u9001\u8FD9\u6761\u6D88\u606F\u4E4B\u524D", closeLabel: "\u5173\u95ED", description: "\u67E5\u770B\u6062\u590D\u7684\u6587\u4EF6\uFF0C\u9009\u62E9\u9002\u5408\u4F60\u7684\u56DE\u9000\u65B9\u5F0F\u3002\u5F53\u524D\u4F1A\u8BDD\u4E0D\u53D7\u5F71\u54CD\u3002", className: "dcl-rewind-dialog", contentClassName: "dcl-rewind-content", footer: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", onClick: close, disabled: applying, children: "\u53D6\u6D88" }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", onClick: () => { void applyRestore(); }, disabled: !canApply, children: applying ? '正在恢复…' : completed === null ? actionLabel : '已完成' })] })), children: (0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-body", children: [loading && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: "\u6B63\u5728\u68C0\u67E5\u53EF\u4EE5\u6062\u590D\u7684\u9879\u76EE\u6587\u4EF6\u2026" }), preview?.status === 'pending' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: "\u8FD9\u6761\u6D88\u606F\u53D1\u9001\u524D\u7684\u6587\u4EF6\u8FD8\u5728\u4FDD\u5B58\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" }), preview?.status === 'missing' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: "\u6CA1\u6709\u4FDD\u5B58\u8FD9\u6761\u6D88\u606F\u53D1\u9001\u524D\u7684\u6587\u4EF6\u3002\u53EF\u80FD\u662F\u5F53\u65F6\u8FD8\u6CA1\u542F\u7528\u56DE\u9000\u529F\u80FD\u3001\u8BB0\u5F55\u5DF2\u8D85\u8FC7\u4FDD\u7559\u671F\u9650\uFF0C\u6216\u5DF2\u5173\u95ED\u81EA\u52A8\u68C0\u67E5\u70B9\u3002\u4ECD\u53EF\u53EA\u56DE\u6EAF\u6D88\u606F\u3002" }), preview?.status === 'skipped' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: explainCheckpointSkip(preview.reason) }), preview?.status === 'failed' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: explainCheckpointFailure(preview.error) }), preview !== null && ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-options", children: [(0, jsx_runtime_1.jsxs)("label", { className: "dcl-rewind-option", "data-selected": mode === 'both', "data-disabled": applying || !hasFileChanges, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: radioName, checked: mode === 'both', disabled: applying || !hasFileChanges, onChange: () => { chooseMode('both'); } }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-rewind-option-content", children: [(0, jsx_runtime_1.jsx)("strong", { children: "\u6062\u590D\u6587\u4EF6\u5E76\u4ECE\u8FD9\u91CC\u7EE7\u7EED" }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-option-description", children: "\u521B\u5EFA\u4E00\u4E2A\u4ECE\u8FD9\u91CC\u5F00\u59CB\u7684\u65B0\u4F1A\u8BDD\uFF08\u5F53\u524D\u5BF9\u8BDD\u4F1A\u4FDD\u7559\uFF09" })] })] }), (0, jsx_runtime_1.jsxs)("label", { className: "dcl-rewind-option", "data-selected": mode === 'code', "data-disabled": applying || !hasFileChanges, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: radioName, checked: mode === 'code', disabled: applying || !hasFileChanges, onChange: () => { chooseMode('code'); } }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-rewind-option-content", children: [(0, jsx_runtime_1.jsx)("strong", { children: "\u53EA\u6062\u590D\u6587\u4EF6" }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-option-description", children: "\u6062\u590D\u8FD9\u6761\u6D88\u606F\u53D1\u9001\u524D\u7684\u6587\u4EF6\uFF0C\u5F53\u524D\u5BF9\u8BDD\u4FDD\u6301\u4E0D\u53D8\u3002" })] })] }), (0, jsx_runtime_1.jsxs)("label", { className: "dcl-rewind-option", "data-selected": mode === 'messages', "data-disabled": applying, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: radioName, checked: mode === 'messages', disabled: applying, onChange: () => { chooseMode('messages'); } }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-rewind-option-content", children: [(0, jsx_runtime_1.jsx)("strong", { children: "\u53EA\u56DE\u6EAF\u6D88\u606F\uFF08\u4E0D\u52A8\u6587\u4EF6\uFF09" }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-option-description", children: "\u521B\u5EFA\u4E00\u4E2A\u4ECE\u8FD9\u91CC\u5F00\u59CB\u7684\u65B0\u4F1A\u8BDD\uFF0C\u9879\u76EE\u6587\u4EF6\u4FDD\u6301\u5F53\u524D\u72B6\u6001\u3002" })] })] })] })), ready !== null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [captureNotice !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-warning", children: captureNotice }), (0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-summary", children: [mode === 'messages'
407
- ? (0, jsx_runtime_1.jsx)("strong", { children: "\u9879\u76EE\u6587\u4EF6\u4FDD\u6301\u4E0D\u53D8" })
408
- : (0, jsx_runtime_1.jsxs)("strong", { children: ["\u5C06\u6062\u590D ", String(ready.totalChanges), " \u4E2A\u6587\u4EF6"] }), (0, jsx_runtime_1.jsx)("span", { children: mode === 'both' ? '恢复后在新对话里继续' : mode === 'code' ? '当前对话保持不变' : '仅创建从这里继续的新对话' })] }), sharedBlocked && ((0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: "\u8FD9\u4E2A\u9879\u76EE\u76EE\u5F55\u8FD8\u6709\u522B\u7684\u5BF9\u8BDD\u6B63\u5728\u8FD0\u884C\u3002\u6062\u590D\u6587\u4EF6\u4F1A\u5F71\u54CD\u5230\u5B83\u4EEC\uFF0C\u56E0\u6B64\u672C\u6B21\u64CD\u4F5C\u5DF2\u88AB\u963B\u6B62\u3002\u8BF7\u7B49\u90A3\u4E9B\u5BF9\u8BDD\u7ED3\u675F\u6216\u505C\u6B62\u540E\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002" })), ready.headChanged && !ready.operationChanged && ((0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-warning", children: branchChanged
409
- ? '当前所在的 Git 分支和发送这条消息时不同。恢复不会切换分支,只会把当时的文件内容恢复到当前分支。'
410
- : '这条消息之后有了新的 Git 提交。恢复只会改文件,不会撤销提交;完成后这些文件会显示为未提交修改。' })), driftBlocked && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-warning", children: "Git \u6B63\u5728\u8FDB\u884C\u5408\u5E76\u3001\u53D8\u57FA\u6216\u7C7B\u4F3C\u64CD\u4F5C\u3002\u8BF7\u5148\u5B8C\u6210\u6216\u53D6\u6D88\u8FD9\u6B21 Git \u64CD\u4F5C\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002" }), planMissing && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: "\u6062\u590D\u4FE1\u606F\u5DF2\u7ECF\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5\u3002" }), stale && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: "\u9879\u76EE\u6587\u4EF6\u5728\u68C0\u67E5\u540E\u53C8\u53D1\u751F\u4E86\u53D8\u5316\u3002\u4E3A\u907F\u514D\u8986\u76D6\u65B0\u4FEE\u6539\uFF0C\u8FD9\u6B21\u6062\u590D\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5\u3002" }), ready.totalChanges === 0 && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: "\u9879\u76EE\u6587\u4EF6\u5DF2\u7ECF\u662F\u8FD9\u6761\u6D88\u606F\u53D1\u9001\u524D\u7684\u72B6\u6001\uFF0C\u65E0\u9700\u6062\u590D\u6587\u4EF6\u3002\u53EF\u9009\u62E9\u300C\u53EA\u56DE\u6EAF\u6D88\u606F\u300D\u91CD\u65B0\u5F00\u59CB\u8FD9\u6BB5\u5BF9\u8BDD\u3002" }), ready.changes.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "dcl-rewind-files", children: ready.changes.map(change => (0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-file", children: [(0, jsx_runtime_1.jsx)("code", { children: change.path }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-kind", children: fileRecoveryLabel(change.kind) })] }, change.path)) })), ready.truncated && ((0, jsx_runtime_1.jsx)("div", { className: "dcl-rewind-file-actions", children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { void loadAllChanges(); }, disabled: loadingDetails, children: loadingDetails ? '正在读取全部文件…' : `查看全部 ${String(ready.totalChanges)} 个文件` }) }))] })), completed !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: completed }), error !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: error }), error !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-backup", children: "\u6062\u590D\u524D\u4F1A\u81EA\u52A8\u5907\u4EFD\u5F53\u524D\u6587\u4EF6\uFF1B\u82E5\u6062\u590D\u5931\u8D25\u4F1A\u81EA\u52A8\u8FD8\u539F\uFF0C\u9879\u76EE\u4E0D\u4F1A\u505C\u7559\u5728\u53EA\u6062\u590D\u4E86\u4E00\u90E8\u5206\u7684\u72B6\u6001\u3002" }), !loading && (preview?.status !== 'ready' || stale || planMissing || sharedBlocked || driftBlocked) && (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { className: "dcl-rewind-retry", variant: "outline", size: "sm", onClick: () => { void load(); }, children: "\u91CD\u65B0\u68C0\u67E5" })] }) })] }));
861
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-tail", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Tooltip, { label: t.rewindTooltip, side: "bottom", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dcl-rewind-trigger", onClick: show, "aria-label": t.rewindTooltip, children: (0, jsx_runtime_1.jsx)(RewindIcon, { size: 16 }) }) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Modal, { open: open, onClose: close, title: t.rewindTooltip, closeLabel: t.dialogClose, description: t.dialogDescription, className: "dcl-rewind-dialog", contentClassName: "dcl-rewind-content", footer: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", onClick: close, disabled: applying, children: t.cancel }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", onClick: () => { void applyRestore(); }, disabled: !canApply, children: applying ? t.applying : completed === null ? actionLabel : t.done })] })), children: (0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-body", children: [loading && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: t.checkingFiles }), preview?.status === 'pending' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: t.previewPending }), preview?.status === 'missing' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: t.previewMissing }), preview?.status === 'skipped' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: explainCheckpointSkip(preview.reason) }), preview?.status === 'failed' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: explainCheckpointFailure(preview.error) }), preview !== null && ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-options", children: [(0, jsx_runtime_1.jsxs)("label", { className: "dcl-rewind-option", "data-selected": mode === 'both', "data-disabled": applying || !hasFileChanges, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: radioName, checked: mode === 'both', disabled: applying || !hasFileChanges, onChange: () => { chooseMode('both'); } }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-rewind-option-content", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.modeBothTitle }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-option-description", children: t.modeBothDescription })] })] }), (0, jsx_runtime_1.jsxs)("label", { className: "dcl-rewind-option", "data-selected": mode === 'code', "data-disabled": applying || !hasFileChanges, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: radioName, checked: mode === 'code', disabled: applying || !hasFileChanges, onChange: () => { chooseMode('code'); } }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-rewind-option-content", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.modeCodeTitle }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-option-description", children: t.modeCodeDescription })] })] }), (0, jsx_runtime_1.jsxs)("label", { className: "dcl-rewind-option", "data-selected": mode === 'messages', "data-disabled": applying, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: radioName, checked: mode === 'messages', disabled: applying, onChange: () => { chooseMode('messages'); } }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-rewind-option-content", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.modeMessagesTitle }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-option-description", children: t.modeMessagesDescription })] })] })] })), ready !== null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [captureNotice !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-warning", children: captureNotice }), (0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-summary", children: [mode === 'messages'
862
+ ? (0, jsx_runtime_1.jsx)("strong", { children: t.filesUnchanged })
863
+ : (0, jsx_runtime_1.jsx)("strong", { children: t.filesToRestore(ready.totalChanges) }), (0, jsx_runtime_1.jsx)("span", { children: mode === 'both' ? t.summaryBoth : mode === 'code' ? t.summaryCode : t.summaryMessages })] }), sharedBlocked && ((0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: t.sharedBlocked })), ready.headChanged && !ready.operationChanged && ((0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-warning", children: branchChanged ? t.branchChanged : t.headAdvanced })), driftBlocked && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-warning", children: t.driftBlocked }), planMissing && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: t.planMissing }), stale && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: t.stale }), ready.totalChanges === 0 && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: t.nothingToRestore }), ready.changes.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "dcl-rewind-files", children: ready.changes.map(change => (0, jsx_runtime_1.jsxs)("div", { className: "dcl-rewind-file", children: [(0, jsx_runtime_1.jsx)("code", { children: change.path }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-rewind-kind", children: fileRecoveryLabel(change.kind) })] }, change.path)) })), ready.truncated && ((0, jsx_runtime_1.jsx)("div", { className: "dcl-rewind-file-actions", children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { void loadAllChanges(); }, disabled: loadingDetails, children: loadingDetails ? t.loadingAllFiles : t.viewAllFiles(ready.totalChanges) }) }))] })), completed !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-status", children: completed }), error !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-error", children: error }), error !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-rewind-backup", children: t.backupNote }), !loading && (preview?.status !== 'ready' || stale || planMissing || sharedBlocked || driftBlocked) && (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { className: "dcl-rewind-retry", variant: "outline", size: "sm", onClick: () => { void load(); }, children: t.recheck })] }) })] }));
411
864
  }
412
865
  const EMPTY_SETTINGS_SNAPSHOT = {
413
866
  status: 'unavailable', value: undefined, base: undefined, user: undefined, revision: undefined, writable: false, mode: 'host',
414
867
  };
868
+ /** Numeric settings fields, in the order the card renders them. */
415
869
  const NUMBER_FIELDS = [
416
- { key: 'maxRestorePoints', label: '用户/救援恢复点上限', description: '每个工作区保留的最大手动与救援恢复点数量' },
417
- { key: 'maxTurnCheckpointsPerSession', label: '轮次检查点上限', description: '每个会话保留的最大自动轮次检查点数量(最旧的先清理)' },
418
- { key: 'maxFiles', label: '单恢复点文件数上限', description: '一个恢复点最多纳入的文件数量' },
419
- { key: 'maxFileBytes', label: '单文件大小上限', description: '读取单个普通文件的最大字节数' },
420
- { key: 'maxSnapshotBytes', label: '快照总量上限', description: '单个恢复点读取的最大字节总量' },
421
- { key: 'planTtlMs', label: '恢复计划有效期(毫秒)', description: '回溯计划从创建到失效的时间' },
422
- { key: 'staleLockMs', label: '锁回收时长(毫秒)', description: '锁属主消失多久后允许回收该锁' },
423
- { key: 'turnCheckpointTimeoutMs', label: '检查点超时(毫秒)', description: '单次自动检查点最多阻塞消息发送的时间,超时记录跳过' },
424
- { key: 'turnCheckpointMaxNewBytes', label: '检查点读取上限', description: '单次 Git 原生检查点最多读取的未缓存字节数' },
870
+ 'maxRestorePoints', 'maxTurnCheckpointsPerSession', 'maxFiles', 'maxFileBytes', 'maxSnapshotBytes',
871
+ 'planTtlMs', 'staleLockMs', 'turnCheckpointTimeoutMs', 'turnCheckpointMaxNewBytes',
425
872
  ];
426
- const CHECKPOINT_MODE_LABELS = {
427
- off: '关闭(不创建文件检查点)',
428
- 'git-native': 'Git 原生(推荐大仓库)',
429
- legacy: '完整快照(兼容模式)',
430
- };
431
- const TRUST_LABELS = {
432
- fast: '快速',
433
- strict: '严格',
434
- };
435
- const POINT_KIND_LABELS = {
436
- user: '手动',
437
- rescue: '救援',
438
- turn: '轮次',
439
- };
873
+ /** Checkpoint modes, in the order the select offers them. */
874
+ const CHECKPOINT_MODES = ['off', 'auto', 'git-native', 'legacy'];
875
+ /** Checkpoint trust policies, in the order the select offers them. */
876
+ const TRUST_MODES = ['fast', 'strict'];
440
877
  /** Settings card for the `turn-rewind` namespace: runtime options plus checkpoint management. */
441
878
  function TurnRewindSettingsCard({ scope }) {
879
+ const t = useText();
442
880
  const snapshot = (0, react_1.useSyncExternalStore)((0, react_1.useCallback)((notify) => scope?.subscribe(notify) ?? (() => { }), [scope]), (0, react_1.useCallback)(() => scope?.getSnapshot() ?? EMPTY_SETTINGS_SNAPSHOT, [scope]));
443
881
  const [manage, setManage] = (0, react_1.useState)(null);
444
882
  const [manageLoading, setManageLoading] = (0, react_1.useState)(false);
@@ -489,7 +927,7 @@ function TurnRewindSettingsCard({ scope }) {
489
927
  }
490
928
  const parsed = Number.parseInt(draft, 10);
491
929
  if (!Number.isSafeInteger(parsed) || parsed <= 0) {
492
- setFormError(`${field} 必须是正整数。`);
930
+ setFormError(t.positiveInteger(field));
493
931
  return;
494
932
  }
495
933
  if (parsed === value?.[field])
@@ -530,18 +968,18 @@ function TurnRewindSettingsCard({ scope }) {
530
968
  return next;
531
969
  });
532
970
  };
533
- return ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-card", "data-open": cardOpen, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "dcl-trs-card-head", "aria-expanded": cardOpen, onClick: () => { setCardOpen(value => !value); }, children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-card-heading", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Turn Rewind \u56DE\u9000\u8BBE\u7F6E" }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-card-description", children: "\u81EA\u52A8\u6587\u4EF6\u68C0\u67E5\u70B9\u3001\u4FE1\u4EFB\u7B56\u7565\u4E0E\u68C0\u67E5\u70B9\u4FDD\u7559\u4E0A\u9650\uFF1B\u56DE\u9000\u6309\u94AE\u5728\u5404\u6761\u7528\u6237\u6D88\u606F\u4E0A" })] }), (0, jsx_runtime_1.jsx)("svg", { className: "dcl-trs-card-chevron", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M4.5 6.5 8 10l3.5-3.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), cardOpen && ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-card-body", children: [(0, jsx_runtime_1.jsxs)("section", { className: "dcl-trs-section", children: [(0, jsx_runtime_1.jsx)("div", { className: "dcl-trs-section-title", children: (0, jsx_runtime_1.jsx)("strong", { children: "\u81EA\u52A8\u6587\u4EF6\u68C0\u67E5\u70B9" }) }), snapshot.status === 'loading' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: "\u6B63\u5728\u52A0\u8F7D\u8BBE\u7F6E\u2026" }), snapshot.status === 'unavailable' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: "\u5F53\u524D\u90E8\u7F72\u672A\u63D0\u4F9B\u8BBE\u7F6E\u670D\u52A1\uFF0C\u4EE5\u4E0B\u9009\u9879\u4E0D\u53EF\u7528\u3002" }), snapshot.status === 'ready' && !snapshot.writable && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: "\u8BBE\u7F6E\u4E3A\u53EA\u8BFB\uFF08\u5F53\u524D\u6D4F\u89C8\u5668\u8FDB\u7A0B\u5185\u4FDD\u5B58\uFF09\uFF0C\u4FEE\u6539\u4E0D\u53EF\u7528\u3002" }), value !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-label", children: [(0, jsx_runtime_1.jsx)("strong", { children: "\u81EA\u52A8\u6587\u4EF6\u68C0\u67E5\u70B9" }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-field-desc", children: "\u5173\u95ED\u540E\u4E0D\u518D\u4E3A\u6BCF\u6761\u6D88\u606F\u4FDD\u5B58\u6587\u4EF6\u68C0\u67E5\u70B9\uFF1B\u56DE\u9000\u5F39\u7A97\u4ECD\u53EF\u53EA\u56DE\u6EAF\u6D88\u606F" })] }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-control", children: [(0, jsx_runtime_1.jsx)("select", { value: value.turnCheckpointMode, disabled: !writable || busy !== null, onChange: (event) => { commitEnum('turnCheckpointMode', event.target.value); }, children: Object.keys(CHECKPOINT_MODE_LABELS).map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: CHECKPOINT_MODE_LABELS[option] }, option))) }), userLayer.turnCheckpointMode !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-override", children: "\u5DF2\u8986\u76D6" }), userLayer.turnCheckpointMode !== undefined && ((0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !writable || busy !== null, onClick: () => { setFormError(null); void scope?.unset('turnCheckpointMode').catch((caught) => { setFormError(messageOf(caught)); }); }, children: "\u6062\u590D\u9ED8\u8BA4" }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-label", children: [(0, jsx_runtime_1.jsx)("strong", { children: "\u68C0\u67E5\u70B9\u4FE1\u4EFB\u7B56\u7565" }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-field-desc", children: "\u5FEB\u901F\u4FE1\u4EFB Git/stat \u5143\u6570\u636E\uFF1B\u4E25\u683C\u4F1A\u9010\u4E00\u91CD\u8BFB\u6587\u4EF6\u5185\u5BB9" })] }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-control", children: [(0, jsx_runtime_1.jsx)("select", { value: value.turnCheckpointTrust, disabled: !writable || busy !== null, onChange: (event) => { commitEnum('turnCheckpointTrust', event.target.value); }, children: Object.keys(TRUST_LABELS).map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: TRUST_LABELS[option] }, option))) }), userLayer.turnCheckpointTrust !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-override", children: "\u5DF2\u8986\u76D6" }), userLayer.turnCheckpointTrust !== undefined && ((0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !writable || busy !== null, onClick: () => { setFormError(null); void scope?.unset('turnCheckpointTrust').catch((caught) => { setFormError(messageOf(caught)); }); }, children: "\u6062\u590D\u9ED8\u8BA4" }))] })] }), NUMBER_FIELDS.map((field) => ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-label", children: [(0, jsx_runtime_1.jsx)("strong", { children: field.label }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-field-desc", children: field.description })] }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-control", children: [(0, jsx_runtime_1.jsx)("input", { type: "number", min: 1, step: 1, value: drafts[field.key] ?? String(value[field.key]), disabled: !writable || busy !== null, onChange: (event) => { setDrafts((current) => ({ ...current, [field.key]: event.target.value })); }, onBlur: () => { commitNumber(field.key); }, onKeyDown: (event) => { if (event.key === 'Enter')
534
- commitNumber(field.key); } }), userLayer[field.key] !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-override", children: "\u5DF2\u8986\u76D6" }), userLayer[field.key] !== undefined && ((0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !writable || busy !== null, onClick: () => { setFormError(null); void scope?.unset(field.key).catch((caught) => { setFormError(messageOf(caught)); }); }, children: "\u6062\u590D\u9ED8\u8BA4" }))] })] }, field.key)))] })), formError !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-error", children: formError }), (0, jsx_runtime_1.jsxs)("p", { className: "dcl-trs-storage", children: ["\u5B58\u50A8\u76EE\u5F55\uFF08\u5728 cordis.patch.yml \u4E2D\u914D\u7F6E\uFF0C\u4E0D\u53EF\u5728\u7EBF\u4FEE\u6539\uFF09\uFF1A", manage?.storageDir ?? '…'] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dcl-trs-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-section-title", children: [(0, jsx_runtime_1.jsx)("strong", { children: "\u68C0\u67E5\u70B9\u7BA1\u7406" }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-section-title-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { void refreshManage(); }, disabled: manageLoading, children: manageLoading ? '正在刷新…' : '刷新' }), manage !== null && manage.workspaces.length > 0 && (confirmClearAll
535
- ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { setConfirmClearAll(false); }, disabled: busy !== null, children: "\u53D6\u6D88" }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", size: "sm", onClick: () => { void runManageAction({ action: 'clear-all' }, 'clear-all'); }, disabled: busy !== null, children: busy === 'clear-all' ? '正在清理…' : '确认清空全部' })] }))
536
- : (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { setConfirmClearAll(true); }, disabled: busy !== null, children: "\u4E00\u952E\u6E05\u7A7A\u5168\u90E8" }))] })] }), (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-manage-total", children: manage === null
537
- ? '正在读取检查点占用…'
538
- : `共 ${String(manage.workspaces.length)} 个工作区,${String(manage.totalBytes >= 0 ? manage.workspaces.reduce((total, workspace) => total + workspace.restorePoints.length, 0) : 0)} 个检查点,约 ${formatBytes(manage.totalBytes)}(Git 原生检查点的实际磁盘占用以 Git 回收为准)。` }), manageNotice !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-notice", "data-warning": manageNotice.warning, children: manageNotice.message }), manageError !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-error", children: manageError }), manage?.workspaces.map((workspace) => ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-workspace", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-workspace-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-workspace-path", title: workspace.workspace, children: workspace.workspace }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-workspace-meta", children: [String(workspace.restorePoints.length), " \u4E2A\u68C0\u67E5\u70B9 \u00B7 ", formatBytes(workspace.totalBytes)] }), workspace.recoveryCount > 0 && (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-badge", children: [String(workspace.recoveryCount), " \u4E2A\u6062\u590D\u5F85\u5904\u7406"] }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", onClick: () => { toggleWorkspace(workspace.workspace); }, children: collapsed.has(workspace.workspace) ? '展开' : '收起' }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { void runManageAction({ action: 'clear-workspace', workspace: workspace.workspace }, `clear:${workspace.workspace}`); }, disabled: busy !== null || workspace.restorePoints.length === 0, children: busy === `clear:${workspace.workspace}` ? '正在清理…' : '清空此项目' })] }), !collapsed.has(workspace.workspace) && workspace.restorePoints.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "dcl-trs-points", children: workspace.restorePoints.map((point) => ((0, jsx_runtime_1.jsxs)("li", { className: "dcl-trs-point", children: [(0, jsx_runtime_1.jsx)("time", { children: formatTime(point.createdAt) }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-point-kind", children: POINT_KIND_LABELS[point.kind] ?? point.kind }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-point-size", children: formatBytes(point.totalBytes) }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-point-files", children: [String(point.fileCount), " \u4E2A\u6587\u4EF6"] }), point.sessionId !== undefined && (0, jsx_runtime_1.jsx)("code", { children: point.sessionId }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", onClick: () => { void runManageAction({ action: 'delete', workspace: workspace.workspace, restorePointId: point.id }, `delete:${point.id}`); }, disabled: busy !== null, children: busy === `delete:${point.id}` ? '正在删除…' : '删除' })] }, point.id))) }))] }, workspace.workspace))), manage !== null && manage.workspaces.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: "\u8FD8\u6CA1\u6709\u4EFB\u4F55\u5DF2\u4FDD\u5B58\u7684\u68C0\u67E5\u70B9\u3002" })] })] }))] }));
971
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-card", "data-open": cardOpen, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "dcl-trs-card-head", "aria-expanded": cardOpen, onClick: () => { setCardOpen(value => !value); }, children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-card-heading", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.settingsCardTitle }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-card-description", children: t.settingsCardDescription })] }), (0, jsx_runtime_1.jsx)("svg", { className: "dcl-trs-card-chevron", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M4.5 6.5 8 10l3.5-3.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), cardOpen && ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-card-body", children: [(0, jsx_runtime_1.jsxs)("section", { className: "dcl-trs-section", children: [(0, jsx_runtime_1.jsx)("div", { className: "dcl-trs-section-title", children: (0, jsx_runtime_1.jsx)("strong", { children: t.autoCheckpointSection }) }), snapshot.status === 'loading' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: t.settingsLoading }), snapshot.status === 'unavailable' && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: t.settingsUnavailable }), snapshot.status === 'ready' && !snapshot.writable && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: t.settingsReadOnly }), value !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-label", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.autoCheckpointLabel }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-field-desc", children: t.autoCheckpointDescription })] }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-control", children: [(0, jsx_runtime_1.jsx)("select", { value: value.turnCheckpointMode, disabled: !writable || busy !== null, onChange: (event) => { commitEnum('turnCheckpointMode', event.target.value); }, children: CHECKPOINT_MODES.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: t.checkpointModes[option] }, option))) }), userLayer.turnCheckpointMode !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-override", children: t.overridden }), userLayer.turnCheckpointMode !== undefined && ((0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !writable || busy !== null, onClick: () => { setFormError(null); void scope?.unset('turnCheckpointMode').catch((caught) => { setFormError(messageOf(caught)); }); }, children: t.resetToDefault }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-label", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.trustLabel }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-field-desc", children: t.trustDescription })] }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-control", children: [(0, jsx_runtime_1.jsx)("select", { value: value.turnCheckpointTrust, disabled: !writable || busy !== null, onChange: (event) => { commitEnum('turnCheckpointTrust', event.target.value); }, children: TRUST_MODES.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: t.trustOptions[option] }, option))) }), userLayer.turnCheckpointTrust !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-override", children: t.overridden }), userLayer.turnCheckpointTrust !== undefined && ((0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !writable || busy !== null, onClick: () => { setFormError(null); void scope?.unset('turnCheckpointTrust').catch((caught) => { setFormError(messageOf(caught)); }); }, children: t.resetToDefault }))] })] }), NUMBER_FIELDS.map((field) => ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-label", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.numberFields[field].label }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-field-desc", children: t.numberFields[field].description })] }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-field-control", children: [(0, jsx_runtime_1.jsx)("input", { type: "number", min: 1, step: 1, value: drafts[field] ?? String(value[field]), disabled: !writable || busy !== null, onChange: (event) => { setDrafts((current) => ({ ...current, [field]: event.target.value })); }, onBlur: () => { commitNumber(field); }, onKeyDown: (event) => { if (event.key === 'Enter')
972
+ commitNumber(field); } }), userLayer[field] !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-override", children: t.overridden }), userLayer[field] !== undefined && ((0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !writable || busy !== null, onClick: () => { setFormError(null); void scope?.unset(field).catch((caught) => { setFormError(messageOf(caught)); }); }, children: t.resetToDefault }))] })] }, field)))] })), formError !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-error", children: formError }), (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-storage", children: t.storageDir(manage?.storageDir ?? '…') })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dcl-trs-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-section-title", children: [(0, jsx_runtime_1.jsx)("strong", { children: t.manageSection }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-section-title-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { void refreshManage(); }, disabled: manageLoading, children: manageLoading ? t.refreshing : t.refresh }), manage !== null && manage.workspaces.length > 0 && (confirmClearAll
973
+ ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { setConfirmClearAll(false); }, disabled: busy !== null, children: t.cancel }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", size: "sm", onClick: () => { void runManageAction({ action: 'clear-all' }, 'clear-all'); }, disabled: busy !== null, children: busy === 'clear-all' ? t.clearing : t.confirmClearAll })] }))
974
+ : (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { setConfirmClearAll(true); }, disabled: busy !== null, children: t.clearAll }))] })] }), (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-manage-total", children: manage === null
975
+ ? t.manageLoading
976
+ : t.manageTotal(manage.workspaces.length, manage.totalBytes >= 0 ? manage.workspaces.reduce((total, workspace) => total + workspace.restorePoints.length, 0) : 0, formatBytes(manage.totalBytes)) }), manageNotice !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-notice", "data-warning": manageNotice.warning, children: manageNotice.message }), manageError !== null && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-error", children: manageError }), manage?.workspaces.map((workspace) => ((0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-workspace", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dcl-trs-workspace-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-workspace-path", title: workspace.workspace, children: workspace.workspace }), (0, jsx_runtime_1.jsxs)("span", { className: "dcl-trs-workspace-meta", children: [t.workspacePoints(workspace.restorePoints.length), " \u00B7 ", formatBytes(workspace.totalBytes)] }), workspace.recoveryCount > 0 && (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-badge", children: t.pendingRecoveries(workspace.recoveryCount) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", onClick: () => { toggleWorkspace(workspace.workspace); }, children: collapsed.has(workspace.workspace) ? t.expand : t.collapse }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { void runManageAction({ action: 'clear-workspace', workspace: workspace.workspace }, `clear:${workspace.workspace}`); }, disabled: busy !== null || workspace.restorePoints.length === 0, children: busy === `clear:${workspace.workspace}` ? t.clearing : t.clearWorkspace })] }), !collapsed.has(workspace.workspace) && workspace.restorePoints.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "dcl-trs-points", children: workspace.restorePoints.map((point) => ((0, jsx_runtime_1.jsxs)("li", { className: "dcl-trs-point", children: [(0, jsx_runtime_1.jsx)("time", { children: formatTime(point.createdAt) }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-point-kind", children: t.pointKinds[point.kind] ?? point.kind }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-point-size", children: formatBytes(point.totalBytes) }), (0, jsx_runtime_1.jsx)("span", { className: "dcl-trs-point-files", children: t.fileCount(point.fileCount) }), point.sessionId !== undefined && (0, jsx_runtime_1.jsx)("code", { children: point.sessionId }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", onClick: () => { void runManageAction({ action: 'delete', workspace: workspace.workspace, restorePointId: point.id }, `delete:${point.id}`); }, disabled: busy !== null, children: busy === `delete:${point.id}` ? t.deleting : t.remove })] }, point.id))) }))] }, workspace.workspace))), manage !== null && manage.workspaces.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "dcl-trs-status", children: t.noCheckpoints })] })] }))] }));
539
977
  }
540
978
  function decodeManageOverview(value) {
541
979
  const record = recordOf(value);
542
980
  const workspacesValue = record.workspaces;
543
981
  if (!Array.isArray(workspacesValue))
544
- throw new Error('管理数据缺少 workspaces');
982
+ throw new Error(uiText().manageMissingWorkspaces);
545
983
  return {
546
984
  storageDir: requiredString(record.storageDir, 'storageDir'),
547
985
  totalBytes: requiredInteger(record.totalBytes, 'totalBytes'),
@@ -549,7 +987,7 @@ function decodeManageOverview(value) {
549
987
  const workspace = recordOf(entry);
550
988
  const pointsValue = workspace.restorePoints;
551
989
  if (!Array.isArray(pointsValue))
552
- throw new Error('管理数据缺少 restorePoints');
990
+ throw new Error(uiText().manageMissingRestorePoints);
553
991
  return {
554
992
  workspace: requiredString(workspace.workspace, 'workspace'),
555
993
  totalBytes: requiredInteger(workspace.totalBytes, 'totalBytes'),
@@ -576,7 +1014,7 @@ function decodeManageActionNotice(value) {
576
1014
  const action = requiredString(record.action, 'action');
577
1015
  if (action === 'clear-all') {
578
1016
  if (!Array.isArray(record.reports))
579
- throw new Error('清理结果缺少 reports');
1017
+ throw new Error(uiText().clearMissingReports);
580
1018
  const totals = record.reports.reduce((current, entry) => {
581
1019
  const report = recordOf(entry);
582
1020
  return {
@@ -588,14 +1026,14 @@ function decodeManageActionNotice(value) {
588
1026
  const warning = record.status === 'partial' || totals.retained > 0 || failures > 0;
589
1027
  return {
590
1028
  warning,
591
- message: `已删除 ${String(totals.deleted)} 个检查点${totals.retained > 0 ? `,${String(totals.retained)} 个受保护检查点未删除` : ''}${failures > 0 ? `,${String(failures)} 个工作区清理失败` : ''}。`,
1029
+ message: uiText().clearedAll(totals.deleted, totals.retained, failures),
592
1030
  };
593
1031
  }
594
1032
  const deleted = requiredInteger(record.deletedRestorePoints, 'deletedRestorePoints');
595
1033
  const retained = requiredInteger(record.retainedRestorePoints, 'retainedRestorePoints');
596
1034
  return {
597
1035
  warning: retained > 0,
598
- message: `已删除 ${String(deleted)} 个检查点${retained > 0 ? `,${String(retained)} 个受保护检查点未删除` : ''}。`,
1036
+ message: uiText().cleared(deleted, retained),
599
1037
  };
600
1038
  }
601
1039
  /** Format one byte count with human-friendly units. */
@@ -625,17 +1063,17 @@ function decodePreview(value) {
625
1063
  if (status === 'failed')
626
1064
  return { status, error: requiredString(record.error, 'error') };
627
1065
  if (status !== 'ready')
628
- throw new Error(`未知回退状态:${status}`);
1066
+ throw new Error(uiText().unknownStatus(status));
629
1067
  const changesValue = record.changes;
630
1068
  if (!Array.isArray(changesValue))
631
- throw new Error('回退预览缺少 changes');
1069
+ throw new Error(uiText().previewMissingChanges);
632
1070
  const changes = changesValue.map((entry) => {
633
1071
  const change = recordOf(entry);
634
1072
  return { path: requiredString(change.path, 'path'), kind: requiredString(change.kind, 'kind') };
635
1073
  });
636
1074
  const activeSessionIdsValue = record.activeSessionIds;
637
1075
  if (!Array.isArray(activeSessionIdsValue) || !activeSessionIdsValue.every(value => typeof value === 'string')) {
638
- throw new Error('回退预览缺少 activeSessionIds');
1076
+ throw new Error(uiText().previewMissingActiveSessionIds);
639
1077
  }
640
1078
  return {
641
1079
  status,
@@ -747,16 +1185,23 @@ function samePortalTargets(left, right) {
747
1185
  });
748
1186
  }
749
1187
  async function openSessionWithDraft(ctx, sessionId, promptText) {
750
- let lastError = new Error('新对话还没有准备好');
1188
+ if (typeof ctx.sessions.open === 'function')
1189
+ ctx.sessions.open(sessionId);
1190
+ else {
1191
+ const navigation = ctx.get?.('uiWorkspace');
1192
+ if (navigation === undefined)
1193
+ throw new Error(uiText().sessionNotReady);
1194
+ navigation.openSession(sessionId);
1195
+ }
1196
+ let lastError = new Error(uiText().sessionNotReady);
751
1197
  for (let attempt = 0; attempt < 20; attempt += 1) {
752
1198
  try {
753
- ctx.sessions.open(sessionId);
754
1199
  const scope = ctx.sessions.scope(sessionId);
755
1200
  if (scope !== undefined) {
756
1201
  ctx.conversation.input.for(scope).setDraft(promptText);
757
1202
  return;
758
1203
  }
759
- lastError = new Error('新对话还没有准备好');
1204
+ lastError = new Error(uiText().sessionNotReady);
760
1205
  }
761
1206
  catch (error) {
762
1207
  lastError = error;
@@ -781,7 +1226,7 @@ async function responseJson(response) {
781
1226
  value = text === '' ? undefined : JSON.parse(text);
782
1227
  }
783
1228
  catch {
784
- throw new RewindRequestError(response.ok ? 'REWIND_INVALID_RESPONSE' : 'REWIND_ENDPOINT_UNAVAILABLE', `回退服务返回了无法解析的内容(HTTP ${String(response.status)})。`);
1229
+ throw new RewindRequestError(response.ok ? 'REWIND_INVALID_RESPONSE' : 'REWIND_ENDPOINT_UNAVAILABLE', uiText().unparsableResponse(String(response.status)));
785
1230
  }
786
1231
  const record = value !== null && typeof value === 'object' && !Array.isArray(value)
787
1232
  ? value
@@ -789,10 +1234,10 @@ async function responseJson(response) {
789
1234
  if (!response.ok) {
790
1235
  throw new RewindRequestError(typeof record?.code === 'string'
791
1236
  ? record.code
792
- : response.status === 404 ? 'REWIND_ENDPOINT_UNAVAILABLE' : 'REWIND_FAILED', typeof record?.error === 'string' ? record.error : `请求失败:${String(response.status)}`);
1237
+ : response.status === 404 ? 'REWIND_ENDPOINT_UNAVAILABLE' : 'REWIND_FAILED', typeof record?.error === 'string' ? record.error : uiText().requestFailed(String(response.status)));
793
1238
  }
794
1239
  if (value === undefined) {
795
- throw new RewindRequestError('REWIND_INVALID_RESPONSE', '回退服务返回了空响应。');
1240
+ throw new RewindRequestError('REWIND_INVALID_RESPONSE', uiText().emptyResponse);
796
1241
  }
797
1242
  return value;
798
1243
  }
@@ -805,22 +1250,22 @@ class RewindRequestError extends Error {
805
1250
  }
806
1251
  function recordOf(value) {
807
1252
  if (value === null || typeof value !== 'object' || Array.isArray(value))
808
- throw new Error('服务器返回了无效对象');
1253
+ throw new Error(uiText().invalidObject);
809
1254
  return value;
810
1255
  }
811
1256
  function requiredString(value, name) {
812
1257
  if (typeof value !== 'string' || value === '')
813
- throw new Error(`${name} 无效`);
1258
+ throw new Error(uiText().invalidField(name));
814
1259
  return value;
815
1260
  }
816
1261
  function requiredInteger(value, name) {
817
1262
  if (!Number.isSafeInteger(value) || value < 0)
818
- throw new Error(`${name} 无效`);
1263
+ throw new Error(uiText().invalidField(name));
819
1264
  return value;
820
1265
  }
821
1266
  function requiredBoolean(value, name) {
822
1267
  if (typeof value !== 'boolean')
823
- throw new Error(`${name} 无效`);
1268
+ throw new Error(uiText().invalidField(name));
824
1269
  return value;
825
1270
  }
826
1271
  function optionalRecordString(record, name) {
@@ -831,13 +1276,7 @@ function optionalRecordString(record, name) {
831
1276
  }
832
1277
  /** Describe the user-visible result of restoring one changed file. */
833
1278
  function fileRecoveryLabel(kind) {
834
- switch (kind) {
835
- case 'added': return '移除后来新增的文件';
836
- case 'deleted': return '找回文件';
837
- case 'modified': return '恢复之前的版本';
838
- case 'mode-changed': return '恢复文件权限';
839
- case 'type-changed': return '恢复之前的文件类型';
840
- }
1279
+ return uiText().fileKinds[kind];
841
1280
  }
842
1281
  function RewindIcon({ size }) {
843
1282
  return ((0, jsx_runtime_1.jsx)("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M6.35 3.25 2.75 7l3.6 3.75M3.1 7h5.15a4.25 4.25 0 0 1 4.25 4.25v1.25", stroke: "currentColor", strokeWidth: "1.45", strokeLinecap: "round", strokeLinejoin: "round" }) }));
@@ -845,25 +1284,7 @@ function RewindIcon({ size }) {
845
1284
  function friendlyError(error) {
846
1285
  if (!(error instanceof RewindRequestError))
847
1286
  return messageOf(error);
848
- switch (error.code) {
849
- case 'REWIND_ENDPOINT_UNAVAILABLE': return '回退服务没有响应。请确认 @anionex/dsh-turn-rewind 已挂载到当前 DSH,并重启 DSH 后重试。';
850
- case 'REWIND_INVALID_RESPONSE': return '回退服务返回了无法解析的内容。请重启 DSH 后重试。';
851
- case 'PLAN_STALE': return '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。';
852
- case 'PLAN_STALE_WORKSPACE': return '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。';
853
- case 'PLAN_STALE_REPOSITORY': return 'Git 状态在检查后又发生了变化,恢复已失效。请重新检查后再试。';
854
- case 'WORKSPACE_IN_USE': return '这个项目目录还有别的对话正在运行。请等那些对话结束或停止后,再重新检查。';
855
- case 'WORKSPACE_LOCKED': return '另一个恢复操作正在处理这个项目目录。请等待它完成后重新检查。';
856
- case 'WORKSPACE_CHANGED': return '这个项目目录已经不在原来的位置,旧的回退点无法再使用。';
857
- case 'WORKSPACE_MODE_CHANGED': return '这个项目目录的工作区类型变了(例如从普通目录变成了 Git 仓库)。旧的回退点不再适用,请重新发送一条消息生成新的回退点。';
858
- case 'HEAD_CHANGED': return '项目的提交或分支已发生变化。为避免覆盖新改动,请重新检查后再恢复。';
859
- case 'REPOSITORY_CHANGED': return '这个项目目录已不属于原来的 Git 工作区,无法恢复。';
860
- case 'GIT_OPERATION_CHANGED': return 'Git 正在执行其他操作。请先完成或取消该操作,再重新检查。';
861
- case 'RESTORE_POINT_NOT_FOUND': return '没有找到对应的文件状态,可能已被清理。';
862
- case 'NO_CHANGES': return '项目文件已经是这条消息发送前的状态,无需恢复文件。可选择「只回溯消息」重新开始这段对话。';
863
- case 'RESTORE_FAILED_ROLLED_BACK': return '恢复未能完成,项目文件已自动还原到操作前的状态。';
864
- case 'CONVERSATION_REWIND_FAILED': return '文件已恢复,但无法创建新对话;项目文件已自动还原。';
865
- default: return error.message;
866
- }
1287
+ return uiText().errors[error.code] ?? error.message;
867
1288
  }
868
1289
  /**
869
1290
  * Describe what a checkpoint could not store, in one user-facing sentence.
@@ -877,15 +1298,12 @@ function describeCaptureNotice(ready) {
877
1298
  if (ready === null)
878
1299
  return null;
879
1300
  const example = (ready.skipped ?? [])[0]?.path;
880
- if (ready.captureTruncated === 'snapshot-limit') {
881
- return '本轮要保存的文件总量超过上限,剩下的文件没有纳入检查点。恢复时不会改动它们。';
882
- }
883
- if (ready.captureTruncated === 'file-limit') {
884
- return '本轮文件数量超过上限,多出来的文件没有纳入检查点。恢复时不会改动它们。';
885
- }
886
- if (ready.skippedCount > 0) {
887
- return `有 ${String(ready.skippedCount)} 个文件因为超过单文件大小上限或类型不受支持,没有纳入检查点${example === undefined ? '' : `(例如 ${example})`}。恢复时不会改动它们。`;
888
- }
1301
+ if (ready.captureTruncated === 'snapshot-limit')
1302
+ return uiText().captureSnapshotLimit;
1303
+ if (ready.captureTruncated === 'file-limit')
1304
+ return uiText().captureFileLimit;
1305
+ if (ready.skippedCount > 0)
1306
+ return uiText().captureSkipped(ready.skippedCount, example);
889
1307
  return null;
890
1308
  }
891
1309
  /**
@@ -899,16 +1317,14 @@ function describeCaptureNotice(ready) {
899
1317
  */
900
1318
  function explainCheckpointSkip(reason) {
901
1319
  const code = /^\[([A-Z_]+)\]/.exec(reason)?.[1];
902
- if (code === 'TURN_CHECKPOINT_TIMEOUT') {
903
- return '这个项目目录太大,在检查点时间上限内没保存完文件快照,所以本轮没有回退点(消息本身没有受影响)。可以在插件设置里调大「检查点时间上限」,或在目录根用 .dsh-rewindignore 排除大目录(例如 node_modules、构建产物、数据集)。仍可只回溯消息。';
904
- }
905
- if (code === 'TURN_CHECKPOINT_DISABLED') {
906
- return '自动文件检查点已在设置里关闭,本轮没有回退点。仍可只回溯消息。';
907
- }
908
- if (code === 'TURN_CHECKPOINT_NEW_CONTENT_LIMIT') {
909
- return '这一轮新增的内容超过检查点预算,没有保存文件快照。仍可只回溯消息。';
910
- }
911
- return `本轮没有保存文件检查点:${reason}仍可只回溯消息。`;
1320
+ const t = uiText();
1321
+ if (code === 'TURN_CHECKPOINT_TIMEOUT')
1322
+ return t.skipTimeout;
1323
+ if (code === 'TURN_CHECKPOINT_DISABLED')
1324
+ return t.skipDisabled;
1325
+ if (code === 'TURN_CHECKPOINT_NEW_CONTENT_LIMIT')
1326
+ return t.skipNewContentLimit;
1327
+ return t.skipOther(reason);
912
1328
  }
913
1329
  /**
914
1330
  * Explain one recorded checkpoint failure in user terms.
@@ -921,23 +1337,22 @@ function explainCheckpointSkip(reason) {
921
1337
  */
922
1338
  function explainCheckpointFailure(message) {
923
1339
  const code = /^\[([A-Z_]+)\]/.exec(message)?.[1];
1340
+ const t = uiText();
924
1341
  switch (code) {
925
1342
  case 'GIT_COMMAND_FAILED':
926
- return /not a git repository/i.test(message)
927
- ? '这个项目目录不是 Git 仓库,回退功能无法保存文件检查点。仍可只回溯消息。'
928
- : `无法读取这个项目目录的 Git 状态:${message}仍可只回溯消息。`;
1343
+ return /not a git repository/i.test(message) ? t.failureNotGitRepository : t.failureGitStatus(message);
929
1344
  case 'FILE_TOO_LARGE':
930
1345
  case 'SNAPSHOT_TOO_LARGE':
931
1346
  case 'TOO_MANY_FILES':
932
- return '本轮有文件超过检查点的大小或数量上限,没有保存文件检查点。仍可只回溯消息。';
1347
+ return t.failureSizeOrCount;
933
1348
  case 'UNSUPPORTED_FILE_TYPE':
934
- return '项目目录里有无法保存的特殊文件(如 socket、设备文件、FIFO),没有保存文件检查点。仍可只回溯消息。';
1349
+ return t.failureUnsupportedType;
935
1350
  case 'IGNORE_FILE_INVALID':
936
- return '项目目录里的 .dsh-rewindignore 内容无效,没有保存文件检查点。修正该文件后可重新发送消息。';
1351
+ return t.failureIgnoreFileInvalid;
937
1352
  case 'INVALID_PATH':
938
- return '项目目录里有无法安全保存的路径(例如嵌套的独立 Git 仓库),没有保存文件检查点。仍可只回溯消息。';
1353
+ return t.failureInvalidPath;
939
1354
  default:
940
- return `没能保存这条消息发送前的文件:${message}仍可只回溯消息。`;
1355
+ return t.failureOther(message);
941
1356
  }
942
1357
  }
943
1358
  function messageOf(error) {