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 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
- // 侧栏 guide 条目的图标用官方图标集;模块表里没有时留空,回落到自带的方块占位。
9
- var IconClock = null
8
+ // 图标和设置页的开关都来自官方控件集;模块表里没有时留空,各自回落到自带的画法。
9
+ var primitives = null
10
10
  try {
11
- IconClock = require('@deepseek-ai/dsh-client-ui-primitives').IconClockOutline16 || null
11
+ primitives = require('@deepseek-ai/dsh-client-ui-primitives')
12
12
  } catch (error) {
13
- IconClock = null
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 && ctx.sidebarRightTabs) {
1220
+ // 设置页里的一张卡:宿主注册了这个命名空间,卡片才会出现在插件列表里。
1221
+ if (slots && settings) {
993
1222
  ctx.effect(function () {
994
- return ctx.sidebarRightTabs.register({
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: 'sidebar.right.pane.tab',
1013
- key: TAB_ID,
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 (props) {
1018
- return h(Panel, {
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: tab body')
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
- if (!Array.isArray(options.excludes)) options.excludes = DEFAULTS.excludes.slice()
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.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
  }