@anionex/dsh-turn-rewind 0.3.8 → 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/CHANGELOG.md +13 -0
- package/README.md +12 -0
- package/README.zh.md +12 -0
- package/lib/client.js +534 -120
- package/lib/client.js.map +1 -1
- package/lib/index.js +16 -3
- package/lib/rewind-host.js +5 -1
- package/lib/settings.js +22 -13
- package/lib/types/client/index.d.ts +38 -9
- package/lib/types/index.d.ts +2 -0
- package/lib/types/settings.d.ts +2 -2
- package/package.json +13 -5
- package/src/client/index.tsx +741 -188
- package/src/index.ts +17 -3
- package/src/rewind-host.ts +7 -1
- package/src/settings.ts +23 -14
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
|
-
*
|
|
107
|
-
*
|
|
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'
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
843
|
+
setError(t.restoredButOpenFailed(messageOf(navigationError)));
|
|
389
844
|
}
|
|
390
845
|
}
|
|
391
846
|
catch (caught) {
|
|
@@ -400,46 +855,28 @@ function RewindMessageAction({ matched, sessionId, openRestoredSession }) {
|
|
|
400
855
|
}
|
|
401
856
|
};
|
|
402
857
|
const captureNotice = describeCaptureNotice(ready);
|
|
403
|
-
const actionLabel = mode === 'both' ?
|
|
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:
|
|
407
|
-
? (0, jsx_runtime_1.jsx)("strong", { children:
|
|
408
|
-
: (0, jsx_runtime_1.
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
legacy: '完整快照(兼容模式)',
|
|
431
|
-
};
|
|
432
|
-
const TRUST_LABELS = {
|
|
433
|
-
fast: '快速',
|
|
434
|
-
strict: '严格',
|
|
435
|
-
};
|
|
436
|
-
const POINT_KIND_LABELS = {
|
|
437
|
-
user: '手动',
|
|
438
|
-
rescue: '救援',
|
|
439
|
-
turn: '轮次',
|
|
440
|
-
};
|
|
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'];
|
|
441
877
|
/** Settings card for the `turn-rewind` namespace: runtime options plus checkpoint management. */
|
|
442
878
|
function TurnRewindSettingsCard({ scope }) {
|
|
879
|
+
const t = useText();
|
|
443
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]));
|
|
444
881
|
const [manage, setManage] = (0, react_1.useState)(null);
|
|
445
882
|
const [manageLoading, setManageLoading] = (0, react_1.useState)(false);
|
|
@@ -490,7 +927,7 @@ function TurnRewindSettingsCard({ scope }) {
|
|
|
490
927
|
}
|
|
491
928
|
const parsed = Number.parseInt(draft, 10);
|
|
492
929
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
493
|
-
setFormError(
|
|
930
|
+
setFormError(t.positiveInteger(field));
|
|
494
931
|
return;
|
|
495
932
|
}
|
|
496
933
|
if (parsed === value?.[field])
|
|
@@ -531,18 +968,18 @@ function TurnRewindSettingsCard({ scope }) {
|
|
|
531
968
|
return next;
|
|
532
969
|
});
|
|
533
970
|
};
|
|
534
|
-
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:
|
|
535
|
-
commitNumber(field
|
|
536
|
-
? ((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:
|
|
537
|
-
: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", onClick: () => { setConfirmClearAll(true); }, disabled: busy !== null, children:
|
|
538
|
-
?
|
|
539
|
-
:
|
|
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 })] })] }))] }));
|
|
540
977
|
}
|
|
541
978
|
function decodeManageOverview(value) {
|
|
542
979
|
const record = recordOf(value);
|
|
543
980
|
const workspacesValue = record.workspaces;
|
|
544
981
|
if (!Array.isArray(workspacesValue))
|
|
545
|
-
throw new Error(
|
|
982
|
+
throw new Error(uiText().manageMissingWorkspaces);
|
|
546
983
|
return {
|
|
547
984
|
storageDir: requiredString(record.storageDir, 'storageDir'),
|
|
548
985
|
totalBytes: requiredInteger(record.totalBytes, 'totalBytes'),
|
|
@@ -550,7 +987,7 @@ function decodeManageOverview(value) {
|
|
|
550
987
|
const workspace = recordOf(entry);
|
|
551
988
|
const pointsValue = workspace.restorePoints;
|
|
552
989
|
if (!Array.isArray(pointsValue))
|
|
553
|
-
throw new Error(
|
|
990
|
+
throw new Error(uiText().manageMissingRestorePoints);
|
|
554
991
|
return {
|
|
555
992
|
workspace: requiredString(workspace.workspace, 'workspace'),
|
|
556
993
|
totalBytes: requiredInteger(workspace.totalBytes, 'totalBytes'),
|
|
@@ -577,7 +1014,7 @@ function decodeManageActionNotice(value) {
|
|
|
577
1014
|
const action = requiredString(record.action, 'action');
|
|
578
1015
|
if (action === 'clear-all') {
|
|
579
1016
|
if (!Array.isArray(record.reports))
|
|
580
|
-
throw new Error(
|
|
1017
|
+
throw new Error(uiText().clearMissingReports);
|
|
581
1018
|
const totals = record.reports.reduce((current, entry) => {
|
|
582
1019
|
const report = recordOf(entry);
|
|
583
1020
|
return {
|
|
@@ -589,14 +1026,14 @@ function decodeManageActionNotice(value) {
|
|
|
589
1026
|
const warning = record.status === 'partial' || totals.retained > 0 || failures > 0;
|
|
590
1027
|
return {
|
|
591
1028
|
warning,
|
|
592
|
-
message:
|
|
1029
|
+
message: uiText().clearedAll(totals.deleted, totals.retained, failures),
|
|
593
1030
|
};
|
|
594
1031
|
}
|
|
595
1032
|
const deleted = requiredInteger(record.deletedRestorePoints, 'deletedRestorePoints');
|
|
596
1033
|
const retained = requiredInteger(record.retainedRestorePoints, 'retainedRestorePoints');
|
|
597
1034
|
return {
|
|
598
1035
|
warning: retained > 0,
|
|
599
|
-
message:
|
|
1036
|
+
message: uiText().cleared(deleted, retained),
|
|
600
1037
|
};
|
|
601
1038
|
}
|
|
602
1039
|
/** Format one byte count with human-friendly units. */
|
|
@@ -626,17 +1063,17 @@ function decodePreview(value) {
|
|
|
626
1063
|
if (status === 'failed')
|
|
627
1064
|
return { status, error: requiredString(record.error, 'error') };
|
|
628
1065
|
if (status !== 'ready')
|
|
629
|
-
throw new Error(
|
|
1066
|
+
throw new Error(uiText().unknownStatus(status));
|
|
630
1067
|
const changesValue = record.changes;
|
|
631
1068
|
if (!Array.isArray(changesValue))
|
|
632
|
-
throw new Error(
|
|
1069
|
+
throw new Error(uiText().previewMissingChanges);
|
|
633
1070
|
const changes = changesValue.map((entry) => {
|
|
634
1071
|
const change = recordOf(entry);
|
|
635
1072
|
return { path: requiredString(change.path, 'path'), kind: requiredString(change.kind, 'kind') };
|
|
636
1073
|
});
|
|
637
1074
|
const activeSessionIdsValue = record.activeSessionIds;
|
|
638
1075
|
if (!Array.isArray(activeSessionIdsValue) || !activeSessionIdsValue.every(value => typeof value === 'string')) {
|
|
639
|
-
throw new Error(
|
|
1076
|
+
throw new Error(uiText().previewMissingActiveSessionIds);
|
|
640
1077
|
}
|
|
641
1078
|
return {
|
|
642
1079
|
status,
|
|
@@ -748,16 +1185,23 @@ function samePortalTargets(left, right) {
|
|
|
748
1185
|
});
|
|
749
1186
|
}
|
|
750
1187
|
async function openSessionWithDraft(ctx, sessionId, promptText) {
|
|
751
|
-
|
|
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);
|
|
752
1197
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
753
1198
|
try {
|
|
754
|
-
ctx.sessions.open(sessionId);
|
|
755
1199
|
const scope = ctx.sessions.scope(sessionId);
|
|
756
1200
|
if (scope !== undefined) {
|
|
757
1201
|
ctx.conversation.input.for(scope).setDraft(promptText);
|
|
758
1202
|
return;
|
|
759
1203
|
}
|
|
760
|
-
lastError = new Error(
|
|
1204
|
+
lastError = new Error(uiText().sessionNotReady);
|
|
761
1205
|
}
|
|
762
1206
|
catch (error) {
|
|
763
1207
|
lastError = error;
|
|
@@ -782,7 +1226,7 @@ async function responseJson(response) {
|
|
|
782
1226
|
value = text === '' ? undefined : JSON.parse(text);
|
|
783
1227
|
}
|
|
784
1228
|
catch {
|
|
785
|
-
throw new RewindRequestError(response.ok ? 'REWIND_INVALID_RESPONSE' : 'REWIND_ENDPOINT_UNAVAILABLE',
|
|
1229
|
+
throw new RewindRequestError(response.ok ? 'REWIND_INVALID_RESPONSE' : 'REWIND_ENDPOINT_UNAVAILABLE', uiText().unparsableResponse(String(response.status)));
|
|
786
1230
|
}
|
|
787
1231
|
const record = value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
788
1232
|
? value
|
|
@@ -790,10 +1234,10 @@ async function responseJson(response) {
|
|
|
790
1234
|
if (!response.ok) {
|
|
791
1235
|
throw new RewindRequestError(typeof record?.code === 'string'
|
|
792
1236
|
? record.code
|
|
793
|
-
: response.status === 404 ? 'REWIND_ENDPOINT_UNAVAILABLE' : 'REWIND_FAILED', typeof record?.error === 'string' ? record.error :
|
|
1237
|
+
: response.status === 404 ? 'REWIND_ENDPOINT_UNAVAILABLE' : 'REWIND_FAILED', typeof record?.error === 'string' ? record.error : uiText().requestFailed(String(response.status)));
|
|
794
1238
|
}
|
|
795
1239
|
if (value === undefined) {
|
|
796
|
-
throw new RewindRequestError('REWIND_INVALID_RESPONSE',
|
|
1240
|
+
throw new RewindRequestError('REWIND_INVALID_RESPONSE', uiText().emptyResponse);
|
|
797
1241
|
}
|
|
798
1242
|
return value;
|
|
799
1243
|
}
|
|
@@ -806,22 +1250,22 @@ class RewindRequestError extends Error {
|
|
|
806
1250
|
}
|
|
807
1251
|
function recordOf(value) {
|
|
808
1252
|
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
809
|
-
throw new Error(
|
|
1253
|
+
throw new Error(uiText().invalidObject);
|
|
810
1254
|
return value;
|
|
811
1255
|
}
|
|
812
1256
|
function requiredString(value, name) {
|
|
813
1257
|
if (typeof value !== 'string' || value === '')
|
|
814
|
-
throw new Error(
|
|
1258
|
+
throw new Error(uiText().invalidField(name));
|
|
815
1259
|
return value;
|
|
816
1260
|
}
|
|
817
1261
|
function requiredInteger(value, name) {
|
|
818
1262
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
819
|
-
throw new Error(
|
|
1263
|
+
throw new Error(uiText().invalidField(name));
|
|
820
1264
|
return value;
|
|
821
1265
|
}
|
|
822
1266
|
function requiredBoolean(value, name) {
|
|
823
1267
|
if (typeof value !== 'boolean')
|
|
824
|
-
throw new Error(
|
|
1268
|
+
throw new Error(uiText().invalidField(name));
|
|
825
1269
|
return value;
|
|
826
1270
|
}
|
|
827
1271
|
function optionalRecordString(record, name) {
|
|
@@ -832,13 +1276,7 @@ function optionalRecordString(record, name) {
|
|
|
832
1276
|
}
|
|
833
1277
|
/** Describe the user-visible result of restoring one changed file. */
|
|
834
1278
|
function fileRecoveryLabel(kind) {
|
|
835
|
-
|
|
836
|
-
case 'added': return '移除后来新增的文件';
|
|
837
|
-
case 'deleted': return '找回文件';
|
|
838
|
-
case 'modified': return '恢复之前的版本';
|
|
839
|
-
case 'mode-changed': return '恢复文件权限';
|
|
840
|
-
case 'type-changed': return '恢复之前的文件类型';
|
|
841
|
-
}
|
|
1279
|
+
return uiText().fileKinds[kind];
|
|
842
1280
|
}
|
|
843
1281
|
function RewindIcon({ size }) {
|
|
844
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" }) }));
|
|
@@ -846,25 +1284,7 @@ function RewindIcon({ size }) {
|
|
|
846
1284
|
function friendlyError(error) {
|
|
847
1285
|
if (!(error instanceof RewindRequestError))
|
|
848
1286
|
return messageOf(error);
|
|
849
|
-
|
|
850
|
-
case 'REWIND_ENDPOINT_UNAVAILABLE': return '回退服务没有响应。请确认 @anionex/dsh-turn-rewind 已挂载到当前 DSH,并重启 DSH 后重试。';
|
|
851
|
-
case 'REWIND_INVALID_RESPONSE': return '回退服务返回了无法解析的内容。请重启 DSH 后重试。';
|
|
852
|
-
case 'PLAN_STALE': return '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。';
|
|
853
|
-
case 'PLAN_STALE_WORKSPACE': return '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。';
|
|
854
|
-
case 'PLAN_STALE_REPOSITORY': return 'Git 状态在检查后又发生了变化,恢复已失效。请重新检查后再试。';
|
|
855
|
-
case 'WORKSPACE_IN_USE': return '这个项目目录还有别的对话正在运行。请等那些对话结束或停止后,再重新检查。';
|
|
856
|
-
case 'WORKSPACE_LOCKED': return '另一个恢复操作正在处理这个项目目录。请等待它完成后重新检查。';
|
|
857
|
-
case 'WORKSPACE_CHANGED': return '这个项目目录已经不在原来的位置,旧的回退点无法再使用。';
|
|
858
|
-
case 'WORKSPACE_MODE_CHANGED': return '这个项目目录的工作区类型变了(例如从普通目录变成了 Git 仓库)。旧的回退点不再适用,请重新发送一条消息生成新的回退点。';
|
|
859
|
-
case 'HEAD_CHANGED': return '项目的提交或分支已发生变化。为避免覆盖新改动,请重新检查后再恢复。';
|
|
860
|
-
case 'REPOSITORY_CHANGED': return '这个项目目录已不属于原来的 Git 工作区,无法恢复。';
|
|
861
|
-
case 'GIT_OPERATION_CHANGED': return 'Git 正在执行其他操作。请先完成或取消该操作,再重新检查。';
|
|
862
|
-
case 'RESTORE_POINT_NOT_FOUND': return '没有找到对应的文件状态,可能已被清理。';
|
|
863
|
-
case 'NO_CHANGES': return '项目文件已经是这条消息发送前的状态,无需恢复文件。可选择「只回溯消息」重新开始这段对话。';
|
|
864
|
-
case 'RESTORE_FAILED_ROLLED_BACK': return '恢复未能完成,项目文件已自动还原到操作前的状态。';
|
|
865
|
-
case 'CONVERSATION_REWIND_FAILED': return '文件已恢复,但无法创建新对话;项目文件已自动还原。';
|
|
866
|
-
default: return error.message;
|
|
867
|
-
}
|
|
1287
|
+
return uiText().errors[error.code] ?? error.message;
|
|
868
1288
|
}
|
|
869
1289
|
/**
|
|
870
1290
|
* Describe what a checkpoint could not store, in one user-facing sentence.
|
|
@@ -878,15 +1298,12 @@ function describeCaptureNotice(ready) {
|
|
|
878
1298
|
if (ready === null)
|
|
879
1299
|
return null;
|
|
880
1300
|
const example = (ready.skipped ?? [])[0]?.path;
|
|
881
|
-
if (ready.captureTruncated === 'snapshot-limit')
|
|
882
|
-
return
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if (ready.skippedCount > 0) {
|
|
888
|
-
return `有 ${String(ready.skippedCount)} 个文件因为超过单文件大小上限或类型不受支持,没有纳入检查点${example === undefined ? '' : `(例如 ${example})`}。恢复时不会改动它们。`;
|
|
889
|
-
}
|
|
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);
|
|
890
1307
|
return null;
|
|
891
1308
|
}
|
|
892
1309
|
/**
|
|
@@ -900,16 +1317,14 @@ function describeCaptureNotice(ready) {
|
|
|
900
1317
|
*/
|
|
901
1318
|
function explainCheckpointSkip(reason) {
|
|
902
1319
|
const code = /^\[([A-Z_]+)\]/.exec(reason)?.[1];
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (code === 'TURN_CHECKPOINT_DISABLED')
|
|
907
|
-
return
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
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);
|
|
913
1328
|
}
|
|
914
1329
|
/**
|
|
915
1330
|
* Explain one recorded checkpoint failure in user terms.
|
|
@@ -922,23 +1337,22 @@ function explainCheckpointSkip(reason) {
|
|
|
922
1337
|
*/
|
|
923
1338
|
function explainCheckpointFailure(message) {
|
|
924
1339
|
const code = /^\[([A-Z_]+)\]/.exec(message)?.[1];
|
|
1340
|
+
const t = uiText();
|
|
925
1341
|
switch (code) {
|
|
926
1342
|
case 'GIT_COMMAND_FAILED':
|
|
927
|
-
return /not a git repository/i.test(message)
|
|
928
|
-
? '这个项目目录不是 Git 仓库,回退功能无法保存文件检查点。仍可只回溯消息。'
|
|
929
|
-
: `无法读取这个项目目录的 Git 状态:${message}仍可只回溯消息。`;
|
|
1343
|
+
return /not a git repository/i.test(message) ? t.failureNotGitRepository : t.failureGitStatus(message);
|
|
930
1344
|
case 'FILE_TOO_LARGE':
|
|
931
1345
|
case 'SNAPSHOT_TOO_LARGE':
|
|
932
1346
|
case 'TOO_MANY_FILES':
|
|
933
|
-
return
|
|
1347
|
+
return t.failureSizeOrCount;
|
|
934
1348
|
case 'UNSUPPORTED_FILE_TYPE':
|
|
935
|
-
return
|
|
1349
|
+
return t.failureUnsupportedType;
|
|
936
1350
|
case 'IGNORE_FILE_INVALID':
|
|
937
|
-
return
|
|
1351
|
+
return t.failureIgnoreFileInvalid;
|
|
938
1352
|
case 'INVALID_PATH':
|
|
939
|
-
return
|
|
1353
|
+
return t.failureInvalidPath;
|
|
940
1354
|
default:
|
|
941
|
-
return
|
|
1355
|
+
return t.failureOther(message);
|
|
942
1356
|
}
|
|
943
1357
|
}
|
|
944
1358
|
function messageOf(error) {
|