@anionex/dsh-turn-rewind 0.3.8 → 0.3.10
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 +18 -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 +14 -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/src/client/index.tsx
CHANGED
|
@@ -78,6 +78,7 @@ interface SlotsLike {
|
|
|
78
78
|
readonly id?: string
|
|
79
79
|
readonly key?: string
|
|
80
80
|
readonly order?: number
|
|
81
|
+
readonly label?: string | (() => string)
|
|
81
82
|
readonly locale?: string
|
|
82
83
|
readonly inject?: () => I
|
|
83
84
|
},
|
|
@@ -85,10 +86,21 @@ interface SlotsLike {
|
|
|
85
86
|
): () => void
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
/** Immutable locale state published by `@deepseek-ai/dsh-client-locale`. */
|
|
90
|
+
interface LocaleSnapshotLike {
|
|
91
|
+
/** Active locale id, a BCP 47-style tag such as `zh` or `en`. */
|
|
92
|
+
readonly active: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The part of the Host locale service this plugin reads. */
|
|
96
|
+
interface LocaleRuntimeLike {
|
|
97
|
+
getSnapshot(): LocaleSnapshotLike
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
interface ClientContextLike {
|
|
89
101
|
readonly slots: SlotsLike
|
|
90
102
|
readonly sessions: {
|
|
91
|
-
open(sessionId: string): void
|
|
103
|
+
open?(sessionId: string): void
|
|
92
104
|
scope(sessionId: string): unknown | undefined
|
|
93
105
|
}
|
|
94
106
|
readonly conversation: {
|
|
@@ -96,10 +108,16 @@ interface ClientContextLike {
|
|
|
96
108
|
for(scope: unknown): { setDraft(text: string): void }
|
|
97
109
|
}
|
|
98
110
|
}
|
|
99
|
-
|
|
100
|
-
bind<T>(spec: { readonly namespace: string }): SettingsScopeLike<T>
|
|
101
|
-
}
|
|
111
|
+
get?(name: string): unknown
|
|
102
112
|
effect(setup: () => (() => void), label?: string): unknown
|
|
113
|
+
/**
|
|
114
|
+
* Cordis's un-injected service read: the service value, or `undefined` when
|
|
115
|
+
* the profile mounts none. Optional because older clients and the test
|
|
116
|
+
* doubles expose no such method.
|
|
117
|
+
*/
|
|
118
|
+
get?(name: 'locale'): LocaleRuntimeLike | undefined
|
|
119
|
+
/** Cordis's event subscription; the result is a disposer when it is one. */
|
|
120
|
+
on?(name: 'locale/change', listener: (snapshot: LocaleSnapshotLike) => void): unknown
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
type RewindMode = 'both' | 'code' | 'messages'
|
|
@@ -156,7 +174,7 @@ export interface TurnRewindSettingsValue {
|
|
|
156
174
|
readonly turnCheckpointTrust: 'fast' | 'strict'
|
|
157
175
|
}
|
|
158
176
|
|
|
159
|
-
/** Browser
|
|
177
|
+
/** Browser settings transport shared by the old namespace and new Config form. */
|
|
160
178
|
export interface SettingsScopeLike<T> {
|
|
161
179
|
getSnapshot(): SettingsScopeSnapshotLike<T>
|
|
162
180
|
subscribe(listener: () => void): () => void
|
|
@@ -271,6 +289,569 @@ const styles = `
|
|
|
271
289
|
.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}
|
|
272
290
|
`
|
|
273
291
|
|
|
292
|
+
/** UI languages this plugin ships copy for. */
|
|
293
|
+
type UiLocale = 'zh' | 'en'
|
|
294
|
+
|
|
295
|
+
/** Label and help text for one numeric settings field. */
|
|
296
|
+
interface FieldText {
|
|
297
|
+
readonly label: string
|
|
298
|
+
readonly description: string
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Every user-facing string the rewind surface renders, in one language. */
|
|
302
|
+
interface RewindText {
|
|
303
|
+
readonly rewindTooltip: string
|
|
304
|
+
readonly dialogClose: string
|
|
305
|
+
readonly dialogDescription: string
|
|
306
|
+
readonly cancel: string
|
|
307
|
+
readonly applying: string
|
|
308
|
+
readonly done: string
|
|
309
|
+
readonly actionRestoreAndRestart: string
|
|
310
|
+
readonly actionRestoreFiles: string
|
|
311
|
+
readonly actionMessagesOnly: string
|
|
312
|
+
readonly checkingFiles: string
|
|
313
|
+
readonly previewPending: string
|
|
314
|
+
readonly previewMissing: string
|
|
315
|
+
readonly modeBothTitle: string
|
|
316
|
+
readonly modeBothDescription: string
|
|
317
|
+
readonly modeCodeTitle: string
|
|
318
|
+
readonly modeCodeDescription: string
|
|
319
|
+
readonly modeMessagesTitle: string
|
|
320
|
+
readonly modeMessagesDescription: string
|
|
321
|
+
readonly filesUnchanged: string
|
|
322
|
+
readonly filesToRestore: (count: number) => string
|
|
323
|
+
readonly summaryBoth: string
|
|
324
|
+
readonly summaryCode: string
|
|
325
|
+
readonly summaryMessages: string
|
|
326
|
+
readonly sharedBlocked: string
|
|
327
|
+
readonly branchChanged: string
|
|
328
|
+
readonly headAdvanced: string
|
|
329
|
+
readonly driftBlocked: string
|
|
330
|
+
readonly planMissing: string
|
|
331
|
+
readonly stale: string
|
|
332
|
+
readonly nothingToRestore: string
|
|
333
|
+
readonly loadingAllFiles: string
|
|
334
|
+
readonly viewAllFiles: (count: number) => string
|
|
335
|
+
readonly backupNote: string
|
|
336
|
+
readonly recheck: string
|
|
337
|
+
readonly completedMessages: string
|
|
338
|
+
readonly completedCode: string
|
|
339
|
+
readonly completedBoth: string
|
|
340
|
+
readonly openFailed: (reason: string) => string
|
|
341
|
+
readonly restoredButOpenFailed: (reason: string) => string
|
|
342
|
+
readonly modeMismatch: (mode: string) => string
|
|
343
|
+
readonly listChangedWhileExpanding: string
|
|
344
|
+
readonly incompleteFileList: string
|
|
345
|
+
readonly fileKinds: Readonly<Record<ChangeKind, string>>
|
|
346
|
+
/** Keyed by the Host error code, which stays the same in every language. */
|
|
347
|
+
readonly errors: Readonly<Record<string, string>>
|
|
348
|
+
readonly captureSnapshotLimit: string
|
|
349
|
+
readonly captureFileLimit: string
|
|
350
|
+
readonly captureSkipped: (count: number, example: string | undefined) => string
|
|
351
|
+
readonly skipTimeout: string
|
|
352
|
+
readonly skipDisabled: string
|
|
353
|
+
readonly skipNewContentLimit: string
|
|
354
|
+
readonly skipOther: (reason: string) => string
|
|
355
|
+
readonly failureNotGitRepository: string
|
|
356
|
+
readonly failureGitStatus: (message: string) => string
|
|
357
|
+
readonly failureSizeOrCount: string
|
|
358
|
+
readonly failureUnsupportedType: string
|
|
359
|
+
readonly failureIgnoreFileInvalid: string
|
|
360
|
+
readonly failureInvalidPath: string
|
|
361
|
+
readonly failureOther: (message: string) => string
|
|
362
|
+
readonly settingsCardTitle: string
|
|
363
|
+
readonly settingsCardDescription: string
|
|
364
|
+
readonly autoCheckpointSection: string
|
|
365
|
+
readonly settingsLoading: string
|
|
366
|
+
readonly settingsUnavailable: string
|
|
367
|
+
readonly settingsReadOnly: string
|
|
368
|
+
readonly autoCheckpointLabel: string
|
|
369
|
+
readonly autoCheckpointDescription: string
|
|
370
|
+
readonly trustLabel: string
|
|
371
|
+
readonly trustDescription: string
|
|
372
|
+
readonly overridden: string
|
|
373
|
+
readonly resetToDefault: string
|
|
374
|
+
readonly positiveInteger: (field: string) => string
|
|
375
|
+
readonly storageDir: (directory: string) => string
|
|
376
|
+
readonly manageSection: string
|
|
377
|
+
readonly refreshing: string
|
|
378
|
+
readonly refresh: string
|
|
379
|
+
readonly clearing: string
|
|
380
|
+
readonly confirmClearAll: string
|
|
381
|
+
readonly clearAll: string
|
|
382
|
+
readonly manageLoading: string
|
|
383
|
+
readonly manageTotal: (workspaces: number, points: number, size: string) => string
|
|
384
|
+
readonly workspacePoints: (count: number) => string
|
|
385
|
+
readonly pendingRecoveries: (count: number) => string
|
|
386
|
+
readonly expand: string
|
|
387
|
+
readonly collapse: string
|
|
388
|
+
readonly clearWorkspace: string
|
|
389
|
+
readonly fileCount: (count: number) => string
|
|
390
|
+
readonly deleting: string
|
|
391
|
+
readonly remove: string
|
|
392
|
+
readonly noCheckpoints: string
|
|
393
|
+
readonly clearedAll: (deleted: number, retained: number, failures: number) => string
|
|
394
|
+
readonly cleared: (deleted: number, retained: number) => string
|
|
395
|
+
readonly checkpointModes: Readonly<Record<TurnRewindSettingsValue['turnCheckpointMode'], string>>
|
|
396
|
+
readonly trustOptions: Readonly<Record<TurnRewindSettingsValue['turnCheckpointTrust'], string>>
|
|
397
|
+
/** Keyed by the durable restore-point kind, which is not a display value. */
|
|
398
|
+
readonly pointKinds: Readonly<Record<string, string>>
|
|
399
|
+
readonly numberFields: Readonly<Record<NumberSettingsField, FieldText>>
|
|
400
|
+
readonly manageMissingWorkspaces: string
|
|
401
|
+
readonly manageMissingRestorePoints: string
|
|
402
|
+
readonly clearMissingReports: string
|
|
403
|
+
readonly previewMissingChanges: string
|
|
404
|
+
readonly previewMissingActiveSessionIds: string
|
|
405
|
+
readonly invalidField: (name: string) => string
|
|
406
|
+
readonly invalidObject: string
|
|
407
|
+
readonly unknownStatus: (status: string) => string
|
|
408
|
+
readonly sessionNotReady: string
|
|
409
|
+
readonly unparsableResponse: (status: string) => string
|
|
410
|
+
readonly requestFailed: (status: string) => string
|
|
411
|
+
readonly emptyResponse: string
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Chinese copy — the language this plugin shipped in before English existed. */
|
|
415
|
+
const ZH: RewindText = {
|
|
416
|
+
rewindTooltip: '恢复到发送这条消息之前',
|
|
417
|
+
dialogClose: '关闭',
|
|
418
|
+
dialogDescription: '查看恢复的文件,选择适合你的回退方式。当前会话不受影响。',
|
|
419
|
+
cancel: '取消',
|
|
420
|
+
applying: '正在恢复…',
|
|
421
|
+
done: '已完成',
|
|
422
|
+
actionRestoreAndRestart: '恢复并从这里继续',
|
|
423
|
+
actionRestoreFiles: '恢复文件',
|
|
424
|
+
actionMessagesOnly: '只回溯消息',
|
|
425
|
+
checkingFiles: '正在检查可以恢复的项目文件…',
|
|
426
|
+
previewPending: '这条消息发送前的文件还在保存,请稍后再试。',
|
|
427
|
+
previewMissing: '没有保存这条消息发送前的文件。可能是当时还没启用回退功能、记录已超过保留期限,或已关闭自动检查点。仍可只回溯消息。',
|
|
428
|
+
modeBothTitle: '恢复文件并从这里继续',
|
|
429
|
+
modeBothDescription: '创建一个从这里开始的新会话(当前对话会保留)',
|
|
430
|
+
modeCodeTitle: '只恢复文件',
|
|
431
|
+
modeCodeDescription: '恢复这条消息发送前的文件,当前对话保持不变。',
|
|
432
|
+
modeMessagesTitle: '只回溯消息(不动文件)',
|
|
433
|
+
modeMessagesDescription: '创建一个从这里开始的新会话,项目文件保持当前状态。',
|
|
434
|
+
filesUnchanged: '项目文件保持不变',
|
|
435
|
+
filesToRestore: count => `将恢复 ${String(count)} 个文件`,
|
|
436
|
+
summaryBoth: '恢复后在新对话里继续',
|
|
437
|
+
summaryCode: '当前对话保持不变',
|
|
438
|
+
summaryMessages: '仅创建从这里继续的新对话',
|
|
439
|
+
sharedBlocked: '这个项目目录还有别的对话正在运行。恢复文件会影响到它们,因此本次操作已被阻止。请等那些对话结束或停止后,再重新检查。',
|
|
440
|
+
branchChanged: '当前所在的 Git 分支和发送这条消息时不同。恢复不会切换分支,只会把当时的文件内容恢复到当前分支。',
|
|
441
|
+
headAdvanced: '这条消息之后有了新的 Git 提交。恢复只会改文件,不会撤销提交;完成后这些文件会显示为未提交修改。',
|
|
442
|
+
driftBlocked: 'Git 正在进行合并、变基或类似操作。请先完成或取消这次 Git 操作,再重新检查。',
|
|
443
|
+
planMissing: '恢复信息已经失效,请重新检查。',
|
|
444
|
+
stale: '项目文件在检查后又发生了变化。为避免覆盖新修改,这次恢复已失效,请重新检查。',
|
|
445
|
+
nothingToRestore: '项目文件已经是这条消息发送前的状态,无需恢复文件。可选择「只回溯消息」重新开始这段对话。',
|
|
446
|
+
loadingAllFiles: '正在读取全部文件…',
|
|
447
|
+
viewAllFiles: count => `查看全部 ${String(count)} 个文件`,
|
|
448
|
+
backupNote: '恢复前会自动备份当前文件;若恢复失败会自动还原,项目不会停留在只恢复了一部分的状态。',
|
|
449
|
+
recheck: '重新检查',
|
|
450
|
+
completedMessages: '已创建从这里开始的新对话;项目文件保持不变。',
|
|
451
|
+
completedCode: '项目文件已恢复;当前对话保持不变。恢复前的文件已自动备份。',
|
|
452
|
+
completedBoth: '项目文件已恢复,并已创建新对话。恢复前的文件已自动备份。',
|
|
453
|
+
openFailed: reason => `新对话已创建,但没能自动打开:${reason}`,
|
|
454
|
+
restoredButOpenFailed: reason => `文件已经恢复,新对话也已创建,但没能自动打开:${reason}`,
|
|
455
|
+
modeMismatch: mode => `服务器返回了不匹配的回退模式:${mode}`,
|
|
456
|
+
listChangedWhileExpanding: '项目文件在展开列表时发生了变化。',
|
|
457
|
+
incompleteFileList: '无法读取完整的文件列表。',
|
|
458
|
+
fileKinds: {
|
|
459
|
+
added: '移除后来新增的文件',
|
|
460
|
+
deleted: '找回文件',
|
|
461
|
+
modified: '恢复之前的版本',
|
|
462
|
+
'mode-changed': '恢复文件权限',
|
|
463
|
+
'type-changed': '恢复之前的文件类型',
|
|
464
|
+
},
|
|
465
|
+
errors: {
|
|
466
|
+
REWIND_ENDPOINT_UNAVAILABLE: '回退服务没有响应。请确认 @anionex/dsh-turn-rewind 已挂载到当前 DSH,并重启 DSH 后重试。',
|
|
467
|
+
REWIND_INVALID_RESPONSE: '回退服务返回了无法解析的内容。请重启 DSH 后重试。',
|
|
468
|
+
PLAN_STALE: '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。',
|
|
469
|
+
PLAN_STALE_WORKSPACE: '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。',
|
|
470
|
+
PLAN_STALE_REPOSITORY: 'Git 状态在检查后又发生了变化,恢复已失效。请重新检查后再试。',
|
|
471
|
+
WORKSPACE_IN_USE: '这个项目目录还有别的对话正在运行。请等那些对话结束或停止后,再重新检查。',
|
|
472
|
+
WORKSPACE_LOCKED: '另一个恢复操作正在处理这个项目目录。请等待它完成后重新检查。',
|
|
473
|
+
WORKSPACE_CHANGED: '这个项目目录已经不在原来的位置,旧的回退点无法再使用。',
|
|
474
|
+
WORKSPACE_MODE_CHANGED: '这个项目目录的工作区类型变了(例如从普通目录变成了 Git 仓库)。旧的回退点不再适用,请重新发送一条消息生成新的回退点。',
|
|
475
|
+
HEAD_CHANGED: '项目的提交或分支已发生变化。为避免覆盖新改动,请重新检查后再恢复。',
|
|
476
|
+
REPOSITORY_CHANGED: '这个项目目录已不属于原来的 Git 工作区,无法恢复。',
|
|
477
|
+
GIT_OPERATION_CHANGED: 'Git 正在执行其他操作。请先完成或取消该操作,再重新检查。',
|
|
478
|
+
RESTORE_POINT_NOT_FOUND: '没有找到对应的文件状态,可能已被清理。',
|
|
479
|
+
NO_CHANGES: '项目文件已经是这条消息发送前的状态,无需恢复文件。可选择「只回溯消息」重新开始这段对话。',
|
|
480
|
+
RESTORE_FAILED_ROLLED_BACK: '恢复未能完成,项目文件已自动还原到操作前的状态。',
|
|
481
|
+
CONVERSATION_REWIND_FAILED: '文件已恢复,但无法创建新对话;项目文件已自动还原。',
|
|
482
|
+
},
|
|
483
|
+
captureSnapshotLimit: '本轮要保存的文件总量超过上限,剩下的文件没有纳入检查点。恢复时不会改动它们。',
|
|
484
|
+
captureFileLimit: '本轮文件数量超过上限,多出来的文件没有纳入检查点。恢复时不会改动它们。',
|
|
485
|
+
captureSkipped: (count, example) => `有 ${String(count)} 个文件因为超过单文件大小上限或类型不受支持,没有纳入检查点${example === undefined ? '' : `(例如 ${example})`}。恢复时不会改动它们。`,
|
|
486
|
+
skipTimeout: '这个项目目录太大,在检查点时间上限内没保存完文件快照,所以本轮没有回退点(消息本身没有受影响)。可以在插件设置里调大「检查点时间上限」,或在目录根用 .dsh-rewindignore 排除大目录(例如 node_modules、构建产物、数据集)。仍可只回溯消息。',
|
|
487
|
+
skipDisabled: '自动文件检查点已在设置里关闭,本轮没有回退点。仍可只回溯消息。',
|
|
488
|
+
skipNewContentLimit: '这一轮新增的内容超过检查点预算,没有保存文件快照。仍可只回溯消息。',
|
|
489
|
+
skipOther: reason => `本轮没有保存文件检查点:${reason}仍可只回溯消息。`,
|
|
490
|
+
failureNotGitRepository: '这个项目目录不是 Git 仓库,回退功能无法保存文件检查点。仍可只回溯消息。',
|
|
491
|
+
failureGitStatus: message => `无法读取这个项目目录的 Git 状态:${message}仍可只回溯消息。`,
|
|
492
|
+
failureSizeOrCount: '本轮有文件超过检查点的大小或数量上限,没有保存文件检查点。仍可只回溯消息。',
|
|
493
|
+
failureUnsupportedType: '项目目录里有无法保存的特殊文件(如 socket、设备文件、FIFO),没有保存文件检查点。仍可只回溯消息。',
|
|
494
|
+
failureIgnoreFileInvalid: '项目目录里的 .dsh-rewindignore 内容无效,没有保存文件检查点。修正该文件后可重新发送消息。',
|
|
495
|
+
failureInvalidPath: '项目目录里有无法安全保存的路径(例如嵌套的独立 Git 仓库),没有保存文件检查点。仍可只回溯消息。',
|
|
496
|
+
failureOther: message => `没能保存这条消息发送前的文件:${message}仍可只回溯消息。`,
|
|
497
|
+
settingsCardTitle: 'Turn Rewind 回退设置',
|
|
498
|
+
settingsCardDescription: '自动文件检查点、信任策略与检查点保留上限;回退按钮在各条用户消息上',
|
|
499
|
+
autoCheckpointSection: '自动文件检查点',
|
|
500
|
+
settingsLoading: '正在加载设置…',
|
|
501
|
+
settingsUnavailable: '当前部署未提供设置服务,以下选项不可用。',
|
|
502
|
+
settingsReadOnly: '设置为只读(当前浏览器进程内保存),修改不可用。',
|
|
503
|
+
autoCheckpointLabel: '自动文件检查点',
|
|
504
|
+
autoCheckpointDescription: '关闭后不再为每条消息保存文件检查点;回退弹窗仍可只回溯消息',
|
|
505
|
+
trustLabel: '检查点信任策略',
|
|
506
|
+
trustDescription: '快速信任 Git/stat 元数据;严格会逐一重读文件内容',
|
|
507
|
+
overridden: '已覆盖',
|
|
508
|
+
resetToDefault: '恢复默认',
|
|
509
|
+
positiveInteger: field => `${field} 必须是正整数。`,
|
|
510
|
+
storageDir: directory => `存储目录(在 cordis.patch.yml 中配置,不可在线修改):${directory}`,
|
|
511
|
+
manageSection: '检查点管理',
|
|
512
|
+
refreshing: '正在刷新…',
|
|
513
|
+
refresh: '刷新',
|
|
514
|
+
clearing: '正在清理…',
|
|
515
|
+
confirmClearAll: '确认清空全部',
|
|
516
|
+
clearAll: '一键清空全部',
|
|
517
|
+
manageLoading: '正在读取检查点占用…',
|
|
518
|
+
manageTotal: (workspaces, points, size) => `共 ${String(workspaces)} 个工作区,${String(points)} 个检查点,约 ${size}(Git 原生检查点的实际磁盘占用以 Git 回收为准)。`,
|
|
519
|
+
workspacePoints: count => `${String(count)} 个检查点`,
|
|
520
|
+
pendingRecoveries: count => `${String(count)} 个恢复待处理`,
|
|
521
|
+
expand: '展开',
|
|
522
|
+
collapse: '收起',
|
|
523
|
+
clearWorkspace: '清空此项目',
|
|
524
|
+
fileCount: count => `${String(count)} 个文件`,
|
|
525
|
+
deleting: '正在删除…',
|
|
526
|
+
remove: '删除',
|
|
527
|
+
noCheckpoints: '还没有任何已保存的检查点。',
|
|
528
|
+
clearedAll: (deleted, retained, failures) => `已删除 ${String(deleted)} 个检查点${retained > 0 ? `,${String(retained)} 个受保护检查点未删除` : ''}${failures > 0 ? `,${String(failures)} 个工作区清理失败` : ''}。`,
|
|
529
|
+
cleared: (deleted, retained) => `已删除 ${String(deleted)} 个检查点${retained > 0 ? `,${String(retained)} 个受保护检查点未删除` : ''}。`,
|
|
530
|
+
checkpointModes: {
|
|
531
|
+
off: '关闭(不创建文件检查点)',
|
|
532
|
+
auto: '自动(推荐)',
|
|
533
|
+
'git-native': 'Git 原生(大仓库)',
|
|
534
|
+
legacy: '完整快照(兼容模式)',
|
|
535
|
+
},
|
|
536
|
+
trustOptions: {
|
|
537
|
+
fast: '快速',
|
|
538
|
+
strict: '严格',
|
|
539
|
+
},
|
|
540
|
+
pointKinds: {
|
|
541
|
+
user: '手动',
|
|
542
|
+
rescue: '救援',
|
|
543
|
+
turn: '轮次',
|
|
544
|
+
},
|
|
545
|
+
numberFields: {
|
|
546
|
+
maxRestorePoints: { label: '用户/救援恢复点上限', description: '每个工作区保留的最大手动与救援恢复点数量' },
|
|
547
|
+
maxTurnCheckpointsPerSession: { label: '轮次检查点上限', description: '每个会话保留的最大自动轮次检查点数量(最旧的先清理)' },
|
|
548
|
+
maxFiles: { label: '单恢复点文件数上限', description: '一个恢复点最多纳入的文件数量' },
|
|
549
|
+
maxFileBytes: { label: '单文件大小上限', description: '读取单个普通文件的最大字节数' },
|
|
550
|
+
maxSnapshotBytes: { label: '快照总量上限', description: '单个恢复点读取的最大字节总量' },
|
|
551
|
+
planTtlMs: { label: '恢复计划有效期(毫秒)', description: '回溯计划从创建到失效的时间' },
|
|
552
|
+
staleLockMs: { label: '锁回收时长(毫秒)', description: '锁属主消失多久后允许回收该锁' },
|
|
553
|
+
turnCheckpointTimeoutMs: { label: '检查点超时(毫秒)', description: '单次自动检查点最多阻塞消息发送的时间,超时记录跳过' },
|
|
554
|
+
turnCheckpointMaxNewBytes: { label: '检查点读取上限', description: '单次 Git 原生检查点最多读取的未缓存字节数' },
|
|
555
|
+
},
|
|
556
|
+
manageMissingWorkspaces: '管理数据缺少 workspaces',
|
|
557
|
+
manageMissingRestorePoints: '管理数据缺少 restorePoints',
|
|
558
|
+
clearMissingReports: '清理结果缺少 reports',
|
|
559
|
+
previewMissingChanges: '回退预览缺少 changes',
|
|
560
|
+
previewMissingActiveSessionIds: '回退预览缺少 activeSessionIds',
|
|
561
|
+
invalidField: name => `${name} 无效`,
|
|
562
|
+
invalidObject: '服务器返回了无效对象',
|
|
563
|
+
unknownStatus: status => `未知回退状态:${status}`,
|
|
564
|
+
sessionNotReady: '新对话还没有准备好',
|
|
565
|
+
unparsableResponse: status => `回退服务返回了无法解析的内容(HTTP ${status})。`,
|
|
566
|
+
requestFailed: status => `请求失败:${status}`,
|
|
567
|
+
emptyResponse: '回退服务返回了空响应。',
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Count one English noun, used only by the English copy.
|
|
572
|
+
* @param count - the quantity to render.
|
|
573
|
+
* @param singular - the noun in its singular form.
|
|
574
|
+
* @param plural - the plural form, when adding `s` is wrong.
|
|
575
|
+
* @returns the counted phrase.
|
|
576
|
+
*/
|
|
577
|
+
function counted(count: number, singular: string, plural = `${singular}s`): string {
|
|
578
|
+
return `${String(count)} ${count === 1 ? singular : plural}`
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** English copy, used when the Host reports a non-Chinese UI language. */
|
|
582
|
+
const EN: RewindText = {
|
|
583
|
+
rewindTooltip: 'Return to before sending this message',
|
|
584
|
+
dialogClose: 'Close',
|
|
585
|
+
dialogDescription: 'Review the files that would be restored and pick the rewind mode you want. The current session is unaffected.',
|
|
586
|
+
cancel: 'Cancel',
|
|
587
|
+
applying: 'Restoring…',
|
|
588
|
+
done: 'Done',
|
|
589
|
+
actionRestoreAndRestart: 'Restore and continue from here',
|
|
590
|
+
actionRestoreFiles: 'Restore files',
|
|
591
|
+
actionMessagesOnly: 'Rewind messages only',
|
|
592
|
+
checkingFiles: 'Checking which project files can be restored…',
|
|
593
|
+
previewPending: 'The files from before this message are still being saved. Try again in a moment.',
|
|
594
|
+
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.',
|
|
595
|
+
modeBothTitle: 'Restore files and restart',
|
|
596
|
+
modeBothDescription: 'Creates a new session starting here (the current conversation is kept)',
|
|
597
|
+
modeCodeTitle: 'Restore files only',
|
|
598
|
+
modeCodeDescription: 'Restores the files from before this message and leaves the current conversation unchanged.',
|
|
599
|
+
modeMessagesTitle: 'Rewind messages only (files untouched)',
|
|
600
|
+
modeMessagesDescription: 'Creates a new session starting here and leaves the project files exactly as they are.',
|
|
601
|
+
filesUnchanged: 'Project files stay unchanged',
|
|
602
|
+
filesToRestore: count => `Will restore ${counted(count, 'file')}`,
|
|
603
|
+
summaryBoth: 'Continue in a new conversation after restoring',
|
|
604
|
+
summaryCode: 'The current conversation stays unchanged',
|
|
605
|
+
summaryMessages: 'Only creates a new conversation continuing from here',
|
|
606
|
+
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.',
|
|
607
|
+
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.',
|
|
608
|
+
headAdvanced: 'There are new Git commits after this message. Restoring only changes files and never undoes commits; afterwards those files appear as uncommitted changes.',
|
|
609
|
+
driftBlocked: 'Git is in the middle of a merge, rebase, or similar operation. Finish or cancel that Git operation, then check again.',
|
|
610
|
+
planMissing: 'The restore information has expired. Check again.',
|
|
611
|
+
stale: 'The project files changed again after the check. To avoid overwriting the newer edits, this restore is no longer valid. Check again.',
|
|
612
|
+
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.',
|
|
613
|
+
loadingAllFiles: 'Loading all files…',
|
|
614
|
+
viewAllFiles: count => `View all ${counted(count, 'file')}`,
|
|
615
|
+
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.',
|
|
616
|
+
recheck: 'Check again',
|
|
617
|
+
completedMessages: 'A new conversation starting here has been created; the project files are unchanged.',
|
|
618
|
+
completedCode: 'The project files have been restored; the current conversation is unchanged. The files from before the restore were backed up automatically.',
|
|
619
|
+
completedBoth: 'The project files have been restored and a new conversation was created. The files from before the restore were backed up automatically.',
|
|
620
|
+
openFailed: reason => `The new conversation was created but could not be opened automatically: ${reason}`,
|
|
621
|
+
restoredButOpenFailed: reason => `The files were restored and the new conversation was created, but it could not be opened automatically: ${reason}`,
|
|
622
|
+
modeMismatch: mode => `The server returned a rewind mode that does not match: ${mode}`,
|
|
623
|
+
listChangedWhileExpanding: 'The project files changed while the full list was being loaded.',
|
|
624
|
+
incompleteFileList: 'Could not read the complete file list.',
|
|
625
|
+
fileKinds: {
|
|
626
|
+
added: 'Remove the file added later',
|
|
627
|
+
deleted: 'Bring the file back',
|
|
628
|
+
modified: 'Restore the earlier version',
|
|
629
|
+
'mode-changed': 'Restore the file permissions',
|
|
630
|
+
'type-changed': 'Restore the earlier file type',
|
|
631
|
+
},
|
|
632
|
+
errors: {
|
|
633
|
+
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.',
|
|
634
|
+
REWIND_INVALID_RESPONSE: 'The rewind service returned content that could not be parsed. Restart DSH and try again.',
|
|
635
|
+
PLAN_STALE: 'The project files changed again after the check. To avoid overwriting the newer edits, check again before restoring.',
|
|
636
|
+
PLAN_STALE_WORKSPACE: 'The project files changed again after the check. To avoid overwriting the newer edits, check again before restoring.',
|
|
637
|
+
PLAN_STALE_REPOSITORY: 'The Git state changed after the check, so the restore is no longer valid. Check again and retry.',
|
|
638
|
+
WORKSPACE_IN_USE: 'Another conversation is still running in this project directory. Wait for those conversations to finish or stop them, then check again.',
|
|
639
|
+
WORKSPACE_LOCKED: 'Another restore is already working on this project directory. Wait for it to finish, then check again.',
|
|
640
|
+
WORKSPACE_CHANGED: 'This project directory is no longer in its original location, so the earlier restore points cannot be used.',
|
|
641
|
+
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.',
|
|
642
|
+
HEAD_CHANGED: 'The commits or branch of this project changed. To avoid overwriting newer work, check again before restoring.',
|
|
643
|
+
REPOSITORY_CHANGED: 'This project directory no longer belongs to its original Git worktree, so it cannot be restored.',
|
|
644
|
+
GIT_OPERATION_CHANGED: 'Git is running another operation. Finish or cancel it, then check again.',
|
|
645
|
+
RESTORE_POINT_NOT_FOUND: 'No matching file state was found; it may already have been cleaned up.',
|
|
646
|
+
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.',
|
|
647
|
+
RESTORE_FAILED_ROLLED_BACK: 'The restore did not finish; the project files were rolled back to their state before the operation.',
|
|
648
|
+
CONVERSATION_REWIND_FAILED: 'The files were restored but the new conversation could not be created; the project files were rolled back.',
|
|
649
|
+
},
|
|
650
|
+
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.',
|
|
651
|
+
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.',
|
|
652
|
+
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.`,
|
|
653
|
+
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.',
|
|
654
|
+
skipDisabled: 'Automatic file checkpoints are turned off in settings, so this turn has no restore point. You can still rewind messages only.',
|
|
655
|
+
skipNewContentLimit: 'This turn added more content than the checkpoint budget allows, so no file snapshot was saved. You can still rewind messages only.',
|
|
656
|
+
skipOther: reason => `No file checkpoint was saved for this turn: ${reason} You can still rewind messages only.`,
|
|
657
|
+
failureNotGitRepository: 'This project directory is not a Git repository, so rewind cannot save file checkpoints. You can still rewind messages only.',
|
|
658
|
+
failureGitStatus: message => `Could not read the Git state of this project directory: ${message} You can still rewind messages only.`,
|
|
659
|
+
failureSizeOrCount: 'Files in this turn exceeded the checkpoint size or count limit, so no file checkpoint was saved. You can still rewind messages only.',
|
|
660
|
+
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.',
|
|
661
|
+
failureIgnoreFileInvalid: 'The .dsh-rewindignore in the project directory is invalid, so no file checkpoint was saved. Fix that file and send the message again.',
|
|
662
|
+
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.',
|
|
663
|
+
failureOther: message => `Could not save the files from before this message: ${message} You can still rewind messages only.`,
|
|
664
|
+
settingsCardTitle: 'Turn Rewind settings',
|
|
665
|
+
settingsCardDescription: 'Automatic file checkpoints, trust policy, and checkpoint retention limits; the rewind button sits on each user message',
|
|
666
|
+
autoCheckpointSection: 'Automatic file checkpoints',
|
|
667
|
+
settingsLoading: 'Loading settings…',
|
|
668
|
+
settingsUnavailable: 'This deployment provides no settings service, so the options below are unavailable.',
|
|
669
|
+
settingsReadOnly: 'Settings are read-only (kept inside this browser process), so changes are unavailable.',
|
|
670
|
+
autoCheckpointLabel: 'Automatic file checkpoints',
|
|
671
|
+
autoCheckpointDescription: 'When off, no file checkpoint is saved for each message; the rewind dialog can still rewind messages only',
|
|
672
|
+
trustLabel: 'Checkpoint trust policy',
|
|
673
|
+
trustDescription: 'Fast trusts Git/stat metadata; Strict re-reads every file’s content',
|
|
674
|
+
overridden: 'Overridden',
|
|
675
|
+
resetToDefault: 'Reset to default',
|
|
676
|
+
positiveInteger: field => `${field} must be a positive integer.`,
|
|
677
|
+
storageDir: directory => `Storage directory (configured in cordis.patch.yml, not editable here): ${directory}`,
|
|
678
|
+
manageSection: 'Checkpoint management',
|
|
679
|
+
refreshing: 'Refreshing…',
|
|
680
|
+
refresh: 'Refresh',
|
|
681
|
+
clearing: 'Clearing…',
|
|
682
|
+
confirmClearAll: 'Confirm clear all',
|
|
683
|
+
clearAll: 'Clear all',
|
|
684
|
+
manageLoading: 'Reading checkpoint usage…',
|
|
685
|
+
manageTotal: (workspaces, points, size) => `${counted(workspaces, 'workspace')}, ${counted(points, 'checkpoint')}, about ${size} (disk use for Git-native checkpoints follows Git garbage collection).`,
|
|
686
|
+
workspacePoints: count => counted(count, 'checkpoint'),
|
|
687
|
+
pendingRecoveries: count => `${counted(count, 'recovery', 'recoveries')} pending`,
|
|
688
|
+
expand: 'Expand',
|
|
689
|
+
collapse: 'Collapse',
|
|
690
|
+
clearWorkspace: 'Clear this project',
|
|
691
|
+
fileCount: count => counted(count, 'file'),
|
|
692
|
+
deleting: 'Deleting…',
|
|
693
|
+
remove: 'Delete',
|
|
694
|
+
noCheckpoints: 'No checkpoints have been saved yet.',
|
|
695
|
+
clearedAll: (deleted, retained, failures) => `Deleted ${counted(deleted, 'checkpoint')}${retained > 0 ? `; ${counted(retained, 'protected checkpoint')} kept` : ''}${failures > 0 ? `; ${counted(failures, 'workspace')} failed to clear` : ''}.`,
|
|
696
|
+
cleared: (deleted, retained) => `Deleted ${counted(deleted, 'checkpoint')}${retained > 0 ? `; ${counted(retained, 'protected checkpoint')} kept` : ''}.`,
|
|
697
|
+
checkpointModes: {
|
|
698
|
+
off: 'Off (no file checkpoints)',
|
|
699
|
+
auto: 'Auto (recommended)',
|
|
700
|
+
'git-native': 'Git-native (large repositories)',
|
|
701
|
+
legacy: 'Full snapshot (compatibility mode)',
|
|
702
|
+
},
|
|
703
|
+
trustOptions: {
|
|
704
|
+
fast: 'Fast',
|
|
705
|
+
strict: 'Strict',
|
|
706
|
+
},
|
|
707
|
+
pointKinds: {
|
|
708
|
+
user: 'Manual',
|
|
709
|
+
rescue: 'Rescue',
|
|
710
|
+
turn: 'Turn',
|
|
711
|
+
},
|
|
712
|
+
numberFields: {
|
|
713
|
+
maxRestorePoints: { label: 'User/rescue restore point limit', description: 'Most manual and rescue restore points kept per workspace' },
|
|
714
|
+
maxTurnCheckpointsPerSession: { label: 'Turn checkpoint limit', description: 'Most automatic turn checkpoints kept per session (oldest pruned first)' },
|
|
715
|
+
maxFiles: { label: 'Files per restore point', description: 'Most files one restore point may include' },
|
|
716
|
+
maxFileBytes: { label: 'Maximum file size', description: 'Most bytes read from a single regular file' },
|
|
717
|
+
maxSnapshotBytes: { label: 'Maximum snapshot size', description: 'Most bytes read in total for one restore point' },
|
|
718
|
+
planTtlMs: { label: 'Restore plan lifetime (ms)', description: 'How long a rewind plan stays valid after it is created' },
|
|
719
|
+
staleLockMs: { label: 'Stale lock timeout (ms)', description: 'How long after its owner disappears a lock may be reclaimed' },
|
|
720
|
+
turnCheckpointTimeoutMs: { label: 'Checkpoint timeout (ms)', description: 'How long one automatic checkpoint may hold up sending a message before it is recorded as skipped' },
|
|
721
|
+
turnCheckpointMaxNewBytes: { label: 'Checkpoint read limit', description: 'Most uncached bytes one Git-native checkpoint may read' },
|
|
722
|
+
},
|
|
723
|
+
manageMissingWorkspaces: 'The management data is missing workspaces',
|
|
724
|
+
manageMissingRestorePoints: 'The management data is missing restorePoints',
|
|
725
|
+
clearMissingReports: 'The clear result is missing reports',
|
|
726
|
+
previewMissingChanges: 'The rewind preview is missing changes',
|
|
727
|
+
previewMissingActiveSessionIds: 'The rewind preview is missing activeSessionIds',
|
|
728
|
+
invalidField: name => `${name} is invalid`,
|
|
729
|
+
invalidObject: 'The server returned an invalid object',
|
|
730
|
+
unknownStatus: status => `Unknown rewind status: ${status}`,
|
|
731
|
+
sessionNotReady: 'The new conversation is not ready yet',
|
|
732
|
+
unparsableResponse: status => `The rewind service returned content that could not be parsed (HTTP ${status}).`,
|
|
733
|
+
requestFailed: status => `Request failed: ${status}`,
|
|
734
|
+
emptyResponse: 'The rewind service returned an empty response.',
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const TEXT: Readonly<Record<UiLocale, RewindText>> = { zh: ZH, en: EN }
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Active UI language, and the listeners rendering it.
|
|
741
|
+
*
|
|
742
|
+
* One browser runs one copy of this plugin, so the active language is module
|
|
743
|
+
* state rather than React context: the pure helpers below (`friendlyError`,
|
|
744
|
+
* `explainCheckpointSkip`, and the decoders) produce user-facing sentences
|
|
745
|
+
* outside any render and would otherwise need their exported signatures
|
|
746
|
+
* changed.
|
|
747
|
+
*/
|
|
748
|
+
let uiLocale: UiLocale = 'zh'
|
|
749
|
+
const localeListeners = new Set<() => void>()
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Resolve one Host locale id to a language this plugin ships copy for.
|
|
753
|
+
*
|
|
754
|
+
* An absent, empty, or unreadable id means the Host published no UI language —
|
|
755
|
+
* DSH releases without the locale plugin, and profiles that do not mount it —
|
|
756
|
+
* and keeps Chinese, so an existing install never changes language on upgrade.
|
|
757
|
+
* Any other registered language resolves to English, mirroring the Host's own
|
|
758
|
+
* per-key fallback chain, which terminates at English rather than Chinese.
|
|
759
|
+
* @param active - the Host's active locale id, when it publishes one.
|
|
760
|
+
* @returns the language whose copy to render.
|
|
761
|
+
*/
|
|
762
|
+
export function resolveUiLocale(active: string | undefined): UiLocale {
|
|
763
|
+
if (typeof active !== 'string' || active === '') return 'zh'
|
|
764
|
+
return /^zh(?:[-_]|$)/i.test(active) ? 'zh' : 'en'
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Current UI copy. Chinese until the Host publishes another language. */
|
|
768
|
+
function uiText(): RewindText {
|
|
769
|
+
return TEXT[uiLocale]
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Adopt one Host locale id and re-render everything currently mounted. */
|
|
773
|
+
function adoptLocale(active: string | undefined): void {
|
|
774
|
+
const next = resolveUiLocale(active)
|
|
775
|
+
if (next === uiLocale) return
|
|
776
|
+
uiLocale = next
|
|
777
|
+
for (const listener of localeListeners) listener()
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** `useSyncExternalStore` subscribe half for the active language. */
|
|
781
|
+
function subscribeLocale(listener: () => void): () => void {
|
|
782
|
+
localeListeners.add(listener)
|
|
783
|
+
return () => { localeListeners.delete(listener) }
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Read the current copy and re-render this component when the language changes. */
|
|
787
|
+
function useText(): RewindText {
|
|
788
|
+
return useSyncExternalStore(subscribeLocale, uiText, uiText)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Follow the Host's UI language for as long as the plugin is mounted.
|
|
793
|
+
*
|
|
794
|
+
* The locale service is read through `ctx.get`, Cordis's un-injected read, and
|
|
795
|
+
* not declared in `inject`: an injected service the profile does not mount
|
|
796
|
+
* holds the fiber inactive, which would stop the rewind button appearing at all
|
|
797
|
+
* on a profile without the locale plugin. Late arrival is covered by the
|
|
798
|
+
* `locale/change` event, which is emitted on the context rather than on the
|
|
799
|
+
* service, so a listener registered here sees a switch even when the locale
|
|
800
|
+
* plugin loads after this one.
|
|
801
|
+
* @param ctx - the client context this plugin was applied to.
|
|
802
|
+
* @returns the teardown restoring the default language.
|
|
803
|
+
*/
|
|
804
|
+
function followHostLocale(ctx: ClientContextLike): () => void {
|
|
805
|
+
adoptLocale(hostLocale(ctx))
|
|
806
|
+
// Client plugin apply() order is not guaranteed, and `locale` is read
|
|
807
|
+
// un-injected (see the doc comment above) so this plugin never forces it
|
|
808
|
+
// to load first. When `dsh-client-locale` mounts after this plugin, the
|
|
809
|
+
// read above returns `undefined` and the surface falls back to Chinese;
|
|
810
|
+
// nothing then corrects it, because a stored preference that was already
|
|
811
|
+
// settled before this plugin mounted never fires another `locale/change`
|
|
812
|
+
// event. Re-checking once on the next microtask and once on the next
|
|
813
|
+
// macrotask catches both same-tick and deferred registration without
|
|
814
|
+
// adding a hard dependency; `adoptLocale` is a no-op when the language
|
|
815
|
+
// has not actually changed, so the extra calls are always safe.
|
|
816
|
+
const recheck = (): void => adoptLocale(hostLocale(ctx))
|
|
817
|
+
// `Promise` is an ECMAScript built-in present in every realm, unlike the
|
|
818
|
+
// host-provided `queueMicrotask`, which a constrained embedding (this
|
|
819
|
+
// plugin's own test sandbox included) need not supply.
|
|
820
|
+
void Promise.resolve().then(recheck)
|
|
821
|
+
const timeoutId = setTimeout(recheck, 0)
|
|
822
|
+
let off: unknown
|
|
823
|
+
try {
|
|
824
|
+
off = ctx.on?.('locale/change', (snapshot: LocaleSnapshotLike) => {
|
|
825
|
+
adoptLocale(typeof snapshot?.active === 'string' ? snapshot.active : hostLocale(ctx))
|
|
826
|
+
})
|
|
827
|
+
} catch {
|
|
828
|
+
off = undefined
|
|
829
|
+
}
|
|
830
|
+
return () => {
|
|
831
|
+
clearTimeout(timeoutId)
|
|
832
|
+
if (typeof off === 'function') off()
|
|
833
|
+
adoptLocale(undefined)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Read the Host's active locale id.
|
|
839
|
+
*
|
|
840
|
+
* The locale service belongs to the Host, so its presence and shape are not
|
|
841
|
+
* this plugin's to guarantee: a language that cannot be read leaves the rewind
|
|
842
|
+
* surface in Chinese instead of failing the effect that installs the button.
|
|
843
|
+
* @param ctx - the client context this plugin was applied to.
|
|
844
|
+
* @returns the active locale id, or `undefined` when none can be read.
|
|
845
|
+
*/
|
|
846
|
+
function hostLocale(ctx: ClientContextLike): string | undefined {
|
|
847
|
+
try {
|
|
848
|
+
const active = ctx.get?.('locale')?.getSnapshot().active
|
|
849
|
+
return typeof active === 'string' ? active : undefined
|
|
850
|
+
} catch {
|
|
851
|
+
return undefined
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
274
855
|
/** Return the rewind anchor and editable text owned by one direct user message. */
|
|
275
856
|
export function selectRewindMessage(node: ConversationNodeLike): RewindMatch | null {
|
|
276
857
|
if (node.kind !== 'user' || !Number.isSafeInteger(node.seq) || node.seq < 0) return null
|
|
@@ -286,11 +867,12 @@ export function selectRewindMessage(node: ConversationNodeLike): RewindMatch | n
|
|
|
286
867
|
*
|
|
287
868
|
* Every service read on `ctx` must be declared here: Cordis throws while reading an
|
|
288
869
|
* undeclared service off the context proxy, before optional chaining can apply.
|
|
289
|
-
*
|
|
290
|
-
*
|
|
870
|
+
* The settings transport is read through `ctx.get`: older Hosts expose
|
|
871
|
+
* `settingsScope`, while DSH 0.1.7 exposes `configForms`.
|
|
291
872
|
*/
|
|
292
|
-
export const inject = ['slots', 'sessions', 'conversation'
|
|
873
|
+
export const inject = ['slots', 'sessions', 'conversation']
|
|
293
874
|
export function apply(ctx: ClientContextLike): void {
|
|
875
|
+
ctx.effect(() => followHostLocale(ctx), 'turn-rewind: locale')
|
|
294
876
|
ctx.effect(() => {
|
|
295
877
|
if (document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) !== null) return () => {}
|
|
296
878
|
const tag = document.createElement('style')
|
|
@@ -314,9 +896,25 @@ export function apply(ctx: ClientContextLike): void {
|
|
|
314
896
|
name: 'settings.plugin.item',
|
|
315
897
|
key: 'turn-rewind',
|
|
316
898
|
inject: () => ({
|
|
317
|
-
scope: ctx
|
|
899
|
+
scope: settingsFormScope(ctx),
|
|
318
900
|
}),
|
|
319
901
|
}, TurnRewindSettingsCard))
|
|
902
|
+
// DSH 0.1.7 replaced the per-plugin item seat with feature-owned Settings tabs.
|
|
903
|
+
ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
|
|
904
|
+
name: 'settings.plugins.tab',
|
|
905
|
+
id: 'turn-rewind',
|
|
906
|
+
order: 80,
|
|
907
|
+
label: () => uiText().settingsCardTitle,
|
|
908
|
+
inject: () => ({ scope: settingsFormScope(ctx) }),
|
|
909
|
+
}, TurnRewindSettingsCard))
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** Read whichever settings transport this Host supplies without requiring both services. */
|
|
913
|
+
function settingsFormScope(ctx: ClientContextLike): SettingsScopeLike<TurnRewindSettingsValue> | undefined {
|
|
914
|
+
const forms = ctx.get?.('configForms') as { get<T>(entryId: string): SettingsScopeLike<T> } | undefined
|
|
915
|
+
if (forms !== undefined) return forms.get<TurnRewindSettingsValue>('turn-rewind')
|
|
916
|
+
const legacy = ctx.get?.('settingsScope') as { bind<T>(spec: { readonly namespace: string }): SettingsScopeLike<T> } | undefined
|
|
917
|
+
return legacy?.bind<TurnRewindSettingsValue>({ namespace: 'turn-rewind' })
|
|
320
918
|
}
|
|
321
919
|
|
|
322
920
|
/** Narrow a chat node source to the keyed store form. */
|
|
@@ -399,6 +997,7 @@ export function RewindMessagePortals({ sessionId, openRestoredSession, useSessio
|
|
|
399
997
|
|
|
400
998
|
/** User-message action and its review-first file/conversation restore dialog. */
|
|
401
999
|
export function RewindMessageAction({ matched, sessionId, openRestoredSession }: RewindMessageActionProps): ReactNode {
|
|
1000
|
+
const t = useText()
|
|
402
1001
|
const [open, setOpen] = useState(false)
|
|
403
1002
|
const [loading, setLoading] = useState(false)
|
|
404
1003
|
const [preview, setPreview] = useState<Preview | null>(null)
|
|
@@ -502,13 +1101,13 @@ export function RewindMessageAction({ matched, sessionId, openRestoredSession }:
|
|
|
502
1101
|
|| page.checkpointId !== ready.checkpointId
|
|
503
1102
|
|| page.totalChanges !== ready.totalChanges
|
|
504
1103
|
|| page.offset !== offset) {
|
|
505
|
-
throw new RewindRequestError('PLAN_STALE',
|
|
1104
|
+
throw new RewindRequestError('PLAN_STALE', t.listChangedWhileExpanding)
|
|
506
1105
|
}
|
|
507
1106
|
collected.push(...page.changes)
|
|
508
1107
|
offset += page.changes.length
|
|
509
1108
|
if (page.changes.length === 0) break
|
|
510
1109
|
}
|
|
511
|
-
if (offset !== ready.totalChanges) throw new RewindRequestError('PLAN_STALE',
|
|
1110
|
+
if (offset !== ready.totalChanges) throw new RewindRequestError('PLAN_STALE', t.incompleteFileList)
|
|
512
1111
|
setPreview({ ...ready, changes: collected, truncated: false })
|
|
513
1112
|
} catch (caught) {
|
|
514
1113
|
if (caught instanceof RewindRequestError && caught.code === 'PLAN_STALE') setStale(true)
|
|
@@ -542,31 +1141,31 @@ export function RewindMessageAction({ matched, sessionId, openRestoredSession }:
|
|
|
542
1141
|
})
|
|
543
1142
|
const result = recordOf(await responseJson(response))
|
|
544
1143
|
const resultMode = requiredString(result.mode, 'mode')
|
|
545
|
-
if (resultMode !== mode) throw new Error(
|
|
1144
|
+
if (resultMode !== mode) throw new Error(t.modeMismatch(resultMode))
|
|
546
1145
|
if (mode === 'messages') {
|
|
547
1146
|
const childSessionId = requiredString(result.sessionId, 'sessionId')
|
|
548
|
-
setCompleted(
|
|
1147
|
+
setCompleted(t.completedMessages)
|
|
549
1148
|
try {
|
|
550
1149
|
await openRestoredSession(childSessionId, matched.promptText)
|
|
551
1150
|
setOpen(false)
|
|
552
1151
|
} catch (navigationError) {
|
|
553
|
-
setError(
|
|
1152
|
+
setError(t.openFailed(messageOf(navigationError)))
|
|
554
1153
|
}
|
|
555
1154
|
return
|
|
556
1155
|
}
|
|
557
1156
|
if (mode === 'code') {
|
|
558
1157
|
requiredString(result.rescuePointId, 'rescuePointId')
|
|
559
|
-
setCompleted(
|
|
1158
|
+
setCompleted(t.completedCode)
|
|
560
1159
|
return
|
|
561
1160
|
}
|
|
562
1161
|
const childSessionId = requiredString(result.sessionId, 'sessionId')
|
|
563
1162
|
requiredString(result.rescuePointId, 'rescuePointId')
|
|
564
|
-
setCompleted(
|
|
1163
|
+
setCompleted(t.completedBoth)
|
|
565
1164
|
try {
|
|
566
1165
|
await openRestoredSession(childSessionId, matched.promptText)
|
|
567
1166
|
setOpen(false)
|
|
568
1167
|
} catch (navigationError) {
|
|
569
|
-
setError(
|
|
1168
|
+
setError(t.restoredButOpenFailed(messageOf(navigationError)))
|
|
570
1169
|
}
|
|
571
1170
|
} catch (caught) {
|
|
572
1171
|
if (caught instanceof RewindRequestError && (caught.code === 'PLAN_STALE' || caught.code === 'WORKSPACE_IN_USE')) {
|
|
@@ -580,53 +1179,53 @@ export function RewindMessageAction({ matched, sessionId, openRestoredSession }:
|
|
|
580
1179
|
}
|
|
581
1180
|
|
|
582
1181
|
const captureNotice = describeCaptureNotice(ready)
|
|
583
|
-
const actionLabel = mode === 'both' ?
|
|
1182
|
+
const actionLabel = mode === 'both' ? t.actionRestoreAndRestart : mode === 'code' ? t.actionRestoreFiles : t.actionMessagesOnly
|
|
584
1183
|
const radioName = `dcl-rewind-${sessionId}-${String(matched.messageSeq)}`
|
|
585
1184
|
const branchChanged = ready !== null && ready.checkpointBranch !== ready.currentBranch
|
|
586
1185
|
|
|
587
1186
|
return (
|
|
588
1187
|
<div className="dcl-rewind-tail">
|
|
589
|
-
<Tooltip label=
|
|
590
|
-
<button type="button" className="dcl-rewind-trigger" onClick={show} aria-label=
|
|
1188
|
+
<Tooltip label={t.rewindTooltip} side="bottom">
|
|
1189
|
+
<button type="button" className="dcl-rewind-trigger" onClick={show} aria-label={t.rewindTooltip}>
|
|
591
1190
|
<RewindIcon size={16} />
|
|
592
1191
|
</button>
|
|
593
1192
|
</Tooltip>
|
|
594
1193
|
<Modal
|
|
595
1194
|
open={open}
|
|
596
1195
|
onClose={close}
|
|
597
|
-
title=
|
|
598
|
-
closeLabel=
|
|
599
|
-
description=
|
|
1196
|
+
title={t.rewindTooltip}
|
|
1197
|
+
closeLabel={t.dialogClose}
|
|
1198
|
+
description={t.dialogDescription}
|
|
600
1199
|
className="dcl-rewind-dialog"
|
|
601
1200
|
contentClassName="dcl-rewind-content"
|
|
602
1201
|
footer={(
|
|
603
1202
|
<>
|
|
604
|
-
<Button variant="outline" onClick={close} disabled={applying}
|
|
1203
|
+
<Button variant="outline" onClick={close} disabled={applying}>{t.cancel}</Button>
|
|
605
1204
|
<Button variant="primary" onClick={() => { void applyRestore() }} disabled={!canApply}>
|
|
606
|
-
{applying ?
|
|
1205
|
+
{applying ? t.applying : completed === null ? actionLabel : t.done}
|
|
607
1206
|
</Button>
|
|
608
1207
|
</>
|
|
609
1208
|
)}
|
|
610
1209
|
>
|
|
611
1210
|
<div className="dcl-rewind-body">
|
|
612
|
-
{loading && <p className="dcl-rewind-status"
|
|
613
|
-
{preview?.status === 'pending' && <p className="dcl-rewind-status"
|
|
614
|
-
{preview?.status === 'missing' && <p className="dcl-rewind-error"
|
|
1211
|
+
{loading && <p className="dcl-rewind-status">{t.checkingFiles}</p>}
|
|
1212
|
+
{preview?.status === 'pending' && <p className="dcl-rewind-status">{t.previewPending}</p>}
|
|
1213
|
+
{preview?.status === 'missing' && <p className="dcl-rewind-error">{t.previewMissing}</p>}
|
|
615
1214
|
{preview?.status === 'skipped' && <p className="dcl-rewind-status">{explainCheckpointSkip(preview.reason)}</p>}
|
|
616
1215
|
{preview?.status === 'failed' && <p className="dcl-rewind-error">{explainCheckpointFailure(preview.error)}</p>}
|
|
617
1216
|
{preview !== null && (
|
|
618
1217
|
<div className="dcl-rewind-options">
|
|
619
1218
|
<label className="dcl-rewind-option" data-selected={mode === 'both'} data-disabled={applying || !hasFileChanges}>
|
|
620
1219
|
<input type="radio" name={radioName} checked={mode === 'both'} disabled={applying || !hasFileChanges} onChange={() => { chooseMode('both') }} />
|
|
621
|
-
<span className="dcl-rewind-option-content"><strong
|
|
1220
|
+
<span className="dcl-rewind-option-content"><strong>{t.modeBothTitle}</strong><span className="dcl-rewind-option-description">{t.modeBothDescription}</span></span>
|
|
622
1221
|
</label>
|
|
623
1222
|
<label className="dcl-rewind-option" data-selected={mode === 'code'} data-disabled={applying || !hasFileChanges}>
|
|
624
1223
|
<input type="radio" name={radioName} checked={mode === 'code'} disabled={applying || !hasFileChanges} onChange={() => { chooseMode('code') }} />
|
|
625
|
-
<span className="dcl-rewind-option-content"><strong
|
|
1224
|
+
<span className="dcl-rewind-option-content"><strong>{t.modeCodeTitle}</strong><span className="dcl-rewind-option-description">{t.modeCodeDescription}</span></span>
|
|
626
1225
|
</label>
|
|
627
1226
|
<label className="dcl-rewind-option" data-selected={mode === 'messages'} data-disabled={applying}>
|
|
628
1227
|
<input type="radio" name={radioName} checked={mode === 'messages'} disabled={applying} onChange={() => { chooseMode('messages') }} />
|
|
629
|
-
<span className="dcl-rewind-option-content"><strong
|
|
1228
|
+
<span className="dcl-rewind-option-content"><strong>{t.modeMessagesTitle}</strong><span className="dcl-rewind-option-description">{t.modeMessagesDescription}</span></span>
|
|
630
1229
|
</label>
|
|
631
1230
|
</div>
|
|
632
1231
|
)}
|
|
@@ -635,36 +1234,34 @@ export function RewindMessageAction({ matched, sessionId, openRestoredSession }:
|
|
|
635
1234
|
{captureNotice !== null && <p className="dcl-rewind-warning">{captureNotice}</p>}
|
|
636
1235
|
<div className="dcl-rewind-summary">
|
|
637
1236
|
{mode === 'messages'
|
|
638
|
-
? <strong
|
|
639
|
-
: <strong
|
|
640
|
-
<span>{mode === 'both' ?
|
|
1237
|
+
? <strong>{t.filesUnchanged}</strong>
|
|
1238
|
+
: <strong>{t.filesToRestore(ready.totalChanges)}</strong>}
|
|
1239
|
+
<span>{mode === 'both' ? t.summaryBoth : mode === 'code' ? t.summaryCode : t.summaryMessages}</span>
|
|
641
1240
|
</div>
|
|
642
1241
|
{sharedBlocked && (
|
|
643
|
-
<p className="dcl-rewind-error"
|
|
1242
|
+
<p className="dcl-rewind-error">{t.sharedBlocked}</p>
|
|
644
1243
|
)}
|
|
645
1244
|
{ready.headChanged && !ready.operationChanged && (
|
|
646
|
-
<p className="dcl-rewind-warning">{branchChanged
|
|
647
|
-
? '当前所在的 Git 分支和发送这条消息时不同。恢复不会切换分支,只会把当时的文件内容恢复到当前分支。'
|
|
648
|
-
: '这条消息之后有了新的 Git 提交。恢复只会改文件,不会撤销提交;完成后这些文件会显示为未提交修改。'}</p>
|
|
1245
|
+
<p className="dcl-rewind-warning">{branchChanged ? t.branchChanged : t.headAdvanced}</p>
|
|
649
1246
|
)}
|
|
650
|
-
{driftBlocked && <p className="dcl-rewind-warning">
|
|
651
|
-
{planMissing && <p className="dcl-rewind-error"
|
|
652
|
-
{stale && <p className="dcl-rewind-error"
|
|
653
|
-
{ready.totalChanges === 0 && <p className="dcl-rewind-status"
|
|
1247
|
+
{driftBlocked && <p className="dcl-rewind-warning">{t.driftBlocked}</p>}
|
|
1248
|
+
{planMissing && <p className="dcl-rewind-error">{t.planMissing}</p>}
|
|
1249
|
+
{stale && <p className="dcl-rewind-error">{t.stale}</p>}
|
|
1250
|
+
{ready.totalChanges === 0 && <p className="dcl-rewind-status">{t.nothingToRestore}</p>}
|
|
654
1251
|
{ready.changes.length > 0 && (
|
|
655
1252
|
<div className="dcl-rewind-files">
|
|
656
1253
|
{ready.changes.map(change => <div className="dcl-rewind-file" key={change.path}><code>{change.path}</code><span className="dcl-rewind-kind">{fileRecoveryLabel(change.kind)}</span></div>)}
|
|
657
1254
|
</div>
|
|
658
1255
|
)}
|
|
659
1256
|
{ready.truncated && (
|
|
660
|
-
<div className="dcl-rewind-file-actions"><Button variant="outline" size="sm" onClick={() => { void loadAllChanges() }} disabled={loadingDetails}>{loadingDetails ?
|
|
1257
|
+
<div className="dcl-rewind-file-actions"><Button variant="outline" size="sm" onClick={() => { void loadAllChanges() }} disabled={loadingDetails}>{loadingDetails ? t.loadingAllFiles : t.viewAllFiles(ready.totalChanges)}</Button></div>
|
|
661
1258
|
)}
|
|
662
1259
|
</>
|
|
663
1260
|
)}
|
|
664
1261
|
{completed !== null && <p className="dcl-rewind-status">{completed}</p>}
|
|
665
1262
|
{error !== null && <p className="dcl-rewind-error">{error}</p>}
|
|
666
|
-
{error !== null && <p className="dcl-rewind-backup"
|
|
667
|
-
{!loading && (preview?.status !== 'ready' || stale || planMissing || sharedBlocked || driftBlocked) && <Button className="dcl-rewind-retry" variant="outline" size="sm" onClick={() => { void load() }}
|
|
1263
|
+
{error !== null && <p className="dcl-rewind-backup">{t.backupNote}</p>}
|
|
1264
|
+
{!loading && (preview?.status !== 'ready' || stale || planMissing || sharedBlocked || driftBlocked) && <Button className="dcl-rewind-retry" variant="outline" size="sm" onClick={() => { void load() }}>{t.recheck}</Button>}
|
|
668
1265
|
</div>
|
|
669
1266
|
</Modal>
|
|
670
1267
|
</div>
|
|
@@ -688,38 +1285,21 @@ type NumberSettingsField = keyof Pick<TurnRewindSettingsValue,
|
|
|
688
1285
|
'maxRestorePoints' | 'maxTurnCheckpointsPerSession' | 'maxFiles' | 'maxFileBytes' | 'maxSnapshotBytes'
|
|
689
1286
|
| 'planTtlMs' | 'staleLockMs' | 'turnCheckpointTimeoutMs' | 'turnCheckpointMaxNewBytes'>
|
|
690
1287
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
{ key: 'maxFileBytes', label: '单文件大小上限', description: '读取单个普通文件的最大字节数' },
|
|
696
|
-
{ key: 'maxSnapshotBytes', label: '快照总量上限', description: '单个恢复点读取的最大字节总量' },
|
|
697
|
-
{ key: 'planTtlMs', label: '恢复计划有效期(毫秒)', description: '回溯计划从创建到失效的时间' },
|
|
698
|
-
{ key: 'staleLockMs', label: '锁回收时长(毫秒)', description: '锁属主消失多久后允许回收该锁' },
|
|
699
|
-
{ key: 'turnCheckpointTimeoutMs', label: '检查点超时(毫秒)', description: '单次自动检查点最多阻塞消息发送的时间,超时记录跳过' },
|
|
700
|
-
{ key: 'turnCheckpointMaxNewBytes', label: '检查点读取上限', description: '单次 Git 原生检查点最多读取的未缓存字节数' },
|
|
1288
|
+
/** Numeric settings fields, in the order the card renders them. */
|
|
1289
|
+
const NUMBER_FIELDS: readonly NumberSettingsField[] = [
|
|
1290
|
+
'maxRestorePoints', 'maxTurnCheckpointsPerSession', 'maxFiles', 'maxFileBytes', 'maxSnapshotBytes',
|
|
1291
|
+
'planTtlMs', 'staleLockMs', 'turnCheckpointTimeoutMs', 'turnCheckpointMaxNewBytes',
|
|
701
1292
|
]
|
|
702
1293
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
auto: '自动(推荐)',
|
|
706
|
-
'git-native': 'Git 原生(大仓库)',
|
|
707
|
-
legacy: '完整快照(兼容模式)',
|
|
708
|
-
}
|
|
1294
|
+
/** Checkpoint modes, in the order the select offers them. */
|
|
1295
|
+
const CHECKPOINT_MODES: readonly TurnRewindSettingsValue['turnCheckpointMode'][] = ['off', 'auto', 'git-native', 'legacy']
|
|
709
1296
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
strict: '严格',
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const POINT_KIND_LABELS: Readonly<Record<string, string>> = {
|
|
716
|
-
user: '手动',
|
|
717
|
-
rescue: '救援',
|
|
718
|
-
turn: '轮次',
|
|
719
|
-
}
|
|
1297
|
+
/** Checkpoint trust policies, in the order the select offers them. */
|
|
1298
|
+
const TRUST_MODES: readonly TurnRewindSettingsValue['turnCheckpointTrust'][] = ['fast', 'strict']
|
|
720
1299
|
|
|
721
1300
|
/** Settings card for the `turn-rewind` namespace: runtime options plus checkpoint management. */
|
|
722
1301
|
export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps): ReactNode {
|
|
1302
|
+
const t = useText()
|
|
723
1303
|
const snapshot = useSyncExternalStore(
|
|
724
1304
|
useCallback((notify: () => void) => scope?.subscribe(notify) ?? (() => {}), [scope]),
|
|
725
1305
|
useCallback(() => scope?.getSnapshot() ?? EMPTY_SETTINGS_SNAPSHOT, [scope]),
|
|
@@ -773,7 +1353,7 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
773
1353
|
}
|
|
774
1354
|
const parsed = Number.parseInt(draft, 10)
|
|
775
1355
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
776
|
-
setFormError(
|
|
1356
|
+
setFormError(t.positiveInteger(field))
|
|
777
1357
|
return
|
|
778
1358
|
}
|
|
779
1359
|
if (parsed === value?.[field]) return
|
|
@@ -820,8 +1400,8 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
820
1400
|
onClick={() => { setCardOpen(value => !value) }}
|
|
821
1401
|
>
|
|
822
1402
|
<span className="dcl-trs-card-heading">
|
|
823
|
-
<strong>
|
|
824
|
-
<span className="dcl-trs-card-description"
|
|
1403
|
+
<strong>{t.settingsCardTitle}</strong>
|
|
1404
|
+
<span className="dcl-trs-card-description">{t.settingsCardDescription}</span>
|
|
825
1405
|
</span>
|
|
826
1406
|
<svg className="dcl-trs-card-chevron" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
827
1407
|
<path d="M4.5 6.5 8 10l3.5-3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
@@ -831,70 +1411,70 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
831
1411
|
<div className="dcl-trs-card-body">
|
|
832
1412
|
<section className="dcl-trs-section">
|
|
833
1413
|
<div className="dcl-trs-section-title">
|
|
834
|
-
<strong
|
|
1414
|
+
<strong>{t.autoCheckpointSection}</strong>
|
|
835
1415
|
</div>
|
|
836
|
-
{snapshot.status === 'loading' && <p className="dcl-trs-status"
|
|
837
|
-
{snapshot.status === 'unavailable' && <p className="dcl-trs-status"
|
|
838
|
-
{snapshot.status === 'ready' && !snapshot.writable && <p className="dcl-trs-status"
|
|
1416
|
+
{snapshot.status === 'loading' && <p className="dcl-trs-status">{t.settingsLoading}</p>}
|
|
1417
|
+
{snapshot.status === 'unavailable' && <p className="dcl-trs-status">{t.settingsUnavailable}</p>}
|
|
1418
|
+
{snapshot.status === 'ready' && !snapshot.writable && <p className="dcl-trs-status">{t.settingsReadOnly}</p>}
|
|
839
1419
|
{value !== undefined && (
|
|
840
1420
|
<>
|
|
841
1421
|
<div className="dcl-trs-field">
|
|
842
|
-
<span className="dcl-trs-field-label"><strong
|
|
843
|
-
<span className="dcl-trs-field-desc"
|
|
1422
|
+
<span className="dcl-trs-field-label"><strong>{t.autoCheckpointLabel}</strong>
|
|
1423
|
+
<span className="dcl-trs-field-desc">{t.autoCheckpointDescription}</span></span>
|
|
844
1424
|
<span className="dcl-trs-field-control">
|
|
845
1425
|
<select
|
|
846
1426
|
value={value.turnCheckpointMode}
|
|
847
1427
|
disabled={!writable || busy !== null}
|
|
848
1428
|
onChange={(event) => { commitEnum('turnCheckpointMode', event.target.value) }}
|
|
849
1429
|
>
|
|
850
|
-
{
|
|
851
|
-
<option key={option} value={option}>{
|
|
1430
|
+
{CHECKPOINT_MODES.map((option) => (
|
|
1431
|
+
<option key={option} value={option}>{t.checkpointModes[option]}</option>
|
|
852
1432
|
))}
|
|
853
1433
|
</select>
|
|
854
|
-
{userLayer.turnCheckpointMode !== undefined && <span className="dcl-trs-override"
|
|
1434
|
+
{userLayer.turnCheckpointMode !== undefined && <span className="dcl-trs-override">{t.overridden}</span>}
|
|
855
1435
|
{userLayer.turnCheckpointMode !== undefined && (
|
|
856
1436
|
<Button variant="ghost" size="sm" disabled={!writable || busy !== null}
|
|
857
|
-
onClick={() => { setFormError(null); void scope?.unset('turnCheckpointMode').catch((caught: unknown) => { setFormError(messageOf(caught)) }) }}
|
|
1437
|
+
onClick={() => { setFormError(null); void scope?.unset('turnCheckpointMode').catch((caught: unknown) => { setFormError(messageOf(caught)) }) }}>{t.resetToDefault}</Button>
|
|
858
1438
|
)}
|
|
859
1439
|
</span>
|
|
860
1440
|
</div>
|
|
861
1441
|
<div className="dcl-trs-field">
|
|
862
|
-
<span className="dcl-trs-field-label"><strong
|
|
863
|
-
<span className="dcl-trs-field-desc"
|
|
1442
|
+
<span className="dcl-trs-field-label"><strong>{t.trustLabel}</strong>
|
|
1443
|
+
<span className="dcl-trs-field-desc">{t.trustDescription}</span></span>
|
|
864
1444
|
<span className="dcl-trs-field-control">
|
|
865
1445
|
<select
|
|
866
1446
|
value={value.turnCheckpointTrust}
|
|
867
1447
|
disabled={!writable || busy !== null}
|
|
868
1448
|
onChange={(event) => { commitEnum('turnCheckpointTrust', event.target.value) }}
|
|
869
1449
|
>
|
|
870
|
-
{
|
|
871
|
-
<option key={option} value={option}>{
|
|
1450
|
+
{TRUST_MODES.map((option) => (
|
|
1451
|
+
<option key={option} value={option}>{t.trustOptions[option]}</option>
|
|
872
1452
|
))}
|
|
873
1453
|
</select>
|
|
874
|
-
{userLayer.turnCheckpointTrust !== undefined && <span className="dcl-trs-override"
|
|
1454
|
+
{userLayer.turnCheckpointTrust !== undefined && <span className="dcl-trs-override">{t.overridden}</span>}
|
|
875
1455
|
{userLayer.turnCheckpointTrust !== undefined && (
|
|
876
1456
|
<Button variant="ghost" size="sm" disabled={!writable || busy !== null}
|
|
877
|
-
onClick={() => { setFormError(null); void scope?.unset('turnCheckpointTrust').catch((caught: unknown) => { setFormError(messageOf(caught)) }) }}
|
|
1457
|
+
onClick={() => { setFormError(null); void scope?.unset('turnCheckpointTrust').catch((caught: unknown) => { setFormError(messageOf(caught)) }) }}>{t.resetToDefault}</Button>
|
|
878
1458
|
)}
|
|
879
1459
|
</span>
|
|
880
1460
|
</div>
|
|
881
1461
|
{NUMBER_FIELDS.map((field) => (
|
|
882
|
-
<div className="dcl-trs-field" key={field
|
|
883
|
-
<span className="dcl-trs-field-label"><strong>{field.label}</strong>
|
|
884
|
-
<span className="dcl-trs-field-desc">{field.description}</span></span>
|
|
1462
|
+
<div className="dcl-trs-field" key={field}>
|
|
1463
|
+
<span className="dcl-trs-field-label"><strong>{t.numberFields[field].label}</strong>
|
|
1464
|
+
<span className="dcl-trs-field-desc">{t.numberFields[field].description}</span></span>
|
|
885
1465
|
<span className="dcl-trs-field-control">
|
|
886
1466
|
<input
|
|
887
1467
|
type="number" min={1} step={1}
|
|
888
|
-
value={drafts[field
|
|
1468
|
+
value={drafts[field] ?? String(value[field])}
|
|
889
1469
|
disabled={!writable || busy !== null}
|
|
890
|
-
onChange={(event) => { setDrafts((current) => ({ ...current, [field
|
|
891
|
-
onBlur={() => { commitNumber(field
|
|
892
|
-
onKeyDown={(event) => { if (event.key === 'Enter') commitNumber(field
|
|
1470
|
+
onChange={(event) => { setDrafts((current) => ({ ...current, [field]: event.target.value })) }}
|
|
1471
|
+
onBlur={() => { commitNumber(field) }}
|
|
1472
|
+
onKeyDown={(event) => { if (event.key === 'Enter') commitNumber(field) }}
|
|
893
1473
|
/>
|
|
894
|
-
{userLayer[field
|
|
895
|
-
{userLayer[field
|
|
1474
|
+
{userLayer[field] !== undefined && <span className="dcl-trs-override">{t.overridden}</span>}
|
|
1475
|
+
{userLayer[field] !== undefined && (
|
|
896
1476
|
<Button variant="ghost" size="sm" disabled={!writable || busy !== null}
|
|
897
|
-
onClick={() => { setFormError(null); void scope?.unset(field
|
|
1477
|
+
onClick={() => { setFormError(null); void scope?.unset(field).catch((caught: unknown) => { setFormError(messageOf(caught)) }) }}>{t.resetToDefault}</Button>
|
|
898
1478
|
)}
|
|
899
1479
|
</span>
|
|
900
1480
|
</div>
|
|
@@ -902,33 +1482,37 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
902
1482
|
</>
|
|
903
1483
|
)}
|
|
904
1484
|
{formError !== null && <p className="dcl-trs-error">{formError}</p>}
|
|
905
|
-
<p className="dcl-trs-storage"
|
|
1485
|
+
<p className="dcl-trs-storage">{t.storageDir(manage?.storageDir ?? '…')}</p>
|
|
906
1486
|
</section>
|
|
907
1487
|
<section className="dcl-trs-section">
|
|
908
1488
|
<div className="dcl-trs-section-title">
|
|
909
|
-
<strong
|
|
1489
|
+
<strong>{t.manageSection}</strong>
|
|
910
1490
|
<span className="dcl-trs-section-title-actions">
|
|
911
1491
|
<Button variant="outline" size="sm" onClick={() => { void refreshManage() }} disabled={manageLoading}>
|
|
912
|
-
{manageLoading ?
|
|
1492
|
+
{manageLoading ? t.refreshing : t.refresh}
|
|
913
1493
|
</Button>
|
|
914
1494
|
{manage !== null && manage.workspaces.length > 0 && (
|
|
915
1495
|
confirmClearAll
|
|
916
1496
|
? (
|
|
917
1497
|
<>
|
|
918
|
-
<Button variant="outline" size="sm" onClick={() => { setConfirmClearAll(false) }} disabled={busy !== null}
|
|
1498
|
+
<Button variant="outline" size="sm" onClick={() => { setConfirmClearAll(false) }} disabled={busy !== null}>{t.cancel}</Button>
|
|
919
1499
|
<Button variant="primary" size="sm" onClick={() => { void runManageAction({ action: 'clear-all' }, 'clear-all') }} disabled={busy !== null}>
|
|
920
|
-
{busy === 'clear-all' ?
|
|
1500
|
+
{busy === 'clear-all' ? t.clearing : t.confirmClearAll}
|
|
921
1501
|
</Button>
|
|
922
1502
|
</>
|
|
923
1503
|
)
|
|
924
|
-
: <Button variant="outline" size="sm" onClick={() => { setConfirmClearAll(true) }} disabled={busy !== null}
|
|
1504
|
+
: <Button variant="outline" size="sm" onClick={() => { setConfirmClearAll(true) }} disabled={busy !== null}>{t.clearAll}</Button>
|
|
925
1505
|
)}
|
|
926
1506
|
</span>
|
|
927
1507
|
</div>
|
|
928
1508
|
<p className="dcl-trs-manage-total">
|
|
929
1509
|
{manage === null
|
|
930
|
-
?
|
|
931
|
-
:
|
|
1510
|
+
? t.manageLoading
|
|
1511
|
+
: t.manageTotal(
|
|
1512
|
+
manage.workspaces.length,
|
|
1513
|
+
manage.totalBytes >= 0 ? manage.workspaces.reduce((total, workspace) => total + workspace.restorePoints.length, 0) : 0,
|
|
1514
|
+
formatBytes(manage.totalBytes),
|
|
1515
|
+
)}
|
|
932
1516
|
</p>
|
|
933
1517
|
{manageNotice !== null && <p className="dcl-trs-notice" data-warning={manageNotice.warning}>{manageNotice.message}</p>}
|
|
934
1518
|
{manageError !== null && <p className="dcl-trs-error">{manageError}</p>}
|
|
@@ -936,15 +1520,15 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
936
1520
|
<div className="dcl-trs-workspace" key={workspace.workspace}>
|
|
937
1521
|
<div className="dcl-trs-workspace-head">
|
|
938
1522
|
<span className="dcl-trs-workspace-path" title={workspace.workspace}>{workspace.workspace}</span>
|
|
939
|
-
<span className="dcl-trs-workspace-meta">{
|
|
940
|
-
{workspace.recoveryCount > 0 && <span className="dcl-trs-badge">{
|
|
1523
|
+
<span className="dcl-trs-workspace-meta">{t.workspacePoints(workspace.restorePoints.length)} · {formatBytes(workspace.totalBytes)}</span>
|
|
1524
|
+
{workspace.recoveryCount > 0 && <span className="dcl-trs-badge">{t.pendingRecoveries(workspace.recoveryCount)}</span>}
|
|
941
1525
|
<Button variant="ghost" size="sm" onClick={() => { toggleWorkspace(workspace.workspace) }}>
|
|
942
|
-
{collapsed.has(workspace.workspace) ?
|
|
1526
|
+
{collapsed.has(workspace.workspace) ? t.expand : t.collapse}
|
|
943
1527
|
</Button>
|
|
944
1528
|
<Button variant="outline" size="sm"
|
|
945
1529
|
onClick={() => { void runManageAction({ action: 'clear-workspace', workspace: workspace.workspace }, `clear:${workspace.workspace}`) }}
|
|
946
1530
|
disabled={busy !== null || workspace.restorePoints.length === 0}>
|
|
947
|
-
{busy === `clear:${workspace.workspace}` ?
|
|
1531
|
+
{busy === `clear:${workspace.workspace}` ? t.clearing : t.clearWorkspace}
|
|
948
1532
|
</Button>
|
|
949
1533
|
</div>
|
|
950
1534
|
{!collapsed.has(workspace.workspace) && workspace.restorePoints.length > 0 && (
|
|
@@ -952,14 +1536,14 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
952
1536
|
{workspace.restorePoints.map((point) => (
|
|
953
1537
|
<li className="dcl-trs-point" key={point.id}>
|
|
954
1538
|
<time>{formatTime(point.createdAt)}</time>
|
|
955
|
-
<span className="dcl-trs-point-kind">{
|
|
1539
|
+
<span className="dcl-trs-point-kind">{t.pointKinds[point.kind] ?? point.kind}</span>
|
|
956
1540
|
<span className="dcl-trs-point-size">{formatBytes(point.totalBytes)}</span>
|
|
957
|
-
<span className="dcl-trs-point-files">{
|
|
1541
|
+
<span className="dcl-trs-point-files">{t.fileCount(point.fileCount)}</span>
|
|
958
1542
|
{point.sessionId !== undefined && <code>{point.sessionId}</code>}
|
|
959
1543
|
<Button variant="ghost" size="sm"
|
|
960
1544
|
onClick={() => { void runManageAction({ action: 'delete', workspace: workspace.workspace, restorePointId: point.id }, `delete:${point.id}`) }}
|
|
961
1545
|
disabled={busy !== null}>
|
|
962
|
-
{busy === `delete:${point.id}` ?
|
|
1546
|
+
{busy === `delete:${point.id}` ? t.deleting : t.remove}
|
|
963
1547
|
</Button>
|
|
964
1548
|
</li>
|
|
965
1549
|
))}
|
|
@@ -967,7 +1551,7 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
967
1551
|
)}
|
|
968
1552
|
</div>
|
|
969
1553
|
))}
|
|
970
|
-
{manage !== null && manage.workspaces.length === 0 && <p className="dcl-trs-status"
|
|
1554
|
+
{manage !== null && manage.workspaces.length === 0 && <p className="dcl-trs-status">{t.noCheckpoints}</p>}
|
|
971
1555
|
</section>
|
|
972
1556
|
</div>
|
|
973
1557
|
)}
|
|
@@ -978,14 +1562,14 @@ export function TurnRewindSettingsCard({ scope }: TurnRewindSettingsCardProps):
|
|
|
978
1562
|
function decodeManageOverview(value: unknown): ManageOverview {
|
|
979
1563
|
const record = recordOf(value)
|
|
980
1564
|
const workspacesValue = record.workspaces
|
|
981
|
-
if (!Array.isArray(workspacesValue)) throw new Error(
|
|
1565
|
+
if (!Array.isArray(workspacesValue)) throw new Error(uiText().manageMissingWorkspaces)
|
|
982
1566
|
return {
|
|
983
1567
|
storageDir: requiredString(record.storageDir, 'storageDir'),
|
|
984
1568
|
totalBytes: requiredInteger(record.totalBytes, 'totalBytes'),
|
|
985
1569
|
workspaces: workspacesValue.map((entry) => {
|
|
986
1570
|
const workspace = recordOf(entry)
|
|
987
1571
|
const pointsValue = workspace.restorePoints
|
|
988
|
-
if (!Array.isArray(pointsValue)) throw new Error(
|
|
1572
|
+
if (!Array.isArray(pointsValue)) throw new Error(uiText().manageMissingRestorePoints)
|
|
989
1573
|
return {
|
|
990
1574
|
workspace: requiredString(workspace.workspace, 'workspace'),
|
|
991
1575
|
totalBytes: requiredInteger(workspace.totalBytes, 'totalBytes'),
|
|
@@ -1012,7 +1596,7 @@ function decodeManageActionNotice(value: unknown): ManageActionNotice {
|
|
|
1012
1596
|
const record = recordOf(value)
|
|
1013
1597
|
const action = requiredString(record.action, 'action')
|
|
1014
1598
|
if (action === 'clear-all') {
|
|
1015
|
-
if (!Array.isArray(record.reports)) throw new Error(
|
|
1599
|
+
if (!Array.isArray(record.reports)) throw new Error(uiText().clearMissingReports)
|
|
1016
1600
|
const totals = record.reports.reduce((current, entry) => {
|
|
1017
1601
|
const report = recordOf(entry)
|
|
1018
1602
|
return {
|
|
@@ -1024,14 +1608,14 @@ function decodeManageActionNotice(value: unknown): ManageActionNotice {
|
|
|
1024
1608
|
const warning = record.status === 'partial' || totals.retained > 0 || failures > 0
|
|
1025
1609
|
return {
|
|
1026
1610
|
warning,
|
|
1027
|
-
message:
|
|
1611
|
+
message: uiText().clearedAll(totals.deleted, totals.retained, failures),
|
|
1028
1612
|
}
|
|
1029
1613
|
}
|
|
1030
1614
|
const deleted = requiredInteger(record.deletedRestorePoints, 'deletedRestorePoints')
|
|
1031
1615
|
const retained = requiredInteger(record.retainedRestorePoints, 'retainedRestorePoints')
|
|
1032
1616
|
return {
|
|
1033
1617
|
warning: retained > 0,
|
|
1034
|
-
message:
|
|
1618
|
+
message: uiText().cleared(deleted, retained),
|
|
1035
1619
|
}
|
|
1036
1620
|
}
|
|
1037
1621
|
|
|
@@ -1059,16 +1643,16 @@ function decodePreview(value: unknown): Preview {
|
|
|
1059
1643
|
if (status === 'pending' || status === 'missing') return { status }
|
|
1060
1644
|
if (status === 'skipped') return { status, reason: requiredString(record.reason, 'reason') }
|
|
1061
1645
|
if (status === 'failed') return { status, error: requiredString(record.error, 'error') }
|
|
1062
|
-
if (status !== 'ready') throw new Error(
|
|
1646
|
+
if (status !== 'ready') throw new Error(uiText().unknownStatus(status))
|
|
1063
1647
|
const changesValue = record.changes
|
|
1064
|
-
if (!Array.isArray(changesValue)) throw new Error(
|
|
1648
|
+
if (!Array.isArray(changesValue)) throw new Error(uiText().previewMissingChanges)
|
|
1065
1649
|
const changes = changesValue.map((entry) => {
|
|
1066
1650
|
const change = recordOf(entry)
|
|
1067
1651
|
return { path: requiredString(change.path, 'path'), kind: requiredString(change.kind, 'kind') as ChangeKind }
|
|
1068
1652
|
})
|
|
1069
1653
|
const activeSessionIdsValue = record.activeSessionIds
|
|
1070
1654
|
if (!Array.isArray(activeSessionIdsValue) || !activeSessionIdsValue.every(value => typeof value === 'string')) {
|
|
1071
|
-
throw new Error(
|
|
1655
|
+
throw new Error(uiText().previewMissingActiveSessionIds)
|
|
1072
1656
|
}
|
|
1073
1657
|
return {
|
|
1074
1658
|
status,
|
|
@@ -1185,17 +1769,22 @@ function samePortalTargets(
|
|
|
1185
1769
|
})
|
|
1186
1770
|
}
|
|
1187
1771
|
|
|
1188
|
-
async function openSessionWithDraft(ctx: ClientContextLike, sessionId: string, promptText: string): Promise<void> {
|
|
1189
|
-
|
|
1772
|
+
export async function openSessionWithDraft(ctx: ClientContextLike, sessionId: string, promptText: string): Promise<void> {
|
|
1773
|
+
if (typeof ctx.sessions.open === 'function') ctx.sessions.open(sessionId)
|
|
1774
|
+
else {
|
|
1775
|
+
const navigation = ctx.get?.('uiWorkspace') as { openSession(id: string): void } | undefined
|
|
1776
|
+
if (navigation === undefined) throw new Error(uiText().sessionNotReady)
|
|
1777
|
+
navigation.openSession(sessionId)
|
|
1778
|
+
}
|
|
1779
|
+
let lastError: unknown = new Error(uiText().sessionNotReady)
|
|
1190
1780
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
1191
1781
|
try {
|
|
1192
|
-
ctx.sessions.open(sessionId)
|
|
1193
1782
|
const scope = ctx.sessions.scope(sessionId)
|
|
1194
1783
|
if (scope !== undefined) {
|
|
1195
1784
|
ctx.conversation.input.for(scope).setDraft(promptText)
|
|
1196
1785
|
return
|
|
1197
1786
|
}
|
|
1198
|
-
lastError = new Error(
|
|
1787
|
+
lastError = new Error(uiText().sessionNotReady)
|
|
1199
1788
|
} catch (error) {
|
|
1200
1789
|
lastError = error
|
|
1201
1790
|
}
|
|
@@ -1221,7 +1810,7 @@ export async function responseJson(response: Response): Promise<unknown> {
|
|
|
1221
1810
|
} catch {
|
|
1222
1811
|
throw new RewindRequestError(
|
|
1223
1812
|
response.ok ? 'REWIND_INVALID_RESPONSE' : 'REWIND_ENDPOINT_UNAVAILABLE',
|
|
1224
|
-
|
|
1813
|
+
uiText().unparsableResponse(String(response.status)),
|
|
1225
1814
|
)
|
|
1226
1815
|
}
|
|
1227
1816
|
const record = value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -1232,11 +1821,11 @@ export async function responseJson(response: Response): Promise<unknown> {
|
|
|
1232
1821
|
typeof record?.code === 'string'
|
|
1233
1822
|
? record.code
|
|
1234
1823
|
: response.status === 404 ? 'REWIND_ENDPOINT_UNAVAILABLE' : 'REWIND_FAILED',
|
|
1235
|
-
typeof record?.error === 'string' ? record.error :
|
|
1824
|
+
typeof record?.error === 'string' ? record.error : uiText().requestFailed(String(response.status)),
|
|
1236
1825
|
)
|
|
1237
1826
|
}
|
|
1238
1827
|
if (value === undefined) {
|
|
1239
|
-
throw new RewindRequestError('REWIND_INVALID_RESPONSE',
|
|
1828
|
+
throw new RewindRequestError('REWIND_INVALID_RESPONSE', uiText().emptyResponse)
|
|
1240
1829
|
}
|
|
1241
1830
|
return value
|
|
1242
1831
|
}
|
|
@@ -1248,22 +1837,22 @@ class RewindRequestError extends Error {
|
|
|
1248
1837
|
}
|
|
1249
1838
|
|
|
1250
1839
|
function recordOf(value: unknown): Record<string, unknown> {
|
|
1251
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(
|
|
1840
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(uiText().invalidObject)
|
|
1252
1841
|
return value as Record<string, unknown>
|
|
1253
1842
|
}
|
|
1254
1843
|
|
|
1255
1844
|
function requiredString(value: unknown, name: string): string {
|
|
1256
|
-
if (typeof value !== 'string' || value === '') throw new Error(
|
|
1845
|
+
if (typeof value !== 'string' || value === '') throw new Error(uiText().invalidField(name))
|
|
1257
1846
|
return value
|
|
1258
1847
|
}
|
|
1259
1848
|
|
|
1260
1849
|
function requiredInteger(value: unknown, name: string): number {
|
|
1261
|
-
if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error(
|
|
1850
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error(uiText().invalidField(name))
|
|
1262
1851
|
return value as number
|
|
1263
1852
|
}
|
|
1264
1853
|
|
|
1265
1854
|
function requiredBoolean(value: unknown, name: string): boolean {
|
|
1266
|
-
if (typeof value !== 'boolean') throw new Error(
|
|
1855
|
+
if (typeof value !== 'boolean') throw new Error(uiText().invalidField(name))
|
|
1267
1856
|
return value
|
|
1268
1857
|
}
|
|
1269
1858
|
|
|
@@ -1275,13 +1864,7 @@ function optionalRecordString(record: Record<string, unknown>, name: string): Re
|
|
|
1275
1864
|
|
|
1276
1865
|
/** Describe the user-visible result of restoring one changed file. */
|
|
1277
1866
|
export function fileRecoveryLabel(kind: ChangeKind): string {
|
|
1278
|
-
|
|
1279
|
-
case 'added': return '移除后来新增的文件'
|
|
1280
|
-
case 'deleted': return '找回文件'
|
|
1281
|
-
case 'modified': return '恢复之前的版本'
|
|
1282
|
-
case 'mode-changed': return '恢复文件权限'
|
|
1283
|
-
case 'type-changed': return '恢复之前的文件类型'
|
|
1284
|
-
}
|
|
1867
|
+
return uiText().fileKinds[kind]
|
|
1285
1868
|
}
|
|
1286
1869
|
|
|
1287
1870
|
function RewindIcon({ size }: { readonly size: number }): ReactNode {
|
|
@@ -1294,25 +1877,7 @@ function RewindIcon({ size }: { readonly size: number }): ReactNode {
|
|
|
1294
1877
|
|
|
1295
1878
|
function friendlyError(error: unknown): string {
|
|
1296
1879
|
if (!(error instanceof RewindRequestError)) return messageOf(error)
|
|
1297
|
-
|
|
1298
|
-
case 'REWIND_ENDPOINT_UNAVAILABLE': return '回退服务没有响应。请确认 @anionex/dsh-turn-rewind 已挂载到当前 DSH,并重启 DSH 后重试。'
|
|
1299
|
-
case 'REWIND_INVALID_RESPONSE': return '回退服务返回了无法解析的内容。请重启 DSH 后重试。'
|
|
1300
|
-
case 'PLAN_STALE': return '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。'
|
|
1301
|
-
case 'PLAN_STALE_WORKSPACE': return '项目文件在检查后又发生了变化。为避免覆盖新修改,请重新检查后再恢复。'
|
|
1302
|
-
case 'PLAN_STALE_REPOSITORY': return 'Git 状态在检查后又发生了变化,恢复已失效。请重新检查后再试。'
|
|
1303
|
-
case 'WORKSPACE_IN_USE': return '这个项目目录还有别的对话正在运行。请等那些对话结束或停止后,再重新检查。'
|
|
1304
|
-
case 'WORKSPACE_LOCKED': return '另一个恢复操作正在处理这个项目目录。请等待它完成后重新检查。'
|
|
1305
|
-
case 'WORKSPACE_CHANGED': return '这个项目目录已经不在原来的位置,旧的回退点无法再使用。'
|
|
1306
|
-
case 'WORKSPACE_MODE_CHANGED': return '这个项目目录的工作区类型变了(例如从普通目录变成了 Git 仓库)。旧的回退点不再适用,请重新发送一条消息生成新的回退点。'
|
|
1307
|
-
case 'HEAD_CHANGED': return '项目的提交或分支已发生变化。为避免覆盖新改动,请重新检查后再恢复。'
|
|
1308
|
-
case 'REPOSITORY_CHANGED': return '这个项目目录已不属于原来的 Git 工作区,无法恢复。'
|
|
1309
|
-
case 'GIT_OPERATION_CHANGED': return 'Git 正在执行其他操作。请先完成或取消该操作,再重新检查。'
|
|
1310
|
-
case 'RESTORE_POINT_NOT_FOUND': return '没有找到对应的文件状态,可能已被清理。'
|
|
1311
|
-
case 'NO_CHANGES': return '项目文件已经是这条消息发送前的状态,无需恢复文件。可选择「只回溯消息」重新开始这段对话。'
|
|
1312
|
-
case 'RESTORE_FAILED_ROLLED_BACK': return '恢复未能完成,项目文件已自动还原到操作前的状态。'
|
|
1313
|
-
case 'CONVERSATION_REWIND_FAILED': return '文件已恢复,但无法创建新对话;项目文件已自动还原。'
|
|
1314
|
-
default: return error.message
|
|
1315
|
-
}
|
|
1880
|
+
return uiText().errors[error.code] ?? error.message
|
|
1316
1881
|
}
|
|
1317
1882
|
|
|
1318
1883
|
/**
|
|
@@ -1326,15 +1891,9 @@ function friendlyError(error: unknown): string {
|
|
|
1326
1891
|
export function describeCaptureNotice(ready: ReadyPreview | null): string | null {
|
|
1327
1892
|
if (ready === null) return null
|
|
1328
1893
|
const example = (ready.skipped ?? [])[0]?.path
|
|
1329
|
-
if (ready.captureTruncated === 'snapshot-limit')
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (ready.captureTruncated === 'file-limit') {
|
|
1333
|
-
return '本轮文件数量超过上限,多出来的文件没有纳入检查点。恢复时不会改动它们。'
|
|
1334
|
-
}
|
|
1335
|
-
if (ready.skippedCount > 0) {
|
|
1336
|
-
return `有 ${String(ready.skippedCount)} 个文件因为超过单文件大小上限或类型不受支持,没有纳入检查点${example === undefined ? '' : `(例如 ${example})`}。恢复时不会改动它们。`
|
|
1337
|
-
}
|
|
1894
|
+
if (ready.captureTruncated === 'snapshot-limit') return uiText().captureSnapshotLimit
|
|
1895
|
+
if (ready.captureTruncated === 'file-limit') return uiText().captureFileLimit
|
|
1896
|
+
if (ready.skippedCount > 0) return uiText().captureSkipped(ready.skippedCount, example)
|
|
1338
1897
|
return null
|
|
1339
1898
|
}
|
|
1340
1899
|
|
|
@@ -1349,16 +1908,11 @@ export function describeCaptureNotice(ready: ReadyPreview | null): string | null
|
|
|
1349
1908
|
*/
|
|
1350
1909
|
export function explainCheckpointSkip(reason: string): string {
|
|
1351
1910
|
const code = /^\[([A-Z_]+)\]/.exec(reason)?.[1]
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
if (code === '
|
|
1356
|
-
|
|
1357
|
-
}
|
|
1358
|
-
if (code === 'TURN_CHECKPOINT_NEW_CONTENT_LIMIT') {
|
|
1359
|
-
return '这一轮新增的内容超过检查点预算,没有保存文件快照。仍可只回溯消息。'
|
|
1360
|
-
}
|
|
1361
|
-
return `本轮没有保存文件检查点:${reason}仍可只回溯消息。`
|
|
1911
|
+
const t = uiText()
|
|
1912
|
+
if (code === 'TURN_CHECKPOINT_TIMEOUT') return t.skipTimeout
|
|
1913
|
+
if (code === 'TURN_CHECKPOINT_DISABLED') return t.skipDisabled
|
|
1914
|
+
if (code === 'TURN_CHECKPOINT_NEW_CONTENT_LIMIT') return t.skipNewContentLimit
|
|
1915
|
+
return t.skipOther(reason)
|
|
1362
1916
|
}
|
|
1363
1917
|
|
|
1364
1918
|
/**
|
|
@@ -1372,23 +1926,22 @@ export function explainCheckpointSkip(reason: string): string {
|
|
|
1372
1926
|
*/
|
|
1373
1927
|
export function explainCheckpointFailure(message: string): string {
|
|
1374
1928
|
const code = /^\[([A-Z_]+)\]/.exec(message)?.[1]
|
|
1929
|
+
const t = uiText()
|
|
1375
1930
|
switch (code) {
|
|
1376
1931
|
case 'GIT_COMMAND_FAILED':
|
|
1377
|
-
return /not a git repository/i.test(message)
|
|
1378
|
-
? '这个项目目录不是 Git 仓库,回退功能无法保存文件检查点。仍可只回溯消息。'
|
|
1379
|
-
: `无法读取这个项目目录的 Git 状态:${message}仍可只回溯消息。`
|
|
1932
|
+
return /not a git repository/i.test(message) ? t.failureNotGitRepository : t.failureGitStatus(message)
|
|
1380
1933
|
case 'FILE_TOO_LARGE':
|
|
1381
1934
|
case 'SNAPSHOT_TOO_LARGE':
|
|
1382
1935
|
case 'TOO_MANY_FILES':
|
|
1383
|
-
return
|
|
1936
|
+
return t.failureSizeOrCount
|
|
1384
1937
|
case 'UNSUPPORTED_FILE_TYPE':
|
|
1385
|
-
return
|
|
1938
|
+
return t.failureUnsupportedType
|
|
1386
1939
|
case 'IGNORE_FILE_INVALID':
|
|
1387
|
-
return
|
|
1940
|
+
return t.failureIgnoreFileInvalid
|
|
1388
1941
|
case 'INVALID_PATH':
|
|
1389
|
-
return
|
|
1942
|
+
return t.failureInvalidPath
|
|
1390
1943
|
default:
|
|
1391
|
-
return
|
|
1944
|
+
return t.failureOther(message)
|
|
1392
1945
|
}
|
|
1393
1946
|
}
|
|
1394
1947
|
|