dsh-file-history 0.2.2 → 0.2.3
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/README.md +11 -1
- package/lib/client.js +289 -37
- package/lib/index.js +28 -2
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
装进 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 之后,AI 每回应一次,插件把这一轮改过的文件整理成卡片贴在该回复下方:改了哪些文件、增删多少行,点文件名看 diff,点按钮还原。
|
|
6
6
|
|
|
7
|
+
## 它做什么
|
|
8
|
+
|
|
9
|
+
每一轮对话结束,插件把整个工作区拍一张快照。不是只记 AI 调用过的那次写入,而是这一轮前后磁盘上真实的样子——AI 用脚本绕过去改的文件同样在里面。快照存在 `~/.dsh/file-history/` 下,不进你的项目目录,也不经过你的 git:项目里没有 `.git` 一样能用,有也不会被读写。
|
|
10
|
+
|
|
11
|
+
于是每条真正改了文件的回复下面多一张卡片:这一轮动了哪些文件、各增删多少行,点文件名看这一轮具体改了什么。最下面四个按钮——只还原文件、只还原对话、两者一起还原、从这一轮开个分支。还原就是拿回这一轮开始前的内容,改错了当没说;还原本身也能撤销。侧栏另有一条时间线,把每轮和你手动标的检查点排在一起,可以跳到任意一个退回去;输入框上方只在工作区跟你标的检查点有差异时出现一条,写明差多少,一键退回。
|
|
12
|
+
|
|
13
|
+
用的东西不陌生:VS Code 侧边栏那个「时间线」按时间列出同一个文件被改过的每个版本,随时退回去。这插件做的是同一件事,只是单位从「一次保存」换成「一轮对话」,退的时候可以连对话一起退,整件事都在对话框里做完,不用切到编辑器、不用翻 git 日志。
|
|
14
|
+
|
|
15
|
+
它不做代码审查,也不替代 git:项目里的提交、分支、暂存区一概不碰。
|
|
16
|
+
|
|
7
17
|
## 安装
|
|
8
18
|
|
|
9
19
|
```bash
|
|
@@ -52,7 +62,7 @@ dsh plugin --profile web remove dsh-file-history
|
|
|
52
62
|
|
|
53
63
|
## 配置
|
|
54
64
|
|
|
55
|
-
写在 profile 的 `cordis.patch.yml` 里,示例见 `config.example.yml
|
|
65
|
+
写在 profile 的 `cordis.patch.yml` 里,示例见 `config.example.yml`。这些值也能在**设置 → 插件 → 文件历史**里直接改:三个界面开关和几个数量上限都在那一张卡里,改完下一轮就生效,不用重启。
|
|
56
66
|
|
|
57
67
|
```yaml
|
|
58
68
|
- insert:
|
package/lib/client.js
CHANGED
|
@@ -5,13 +5,16 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
7
7
|
var React = require('react')
|
|
8
|
-
//
|
|
9
|
-
var
|
|
8
|
+
// 图标和设置页的开关都来自官方控件集;模块表里没有时留空,各自回落到自带的画法。
|
|
9
|
+
var primitives = null
|
|
10
10
|
try {
|
|
11
|
-
|
|
11
|
+
primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
12
12
|
} catch (error) {
|
|
13
|
-
|
|
13
|
+
primitives = null
|
|
14
14
|
}
|
|
15
|
+
var IconClock = primitives && primitives.IconClockOutline16 || null
|
|
16
|
+
var IconChevron = primitives && primitives.IconChevronDownOutline14 || null
|
|
17
|
+
var Switch = primitives && primitives.Switch || null
|
|
15
18
|
var h = React.createElement
|
|
16
19
|
var CHANNEL = '/dsh-file-history'
|
|
17
20
|
var NS = 'file-history'
|
|
@@ -196,6 +199,32 @@ window.__ModuleLoader__.load({
|
|
|
196
199
|
'.fh-card-chk{border-color:var(--dsw-alias-border-l2);box-shadow:inset 2px 0 0 var(--dsw-alias-state-warn-primary)}',
|
|
197
200
|
// 「当前」是自己的一格:手改出来、还没被任何轮次或检查点记下来的那一版,同样按重的处理。
|
|
198
201
|
'.fh-card-now{border-color:var(--dsw-alias-border-l2);box-shadow:inset 2px 0 0 var(--dsw-alias-state-business-primary)}',
|
|
202
|
+
// 设置页那张卡照官方那张插件卡来:默认折叠,点标题展开,展开的是一行一个 field。
|
|
203
|
+
'.fh-set{border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s}',
|
|
204
|
+
'.fh-set:hover{border-color:var(--dsw-alias-label-dimmed)}',
|
|
205
|
+
'.fh-set-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',
|
|
206
|
+
'.fh-set-head{appearance:none;width:100%;padding:14px 16px;display:flex;align-items:center;gap:12px;border:0;border-radius:12px;background:0 0;color:inherit;font:inherit;text-align:left;cursor:pointer}',
|
|
207
|
+
'.fh-set-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}',
|
|
208
|
+
'.fh-set-headText{display:flex;flex-direction:column;flex:1;gap:4px;min-width:0}',
|
|
209
|
+
'.fh-set-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}',
|
|
210
|
+
'.fh-set-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}',
|
|
211
|
+
'.fh-chev{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}',
|
|
212
|
+
'.fh-chev-open{transform:rotate(180deg)}',
|
|
213
|
+
'.fh-set-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}',
|
|
214
|
+
'.fh-set-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}',
|
|
215
|
+
'.fh-set-field+.fh-set-field{border-top:.5px solid var(--dsw-alias-border-l2)}',
|
|
216
|
+
'.fh-set-row{display:flex;align-items:center;gap:8px}',
|
|
217
|
+
'.fh-set-label{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}',
|
|
218
|
+
'.fh-set-hint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}',
|
|
219
|
+
'.fh-set-input{display:flex;align-items:center;gap:6px}',
|
|
220
|
+
'.fh-set-num{width:88px;height:34px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:1.5;text-align:right}',
|
|
221
|
+
'.fh-set-num:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}',
|
|
222
|
+
'.fh-set-unit{flex:none;width:30px;color:var(--dsw-alias-label-tertiary);font-size:12px}',
|
|
223
|
+
'.fh-sw{position:relative;flex:none;width:32px;height:18px;padding:0;border:none;border-radius:9px;background:var(--dsw-alias-border-l2);cursor:pointer;transition:background .15s}',
|
|
224
|
+
'.fh-sw-on{background:var(--dsw-alias-state-business-primary)}',
|
|
225
|
+
'.fh-sw:disabled{cursor:default;opacity:.45}',
|
|
226
|
+
'.fh-sw-dot{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-bg-layer-1);transition:left .15s}',
|
|
227
|
+
'.fh-sw-on .fh-sw-dot{left:16px}',
|
|
199
228
|
'.fh-note{color:var(--dsw-alias-label-tertiary)}',
|
|
200
229
|
'.fh-error{color:var(--dsw-alias-state-error-primary)}',
|
|
201
230
|
'.fh-ok{color:var(--dsw-alias-state-success-primary)}',
|
|
@@ -297,7 +326,28 @@ window.__ModuleLoader__.load({
|
|
|
297
326
|
tooBig: '太大没记:',
|
|
298
327
|
crossLineHint: '父分支的轮次只能查看,要动它请从上方那张卡跳过去',
|
|
299
328
|
elsewhereTitle: '文件现在停在另一条分支上',
|
|
300
|
-
elsewhereGo: '跳转到该分支'
|
|
329
|
+
elsewhereGo: '跳转到该分支',
|
|
330
|
+
setTurnCard: '轮次卡片',
|
|
331
|
+
setDock: '改动条',
|
|
332
|
+
setTimeline: '侧栏时间线',
|
|
333
|
+
setKeep: '保留最近轮次',
|
|
334
|
+
setFileSize: '单个文件上限',
|
|
335
|
+
setRepoFiles: '单个仓库文件数',
|
|
336
|
+
setTotal: '快照总量上限',
|
|
337
|
+
setDesc: '每轮对话结束后给整个工作区拍一张快照,这里管它的开关和上限。',
|
|
338
|
+
hintTurnCard: '每轮对话结束,在那条回复下面列出改动的文件。',
|
|
339
|
+
hintDock: '输入框上方那条提示,只在工作区和检查点有差别时出现。',
|
|
340
|
+
hintTimeline: '右侧栏里把轮次和检查点排成一条线。',
|
|
341
|
+
hintKeep: '超过这个数目的旧轮次会被清理,检查点不受影响。',
|
|
342
|
+
hintFileSize: '超过这个大小的文件不记内容,只在卡片里标出来。',
|
|
343
|
+
hintRepoFiles: '单个内嵌的仓库文件比这多,就整块跳过。',
|
|
344
|
+
hintTotal: '快照总共占多少盘,超了先清最旧的轮次。',
|
|
345
|
+
hintPrune: '每设一个检查点,就把走到它的轮次清掉。',
|
|
346
|
+
setPrune: '检查点后清理到',
|
|
347
|
+
setUnitTurn: '轮',
|
|
348
|
+
setUnitPiece: '个',
|
|
349
|
+
setUnitMB: 'MB',
|
|
350
|
+
saveFailed: '没存上'
|
|
301
351
|
}
|
|
302
352
|
|
|
303
353
|
var EN = {
|
|
@@ -355,7 +405,28 @@ window.__ModuleLoader__.load({
|
|
|
355
405
|
tooBig: 'Too large to record:',
|
|
356
406
|
crossLineHint: 'Turns from a parent branch are read-only — jump to that branch to change anything',
|
|
357
407
|
elsewhereTitle: 'The files now sit on another branch',
|
|
358
|
-
elsewhereGo: 'Go to that branch'
|
|
408
|
+
elsewhereGo: 'Go to that branch',
|
|
409
|
+
setTurnCard: 'Turn card',
|
|
410
|
+
setDock: 'Change bar',
|
|
411
|
+
setTimeline: 'Sidebar timeline',
|
|
412
|
+
setKeep: 'Keep recent turns',
|
|
413
|
+
setFileSize: 'Max file size',
|
|
414
|
+
setRepoFiles: 'Max files per repository',
|
|
415
|
+
setTotal: 'Max snapshot size',
|
|
416
|
+
setDesc: 'A snapshot of the whole workspace after each turn; its switches and limits live here.',
|
|
417
|
+
hintTurnCard: 'Lists the changed files under each reply, once the turn ends.',
|
|
418
|
+
hintDock: 'The bar above the input, shown only when the workspace differs from the checkpoint.',
|
|
419
|
+
hintTimeline: 'Turns and checkpoints as one line in the sidebar.',
|
|
420
|
+
hintKeep: 'Turns past this count get pruned; checkpoints stay.',
|
|
421
|
+
hintFileSize: 'Files larger than this are listed but not stored.',
|
|
422
|
+
hintRepoFiles: 'An embedded repository with more files than this is skipped whole.',
|
|
423
|
+
hintTotal: 'Disk the snapshots may use; the oldest turns go first.',
|
|
424
|
+
hintPrune: 'Each checkpoint prunes the turns leading up to it.',
|
|
425
|
+
setPrune: 'Prune to checkpoint',
|
|
426
|
+
setUnitTurn: 'turns',
|
|
427
|
+
setUnitPiece: 'files',
|
|
428
|
+
setUnitMB: 'MB',
|
|
429
|
+
saveFailed: 'Not saved'
|
|
359
430
|
}
|
|
360
431
|
|
|
361
432
|
function Entry(props) {
|
|
@@ -952,6 +1023,160 @@ window.__ModuleLoader__.load({
|
|
|
952
1023
|
onChanged: function () { reloadSession(call, sessionId) }
|
|
953
1024
|
})
|
|
954
1025
|
}
|
|
1026
|
+
// 设置页里的那张卡。命名空间跟宿主注册的那个一样,值也只有一份:
|
|
1027
|
+
// 卡片改完,宿主那边下一轮读到的就是新值,不必重启。
|
|
1028
|
+
var SETTINGS_NS = 'dsh-file-history'
|
|
1029
|
+
// 按常用程度从上往下排:三个界面开关在前,数量类在后。
|
|
1030
|
+
// factor 是把界面上显示的单位换成存储单位的倍数——字节的东西按 MB 给人看。
|
|
1031
|
+
var FIELDS = [
|
|
1032
|
+
{ key: 'turnCard', type: 'bool', label: 'setTurnCard', hint: 'hintTurnCard' },
|
|
1033
|
+
{ key: 'dock', type: 'bool', label: 'setDock', hint: 'hintDock' },
|
|
1034
|
+
{ key: 'timeline', type: 'bool', label: 'setTimeline', hint: 'hintTimeline' },
|
|
1035
|
+
{ key: 'keepRecentTurns', type: 'num', label: 'setKeep', hint: 'hintKeep', unit: 'setUnitTurn', min: 0, max: 1000, step: 1 },
|
|
1036
|
+
{ key: 'maxFileBytes', type: 'num', label: 'setFileSize', hint: 'hintFileSize', unit: 'setUnitMB', min: 0, max: 1024, step: 1, factor: 1048576 },
|
|
1037
|
+
{ key: 'maxRepoFiles', type: 'num', label: 'setRepoFiles', hint: 'hintRepoFiles', unit: 'setUnitPiece', min: 0, max: 1000000, step: 1000 },
|
|
1038
|
+
{ key: 'maxBytes', type: 'num', label: 'setTotal', hint: 'hintTotal', unit: 'setUnitMB', min: 0, max: 1048576, step: 256, factor: 1048576 },
|
|
1039
|
+
{ key: 'pruneAfterCheckpoints', type: 'num', label: 'setPrune', hint: 'hintPrune', unit: 'setUnitPiece', min: 0, max: 100, step: 1 }
|
|
1040
|
+
]
|
|
1041
|
+
|
|
1042
|
+
// 快照是客户端这边的镜像:订阅一次,宿主一提交再取一份。引用没变就不重渲染。
|
|
1043
|
+
function useSettingsSnapshot(scope) {
|
|
1044
|
+
var pair = React.useState(function () { return scope ? scope.getSnapshot() : null })
|
|
1045
|
+
var snapshot = pair[0]
|
|
1046
|
+
var setSnapshot = pair[1]
|
|
1047
|
+
React.useEffect(function () {
|
|
1048
|
+
if (!scope) return undefined
|
|
1049
|
+
setSnapshot(scope.getSnapshot())
|
|
1050
|
+
return scope.subscribe(function () { setSnapshot(scope.getSnapshot()) })
|
|
1051
|
+
}, [scope])
|
|
1052
|
+
return snapshot
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 读不到(还没送到、或这边没有设置通道)就当开着:插件默认是三个界面都在。
|
|
1056
|
+
function flagOf(snapshot, key) {
|
|
1057
|
+
if (!snapshot || snapshot.status !== 'ready' || !snapshot.value) return true
|
|
1058
|
+
return snapshot.value[key] !== false
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// 关掉时不光不渲染,连注册一起撤掉:留个空槽比不注册更难看。开关一变就重挂一次。
|
|
1062
|
+
function whenShown(scope, key, setup) {
|
|
1063
|
+
var dispose = null
|
|
1064
|
+
function sync() {
|
|
1065
|
+
var on = scope === null ? true : flagOf(scope.getSnapshot(), key)
|
|
1066
|
+
if (on && dispose === null) {
|
|
1067
|
+
dispose = setup() || function () {}
|
|
1068
|
+
} else if (!on && dispose !== null) {
|
|
1069
|
+
var stop = dispose
|
|
1070
|
+
dispose = null
|
|
1071
|
+
stop()
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
sync()
|
|
1075
|
+
var off = scope === null ? null : scope.subscribe(sync)
|
|
1076
|
+
return function () {
|
|
1077
|
+
if (off !== null) off()
|
|
1078
|
+
if (dispose !== null) dispose()
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function BoolRow(props) {
|
|
1083
|
+
var t = props.t
|
|
1084
|
+
var failed = React.useState(false)
|
|
1085
|
+
var busy = React.useState(false)
|
|
1086
|
+
function flip(next) {
|
|
1087
|
+
busy[1](true)
|
|
1088
|
+
failed[1](false)
|
|
1089
|
+
Promise.resolve(props.scope.set(props.field.key, typeof next === 'boolean' ? next : !props.value))
|
|
1090
|
+
.catch(function () { failed[1](true) })
|
|
1091
|
+
.then(function () { busy[1](false) })
|
|
1092
|
+
}
|
|
1093
|
+
return h('div', { className: 'fh-set-field' },
|
|
1094
|
+
h('div', { className: 'fh-set-row' },
|
|
1095
|
+
h('span', { className: 'fh-set-label' }, t(props.field.label)),
|
|
1096
|
+
Switch
|
|
1097
|
+
? h(Switch, { checked: props.value, disabled: busy[0], label: t(props.field.label), onChange: flip })
|
|
1098
|
+
: h('button', {
|
|
1099
|
+
type: 'button',
|
|
1100
|
+
role: 'switch',
|
|
1101
|
+
'aria-checked': props.value,
|
|
1102
|
+
className: 'fh-sw' + (props.value ? ' fh-sw-on' : ''),
|
|
1103
|
+
disabled: busy[0],
|
|
1104
|
+
onClick: function () { flip(!props.value) }
|
|
1105
|
+
}, h('span', { className: 'fh-sw-dot' })),
|
|
1106
|
+
failed[0] ? h('span', { className: 'fh-error' }, t('saveFailed')) : null),
|
|
1107
|
+
h('p', { className: 'fh-set-hint' }, t(props.field.hint)))
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function NumRow(props) {
|
|
1111
|
+
var t = props.t
|
|
1112
|
+
var field = props.field
|
|
1113
|
+
var factor = field.factor || 1
|
|
1114
|
+
var shown = typeof props.value === 'number' ? Math.round(props.value / factor) : ''
|
|
1115
|
+
// 输入框只在编辑时用草稿:不编辑的时候永远显示宿主那一份,别人改了也会跟着动。
|
|
1116
|
+
var draft = React.useState(null)
|
|
1117
|
+
var failed = React.useState(false)
|
|
1118
|
+
function commit() {
|
|
1119
|
+
var text = draft[0]
|
|
1120
|
+
if (text === null) return
|
|
1121
|
+
draft[1](null)
|
|
1122
|
+
var parsed = Number(String(text).trim())
|
|
1123
|
+
if (String(text).trim() === '' || !isFinite(parsed)) return
|
|
1124
|
+
var real = Math.round(parsed) * factor
|
|
1125
|
+
if (real < field.min * factor) real = field.min * factor
|
|
1126
|
+
if (real > field.max * factor) real = field.max * factor
|
|
1127
|
+
if (real === props.value) return
|
|
1128
|
+
failed[1](false)
|
|
1129
|
+
Promise.resolve(props.scope.set(field.key, real)).catch(function () { failed[1](true) })
|
|
1130
|
+
}
|
|
1131
|
+
return h('div', { className: 'fh-set-field' },
|
|
1132
|
+
h('div', { className: 'fh-set-row' },
|
|
1133
|
+
h('span', { className: 'fh-set-label' }, t(field.label)),
|
|
1134
|
+
h('span', { className: 'fh-set-input' },
|
|
1135
|
+
h('input', {
|
|
1136
|
+
type: 'number',
|
|
1137
|
+
className: 'fh-set-num',
|
|
1138
|
+
min: field.min,
|
|
1139
|
+
max: field.max,
|
|
1140
|
+
step: field.step,
|
|
1141
|
+
value: draft[0] === null ? String(shown) : draft[0],
|
|
1142
|
+
onChange: function (event) { draft[1](event.target.value) },
|
|
1143
|
+
onBlur: commit,
|
|
1144
|
+
onKeyDown: function (event) { if (event.key === 'Enter') event.currentTarget.blur() }
|
|
1145
|
+
}),
|
|
1146
|
+
h('span', { className: 'fh-set-unit' }, t(field.unit))),
|
|
1147
|
+
failed[0] ? h('span', { className: 'fh-error' }, t('saveFailed')) : null),
|
|
1148
|
+
h('p', { className: 'fh-set-hint' }, t(field.hint)))
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function SettingsCard(props) {
|
|
1152
|
+
var t = props.t
|
|
1153
|
+
var scope = props.scope
|
|
1154
|
+
var snapshot = useSettingsSnapshot(scope)
|
|
1155
|
+
// 宿主还没把这一份发下来时整张卡不出现,免得先亮一排默认值再跳一次。
|
|
1156
|
+
if (!scope || snapshot === null || snapshot.status !== 'ready' || !snapshot.value) return null
|
|
1157
|
+
var value = snapshot.value
|
|
1158
|
+
// 跟官方那张插件卡一样先折着:一页里可能有好几个插件,摊开会很长。
|
|
1159
|
+
var open = React.useState(false)
|
|
1160
|
+
return h('div', { className: 'fh-set' + (open[0] ? ' fh-set-open' : '') },
|
|
1161
|
+
h('button', {
|
|
1162
|
+
type: 'button',
|
|
1163
|
+
className: 'fh-set-head',
|
|
1164
|
+
'aria-expanded': open[0],
|
|
1165
|
+
onClick: function () { open[1](!open[0]) }
|
|
1166
|
+
},
|
|
1167
|
+
h('span', { className: 'fh-set-headText' },
|
|
1168
|
+
h('span', { className: 'fh-set-name' }, t('panelTitle')),
|
|
1169
|
+
h('span', { className: 'fh-set-desc' }, t('setDesc'))),
|
|
1170
|
+
h('span', { className: 'fh-chev' + (open[0] ? ' fh-chev-open' : '') }, IconChevron ? h(IconChevron) : '▾')),
|
|
1171
|
+
open[0]
|
|
1172
|
+
? h('div', { className: 'fh-set-body' }, h('div', { className: 'fh-set-rows' }, FIELDS.map(function (field) {
|
|
1173
|
+
return field.type === 'bool'
|
|
1174
|
+
? h(BoolRow, { key: field.key, t: t, scope: scope, field: field, value: value[field.key] !== false })
|
|
1175
|
+
: h(NumRow, { key: field.key, t: t, scope: scope, field: field, value: value[field.key] })
|
|
1176
|
+
})))
|
|
1177
|
+
: null)
|
|
1178
|
+
}
|
|
1179
|
+
|
|
955
1180
|
function apply(ctx) {
|
|
956
1181
|
var slots = ctx.slots || ctx.get('slots')
|
|
957
1182
|
var locale = ctx.locale || ctx.get('locale')
|
|
@@ -981,6 +1206,10 @@ window.__ModuleLoader__.load({
|
|
|
981
1206
|
|
|
982
1207
|
var t = locale && typeof locale.bind === 'function' ? locale.bind(NS) : function (key) { return key }
|
|
983
1208
|
|
|
1209
|
+
// 设置页的那张卡和三个界面开关共用这一份:宿主注册的命名空间,客户端这边只是镜像。
|
|
1210
|
+
var settingsScope = ctx.settingsScope || ctx.get('settingsScope')
|
|
1211
|
+
var settings = settingsScope && typeof settingsScope.bind === 'function' ? settingsScope.bind({ namespace: SETTINGS_NS }) : null
|
|
1212
|
+
|
|
984
1213
|
async function call(endpoint, payload) {
|
|
985
1214
|
var result = await connection.rpc.call(CHANNEL, endpoint, payload)
|
|
986
1215
|
if (!result || result.ok !== true) {
|
|
@@ -988,47 +1217,69 @@ window.__ModuleLoader__.load({
|
|
|
988
1217
|
}
|
|
989
1218
|
return result.value
|
|
990
1219
|
}
|
|
991
|
-
|
|
992
|
-
if (slots &&
|
|
1220
|
+
// 设置页里的一张卡:宿主注册了这个命名空间,卡片才会出现在插件列表里。
|
|
1221
|
+
if (slots && settings) {
|
|
993
1222
|
ctx.effect(function () {
|
|
994
|
-
return
|
|
995
|
-
id: TAB_ID,
|
|
996
|
-
kind: TAB_ID,
|
|
997
|
-
priority: 'extension',
|
|
998
|
-
title: function () { return t('panelTitle') },
|
|
999
|
-
guide: [{
|
|
1000
|
-
order: 15,
|
|
1001
|
-
title: function () { return t('panelTitle') },
|
|
1002
|
-
description: function () { return t('panelDesc') },
|
|
1003
|
-
icon: IconClock || undefined
|
|
1004
|
-
}]
|
|
1005
|
-
})
|
|
1006
|
-
}, 'dsh-file-history: tab type')
|
|
1007
|
-
|
|
1008
|
-
ctx.effect(function () {
|
|
1009
|
-
return slots.inject('sidebar.right.pane.tab', function () {
|
|
1223
|
+
return slots.inject('settings.plugin.item', function () {
|
|
1010
1224
|
return slots.register(
|
|
1011
1225
|
{
|
|
1012
|
-
name: '
|
|
1013
|
-
key:
|
|
1014
|
-
locale: NS
|
|
1015
|
-
inject: function (sessionId) { return { sessionId: sessionId } }
|
|
1226
|
+
name: 'settings.plugin.item',
|
|
1227
|
+
key: SETTINGS_NS,
|
|
1228
|
+
locale: NS
|
|
1016
1229
|
},
|
|
1017
|
-
function (
|
|
1018
|
-
return h(
|
|
1019
|
-
t: t,
|
|
1020
|
-
call: call,
|
|
1021
|
-
uiWorkspace: uiWorkspace,
|
|
1022
|
-
sessionId: props.sessionId
|
|
1023
|
-
})
|
|
1230
|
+
function () {
|
|
1231
|
+
return h(SettingsCard, { t: t, scope: settings })
|
|
1024
1232
|
}
|
|
1025
1233
|
)
|
|
1026
1234
|
})
|
|
1027
|
-
}, 'dsh-file-history:
|
|
1235
|
+
}, 'dsh-file-history: settings card')
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (slots && ctx.sidebarRightTabs) {
|
|
1239
|
+
// 时间线关掉时,标签类型和面板一起撤:只撤面板会留下一个空标签。
|
|
1240
|
+
ctx.effect(function () {
|
|
1241
|
+
return whenShown(settings, 'timeline', function () {
|
|
1242
|
+
var unTab = ctx.sidebarRightTabs.register({
|
|
1243
|
+
id: TAB_ID,
|
|
1244
|
+
kind: TAB_ID,
|
|
1245
|
+
priority: 'extension',
|
|
1246
|
+
title: function () { return t('panelTitle') },
|
|
1247
|
+
guide: [{
|
|
1248
|
+
order: 15,
|
|
1249
|
+
title: function () { return t('panelTitle') },
|
|
1250
|
+
description: function () { return t('panelDesc') },
|
|
1251
|
+
icon: IconClock || undefined
|
|
1252
|
+
}]
|
|
1253
|
+
})
|
|
1254
|
+
var unBody = slots.inject('sidebar.right.pane.tab', function () {
|
|
1255
|
+
return slots.register(
|
|
1256
|
+
{
|
|
1257
|
+
name: 'sidebar.right.pane.tab',
|
|
1258
|
+
key: TAB_ID,
|
|
1259
|
+
locale: NS,
|
|
1260
|
+
inject: function (sessionId) { return { sessionId: sessionId } }
|
|
1261
|
+
},
|
|
1262
|
+
function (props) {
|
|
1263
|
+
return h(Panel, {
|
|
1264
|
+
t: t,
|
|
1265
|
+
call: call,
|
|
1266
|
+
uiWorkspace: uiWorkspace,
|
|
1267
|
+
sessionId: props.sessionId
|
|
1268
|
+
})
|
|
1269
|
+
}
|
|
1270
|
+
)
|
|
1271
|
+
})
|
|
1272
|
+
return function () {
|
|
1273
|
+
unBody()
|
|
1274
|
+
unTab()
|
|
1275
|
+
}
|
|
1276
|
+
})
|
|
1277
|
+
}, 'dsh-file-history: tab')
|
|
1028
1278
|
}
|
|
1029
1279
|
|
|
1030
1280
|
if (slots) {
|
|
1031
1281
|
ctx.effect(function () {
|
|
1282
|
+
return whenShown(settings, 'turnCard', function () {
|
|
1032
1283
|
return slots.inject('conversation.chat.turnTail', function () {
|
|
1033
1284
|
return slots.register(
|
|
1034
1285
|
{
|
|
@@ -1056,6 +1307,7 @@ window.__ModuleLoader__.load({
|
|
|
1056
1307
|
}
|
|
1057
1308
|
)
|
|
1058
1309
|
})
|
|
1310
|
+
})
|
|
1059
1311
|
}, 'dsh-file-history: turn tail')
|
|
1060
1312
|
}
|
|
1061
1313
|
|
|
@@ -1079,7 +1331,7 @@ window.__ModuleLoader__.load({
|
|
|
1079
1331
|
}
|
|
1080
1332
|
|
|
1081
1333
|
exports.name = 'dsh-file-history'
|
|
1082
|
-
exports.inject = ['connection', 'locale', 'slots', 'sidebarRightTabs']
|
|
1334
|
+
exports.inject = ['connection', 'locale', 'slots', 'sidebarRightTabs', 'settingsScope']
|
|
1083
1335
|
exports.apply = apply
|
|
1084
1336
|
return module.exports
|
|
1085
1337
|
}
|
package/lib/index.js
CHANGED
|
@@ -7,12 +7,13 @@ import fs from 'node:fs'
|
|
|
7
7
|
import fsp from 'node:fs/promises'
|
|
8
8
|
import os from 'node:os'
|
|
9
9
|
import path from 'node:path'
|
|
10
|
+
import z from 'schemastery'
|
|
10
11
|
|
|
11
12
|
// git 输出的解析与合并规则单独一处:它们最容易改错,也最容易单独验。
|
|
12
13
|
import { foldPointers, parseBatchBlobs, parseLsFiles, parseNumstat, parseRaw, parseSizes, summarize } from './parse.js'
|
|
13
14
|
|
|
14
15
|
export const name = 'dsh-file-history'
|
|
15
|
-
export const inject = ['sessions', 'workspaceRegistry', 'connection']
|
|
16
|
+
export const inject = ['sessions', 'workspaceRegistry', 'connection', 'settings']
|
|
16
17
|
|
|
17
18
|
const CHANNEL = '/dsh-file-history'
|
|
18
19
|
const LEDGER_VERSION = 1
|
|
@@ -57,6 +58,21 @@ const DEFAULTS = {
|
|
|
57
58
|
gcIntervalMs: 10 * 60 * 1000
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
const SETTINGS_NS = 'dsh-file-history'
|
|
62
|
+
|
|
63
|
+
// 设置页里露出来的就这几项,按用到的先后排:三个界面开关在前,数量类在后。
|
|
64
|
+
// 单位:两个上限按字节存,卡片里换成 MB 给人看。
|
|
65
|
+
const SettingsSchema = z.object({
|
|
66
|
+
turnCard: z.boolean().default(true),
|
|
67
|
+
dock: z.boolean().default(true),
|
|
68
|
+
timeline: z.boolean().default(true),
|
|
69
|
+
keepRecentTurns: z.number().min(0).max(1000).default(DEFAULTS.keepRecentTurns),
|
|
70
|
+
maxFileBytes: z.number().min(0).max(1024 * 1024 * 1024).default(DEFAULTS.maxFileBytes),
|
|
71
|
+
maxRepoFiles: z.number().min(0).max(1000000).default(DEFAULTS.maxRepoFiles),
|
|
72
|
+
maxBytes: z.number().min(0).max(1024 * 1024 * 1024 * 1024).default(DEFAULTS.maxBytes),
|
|
73
|
+
pruneAfterCheckpoints: z.number().min(0).max(100).default(DEFAULTS.pruneAfterCheckpoints)
|
|
74
|
+
})
|
|
75
|
+
|
|
60
76
|
function stateRoot() {
|
|
61
77
|
return path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'file-history')
|
|
62
78
|
}
|
|
@@ -1277,7 +1293,17 @@ class History {
|
|
|
1277
1293
|
|
|
1278
1294
|
export async function apply(ctx, config) {
|
|
1279
1295
|
const options = Object.assign({}, DEFAULTS, config && typeof config === 'object' ? config : {})
|
|
1280
|
-
|
|
1296
|
+
// 设置页写的是同一份值:注册成命名空间后,卡片一改就往这个对象里覆盖,
|
|
1297
|
+
// 各个工作区在构造时拿的就是这个引用,不必重建。
|
|
1298
|
+
const scope = ctx.settings.register(SETTINGS_NS, SettingsSchema, { base: options })
|
|
1299
|
+
const keys = Object.keys(SettingsSchema.dict)
|
|
1300
|
+
const spread = (next) => {
|
|
1301
|
+
for (const key of keys) if (next[key] !== undefined) options[key] = next[key]
|
|
1302
|
+
}
|
|
1303
|
+
spread(scope.get())
|
|
1304
|
+
ctx.effect(() => scope.watch((next) => {
|
|
1305
|
+
spread(next)
|
|
1306
|
+
}))
|
|
1281
1307
|
const history = new History(ctx, options)
|
|
1282
1308
|
ctx.on('session/event', (session, event) => {
|
|
1283
1309
|
try {
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-file-history",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Per-turn file history for DeepSeek Harness: every conversation turn gets a snapshot, a change card in the chat, and one-click rollback of files, conversation, or both.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "node --test"
|
|
9
9
|
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"schemastery": "^3.18.0"
|
|
12
|
+
},
|
|
10
13
|
"exports": {
|
|
11
14
|
".": "./lib/index.js",
|
|
12
15
|
"./client": "./lib/client.js",
|
|
@@ -44,7 +47,7 @@
|
|
|
44
47
|
},
|
|
45
48
|
"client": {
|
|
46
49
|
"platform": "web",
|
|
47
|
-
"inject": ["@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-primitives", "@deepseek-ai/dsh-client-ui-slots", "@deepseek-ai/dsh-client-ui-conversation"]
|
|
50
|
+
"inject": ["@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-primitives", "@deepseek-ai/dsh-client-ui-slots", "@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-ui-conversation"]
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
}
|