dsh-config-manager 0.1.72 → 0.1.76

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.
Files changed (153) hide show
  1. package/README.md +40 -4
  2. package/README.zh-CN.md +40 -4
  3. package/lib/adapters/index.d.ts +1 -0
  4. package/lib/adapters/index.js +5 -0
  5. package/lib/adapters/messages.d.ts +3 -0
  6. package/lib/adapters/messages.js +7 -0
  7. package/lib/adapters/plugin-data.d.ts +114 -0
  8. package/lib/adapters/plugin-data.js +309 -0
  9. package/lib/adapters/self.d.ts +1 -1
  10. package/lib/adapters/self.js +2 -1
  11. package/lib/adapters/test-helpers.d.ts +9 -0
  12. package/lib/adapters/test-helpers.js +14 -0
  13. package/lib/client.d.ts +103 -10
  14. package/lib/client.js +10748 -10119
  15. package/lib/core/analyzer.d.ts +11 -2
  16. package/lib/core/analyzer.js +31 -8
  17. package/lib/core/backup-plan.d.ts +10 -0
  18. package/lib/core/backup-plan.js +21 -3
  19. package/lib/core/backup.d.ts +20 -1
  20. package/lib/core/backup.js +48 -3
  21. package/lib/core/command-rollback.d.ts +6 -2
  22. package/lib/core/command-rollback.js +6 -2
  23. package/lib/core/messages-core.d.ts +2 -0
  24. package/lib/core/messages-core.js +4 -0
  25. package/lib/core/messages.d.ts +2 -1
  26. package/lib/core/messages.js +4 -2
  27. package/lib/core/plugin-data-registry.d.ts +124 -0
  28. package/lib/core/plugin-data-registry.js +198 -0
  29. package/lib/core/rollback-gate.d.ts +33 -0
  30. package/lib/core/rollback-gate.js +57 -0
  31. package/lib/core/rollback-ledger.d.ts +274 -0
  32. package/lib/core/rollback-ledger.js +1400 -0
  33. package/lib/core/rollback-point.d.ts +406 -5
  34. package/lib/core/rollback-point.js +1453 -306
  35. package/lib/core/rollback.js +6 -4
  36. package/lib/core/types.d.ts +19 -0
  37. package/lib/foreign/registry.d.ts +1 -1
  38. package/lib/foreign/registry.js +1 -1
  39. package/lib/index.d.ts +5 -1
  40. package/lib/index.js +83 -13
  41. package/lib/routes/messages.d.ts +1 -0
  42. package/lib/routes/messages.js +2 -0
  43. package/lib/routes/prefs.js +26 -3
  44. package/lib/routes/recovery.d.ts +21 -0
  45. package/lib/routes/recovery.js +127 -7
  46. package/lib/routes/sync.js +11 -1
  47. package/lib/schema/section-registry.d.ts +1 -1
  48. package/lib/schema/section-registry.js +32 -3
  49. package/lib/schema/types.d.ts +1 -1
  50. package/lib/sync/messages.d.ts +1 -0
  51. package/lib/sync/messages.js +3 -0
  52. package/lib/sync/sync-engine.d.ts +21 -7
  53. package/lib/sync/sync-engine.js +108 -18
  54. package/lib/sync/ui-prefs.d.ts +22 -0
  55. package/lib/sync/ui-prefs.js +28 -0
  56. package/lib/ui/diff-view.d.ts +10 -0
  57. package/lib/ui/diff-view.js +17 -0
  58. package/lib/ui/export-flow.js +2 -0
  59. package/lib/ui/i18n.d.ts +5 -3
  60. package/lib/ui/i18n.js +10 -6
  61. package/lib/ui/rollback-view.d.ts +59 -0
  62. package/lib/ui/rollback-view.js +14 -0
  63. package/lib/ui/test-helpers.js +2 -1
  64. package/lib/utils/bundle-scan.d.ts +5 -3
  65. package/lib/utils/bundle-scan.js +5 -3
  66. package/lib/utils/line-diff.d.ts +7 -1
  67. package/lib/utils/line-diff.js +8 -2
  68. package/package.json +1 -1
  69. package/src/adapters/index.ts +5 -0
  70. package/src/adapters/messages.ts +7 -0
  71. package/src/adapters/plugin-data.test.ts +225 -0
  72. package/src/adapters/plugin-data.ts +351 -0
  73. package/src/adapters/self.ts +2 -1
  74. package/src/adapters/test-helpers.ts +15 -0
  75. package/src/client/chat/ChatEntryView.tsx +59 -10
  76. package/src/client/chat/RollbackDiffTab.tsx +49 -2
  77. package/src/client/chat/RollbackFileIcon.tsx +7 -45
  78. package/src/client/chat/RollbackReviewDialog.tsx +20 -4
  79. package/src/client/chat/chat-entry-view.test.ts +164 -1
  80. package/src/client/chat/chat-entry-view.ts +127 -10
  81. package/src/client/chat/chat-node-registration.ts +40 -6
  82. package/src/client/chat/chat-point-window.ts +11 -1
  83. package/src/client/chat/rollback-diff-tab.ts +13 -3
  84. package/src/client/chat/rollback-entry-view.ts +5 -0
  85. package/src/client/chat/sidebar-slots.ts +24 -2
  86. package/src/client/common/DiffTable.tsx +54 -29
  87. package/src/client/common/client-route-parity.test.ts +6 -0
  88. package/src/client/common/host-primitives.ts +78 -0
  89. package/src/client/common/routes.ts +10 -0
  90. package/src/client/common/section-labels.test.ts +1 -0
  91. package/src/client/common/section-labels.ts +2 -0
  92. package/src/client/config-manager.module.css +138 -14
  93. package/src/client/environment/maintenance/DiskUsageCard.tsx +20 -3
  94. package/src/client/index.ts +54 -2
  95. package/src/client/library/RestorePlanView.tsx +0 -1
  96. package/src/client/locales.ts +8 -6
  97. package/src/client/recovery/RollbackCard.tsx +201 -7
  98. package/src/client/recovery/RollbackDiffPane.tsx +26 -8
  99. package/src/client/recovery/recovery-api.ts +28 -0
  100. package/src/client/recovery/recovery-locales.ts +36 -6
  101. package/src/client/recovery/rollback-enabled.ts +41 -0
  102. package/src/client/recovery/rollback-file-changes.test.ts +77 -0
  103. package/src/client/sync/PushPreviewCard.tsx +57 -0
  104. package/src/client/sync/SyncPanel.tsx +2 -37
  105. package/src/client/sync/SyncSettingsView.test.ts +3 -0
  106. package/src/client/sync/sync-push-preview.test.ts +14 -0
  107. package/src/client/sync/sync-view.test.ts +1 -1
  108. package/src/client/sync/sync-view.ts +9 -1
  109. package/src/core/analyzer.ts +31 -9
  110. package/src/core/backup-plan.test.ts +20 -5
  111. package/src/core/backup-plan.ts +22 -3
  112. package/src/core/backup.ts +50 -4
  113. package/src/core/command-rollback.ts +6 -2
  114. package/src/core/messages-core.ts +4 -0
  115. package/src/core/messages.ts +4 -2
  116. package/src/core/plugin-data-registry.test.ts +113 -0
  117. package/src/core/plugin-data-registry.ts +261 -0
  118. package/src/core/rollback-gate.test.ts +66 -0
  119. package/src/core/rollback-gate.ts +61 -0
  120. package/src/core/rollback-ledger.test.ts +655 -0
  121. package/src/core/rollback-ledger.ts +1430 -0
  122. package/src/core/rollback-point.test.ts +866 -41
  123. package/src/core/rollback-point.ts +1582 -291
  124. package/src/core/rollback.ts +5 -3
  125. package/src/core/types.ts +19 -0
  126. package/src/foreign/registry.test.ts +1 -1
  127. package/src/foreign/registry.ts +1 -1
  128. package/src/index.ts +73 -10
  129. package/src/routes/messages.ts +2 -0
  130. package/src/routes/prefs.ts +31 -2
  131. package/src/routes/recovery.ts +122 -7
  132. package/src/routes/sync.ts +15 -1
  133. package/src/schema/manifest-schema.test.ts +4 -4
  134. package/src/schema/registry.test.ts +9 -2
  135. package/src/schema/section-registry.ts +32 -3
  136. package/src/schema/types.ts +2 -1
  137. package/src/sync/messages.ts +3 -0
  138. package/src/sync/sync-credentials.test.ts +119 -0
  139. package/src/sync/sync-engine.ts +106 -18
  140. package/src/sync/sync-preview-push.test.ts +158 -0
  141. package/src/sync/transport.test.ts +1 -0
  142. package/src/sync/ui-prefs.test.ts +44 -1
  143. package/src/sync/ui-prefs.ts +39 -0
  144. package/src/ui/diff-view.test.ts +9 -1
  145. package/src/ui/diff-view.ts +22 -0
  146. package/src/ui/export-flow.ts +2 -0
  147. package/src/ui/i18n.ts +10 -6
  148. package/src/ui/rollback-view.test.ts +17 -0
  149. package/src/ui/rollback-view.ts +66 -0
  150. package/src/ui/test-helpers.ts +2 -1
  151. package/src/utils/bundle-scan.ts +5 -3
  152. package/src/utils/line-diff.test.ts +12 -0
  153. package/src/utils/line-diff.ts +9 -3
@@ -21,6 +21,7 @@
21
21
  * 避免「sync/ui-prefs 写 lastSyncChannel」与「star-prompt 写弹窗状态」互相覆盖。
22
22
  */
23
23
  import fs from 'node:fs/promises';
24
+ import fsSync from 'node:fs';
24
25
  import path from 'node:path';
25
26
 
26
27
  import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.ts';
@@ -60,6 +61,18 @@ export interface UiPrefs {
60
61
  * 「用户主动改配置」或「非本插件客户端」时起作用。
61
62
  */
62
63
  sessionFormatDisposition?: SessionFormatDisposition;
64
+ /**
65
+ * 回合自动留点(回滚点)总开关。**缺省 / 任何非 true 值 = 关闭**(issue #88 / #89:
66
+ * 该功能与「配置备份 / 同步」是两件事,却随 bundle 默认启用且没有关闭入口;
67
+ * 默认启用时账本每回合整份读改写,真机上把宿主拖到 V8 heap OOM)。
68
+ *
69
+ * 关闭语义(宿主侧 gate,见 src/core/rollback-gate.ts):
70
+ * · 不订阅 `session/event`(不是「订阅了什么都不做」);
71
+ * · 不留点、不写台账;
72
+ * · 聊天内的「↺ 回退」入口收起(不是置灰)。
73
+ * 台账 / 回退路由本身仍可用:已有回滚点仍能查看、回退、删除、清理 —— 关的是「自动留点」。
74
+ */
75
+ rollbackEnabled?: boolean;
63
76
  }
64
77
 
65
78
  /** 缺省配置(首次无文件 / 损坏 / 不支持 schema 时回退) */
@@ -118,9 +131,34 @@ export async function readUiPrefs(dir: string): Promise<UiPrefs> {
118
131
  if (disposition === 'abort' || disposition === 'skip' || disposition === 'guide') {
119
132
  prefs.sessionFormatDisposition = disposition;
120
133
  }
134
+ // 开关只认字面 true:false / 缺省 / 形状不符一律 = 关闭(fail-closed,绝不猜用户想开)
135
+ if (obj['rollbackEnabled'] === true) {
136
+ prefs.rollbackEnabled = true;
137
+ }
121
138
  return prefs;
122
139
  }
123
140
 
141
+ /**
142
+ * 同步读取「回滚点开关」(启动期专用)。
143
+ *
144
+ * 为什么必须有同步版本:宿主 `apply()` 是同步函数(`src/index.ts`),而开关必须在
145
+ * **注册订阅之前**就定下来 —— 否则「关闭」只等于「订阅了什么都不做」,与 issue 要求的
146
+ * 「关闭后不再订阅 `session/event`」不符。读一个几百字节的小文件,启动期一次,代价可忽略。
147
+ *
148
+ * 任何异常(不存在 / 损坏 / 形状不符)一律回 false = 关闭。
149
+ */
150
+ export function readRollbackEnabledSync(dir: string): boolean {
151
+ try {
152
+ const raw = fsSync.readFileSync(path.join(dir, UI_PREFS_FILE), 'utf8');
153
+ const parsed: unknown = JSON.parse(raw);
154
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
155
+ const obj = parsed as Record<string, unknown>;
156
+ return obj['schemaVersion'] === UI_PREFS_SCHEMA_VERSION && obj['rollbackEnabled'] === true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
124
162
  /** 写入 UI 偏好(原子写:临时文件 + rename;自动创建目录)。 */
125
163
  export async function writeUiPrefs(dir: string, prefs: UiPrefs): Promise<void> {
126
164
  await fs.mkdir(dir, { recursive: true });
@@ -133,6 +171,7 @@ export async function writeUiPrefs(dir: string, prefs: UiPrefs): Promise<void> {
133
171
  ...(prefs.releaseNotesLastSeenVersion !== undefined ? { releaseNotesLastSeenVersion: prefs.releaseNotesLastSeenVersion } : {}),
134
172
  ...(prefs.releaseNotesDismissed === true ? { releaseNotesDismissed: true } : {}),
135
173
  ...(prefs.sessionFormatDisposition !== undefined ? { sessionFormatDisposition: prefs.sessionFormatDisposition } : {}),
174
+ ...(prefs.rollbackEnabled === true ? { rollbackEnabled: true } : {}),
136
175
  };
137
176
  const target = path.join(dir, UI_PREFS_FILE);
138
177
  const data = stringifyJsonSafe(payload, { space: 2 });
@@ -5,7 +5,7 @@ import test from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
 
7
7
  import { diffLines } from '../utils/line-diff.ts';
8
- import { countSideRows, hunkHeader, maxLineDigits, toSideBySide } from './diff-view.ts';
8
+ import { countSideRows, diffLineCounts, hunkHeader, maxLineDigits, toSideBySide } from './diff-view.ts';
9
9
 
10
10
  test('V-01 成对修改:3 删 1 增 → 1 个 change 行对 + 2 个 del 行对(左有右空)', () => {
11
11
  const r = diffLines('a\nX1\nX2\nX3\nb', 'a\nY1\nb', { context: 1 });
@@ -56,6 +56,14 @@ test('V-06 行对总数 = 上下文 + 成对/单侧行', () => {
56
56
  assert.equal(countSideRows(groups), 3, 'context 1 + 1 + context 1');
57
57
  });
58
58
 
59
+ test('V-08 diffLineCounts:只数增删行(上下文不计),口径与 git 的 +A −R 一致', () => {
60
+ const r = diffLines('a\nX1\nX2\nX3\nb', 'a\nY1\nb', { context: 1 });
61
+ assert.deepEqual(diffLineCounts(r.hunks), { added: 1, removed: 3 });
62
+ assert.deepEqual(diffLineCounts([]), { added: 0, removed: 0 }, '无差异 → 两个 0');
63
+ const only = diffLines('a\nb', 'a\nNEW\nb');
64
+ assert.deepEqual(diffLineCounts(only.hunks), { added: 1, removed: 0 }, '纯新增不计删除');
65
+ });
66
+
59
67
  test('V-07 maxLineDigits:按实际出现的最大行号取位数(列宽档位依据)', () => {
60
68
  assert.equal(maxLineDigits([]), 1, '空 hunks → 至少 1 位');
61
69
  const small = diffLines('a\nb\nc', 'a\nX\nc', { context: 1 });
@@ -89,3 +89,25 @@ export function maxLineDigits(hunks: readonly DiffHunk[]): number {
89
89
  export function countSideRows(groups: readonly SideBySideHunk[]): number {
90
90
  return groups.reduce((sum, group) => sum + group.rows.length, 0);
91
91
  }
92
+
93
+ /** 增删行数(编辑器式文件头的 `+A` / `−R` 用;上下文行不计)。 */
94
+ export interface DiffLineCounts {
95
+ added: number
96
+ removed: number
97
+ }
98
+
99
+ /**
100
+ * 一侧的行数统计:**只数 unidiff 语义的增删行**(与 git 的 `+A −R` 同口径)。
101
+ * 成对修改在 unidiff 里本来就是「一删一增」,所以这里不需要按行对再折算。
102
+ */
103
+ export function diffLineCounts(hunks: readonly DiffHunk[]): DiffLineCounts {
104
+ let added = 0
105
+ let removed = 0
106
+ for (const hunk of hunks) {
107
+ for (const line of hunk.lines) {
108
+ if (line.kind === 'add') added += 1
109
+ else if (line.kind === 'del') removed += 1
110
+ }
111
+ }
112
+ return { added, removed }
113
+ }
@@ -105,6 +105,8 @@ const CATEGORY_DESCRIPTION_KEY: Record<Exclude<SectionId, 'secrets'>, UiTextKey>
105
105
  ui: 'export.category.ui',
106
106
  credentialsStatus: 'export.category.credentialsStatus',
107
107
  sessions: 'export.category.sessions',
108
+ pluginData: 'export.category.pluginData',
109
+ pluginAssets: 'export.category.pluginAssets',
108
110
  };
109
111
 
110
112
  /** 需在 UI 标注安全提示、但绝不显示值的分区(凭据状态只有 configured 标记) */
package/src/ui/i18n.ts CHANGED
@@ -100,7 +100,7 @@ export const uiZh = {
100
100
  'error.downloadFailed': '下载失败:HTTP {status}',
101
101
  'error.exportTimeout': '导出超时({minutes} 分钟):导出过程未完成,请重试;若反复超时请检查 DSH 状态',
102
102
  'error.syncTimeout': '同步请求超时({minutes} 分钟):请检查网络与仓库可达性后重试',
103
- 'error.recoveryTimeout': '恢复请求超时({minutes} 分钟):恢复过程未完成,请重试;若反复超时请检查 DSH 状态',
103
+ 'error.recoveryTimeout': '恢复请求超时({minutes} 分钟):过程可能仍在后台进行,请先刷新预览确认;确实未完成再重试',
104
104
  'error.settingsConflict.title': '配置已被并发修改',
105
105
  'error.settingsConflict.action': '目标 DSH 的该配置在导入期间被其他进程修改,为避免覆盖请重新导出或手动核对后再试。',
106
106
  'error.notConfirmed.title': '导入未确认',
@@ -173,7 +173,7 @@ export const uiZh = {
173
173
  'sync.kind.skip': '跳过',
174
174
  'sync.kind.conflict': '冲突',
175
175
  'sync.kind.install': '安装',
176
- 'sync.kind.missingSecret': '缺密钥',
176
+ 'sync.kind.missingSecret': '凭据迁移',
177
177
  'sync.kind.missingDependency': '缺依赖',
178
178
  'sync.kind.pathMapping': '路径映射',
179
179
  'sync.kind.warning': '警告',
@@ -548,8 +548,10 @@ export const uiZh = {
548
548
  'export.category.settings': 'DSH 全局设置(namespace 分区,redacted)',
549
549
  'export.category.providers': 'LLM Provider / Model / 默认模型 / BaseURL',
550
550
  'export.category.plugins': '已安装插件清单与启用状态(不含二进制)',
551
- 'export.category.pluginFiles': '插件自有配置文件(白名单 + plugin-config/ 目录,整文件复制)',
551
+ 'export.category.pluginFiles': '其他插件放在 ~/.dsh 根下的零散配置文件(白名单 + plugin-config/ 目录,整文件复制)',
552
552
  'export.category.self': '本插件自身配置(同步/自动同步/分区选择/UI 偏好/市场;sync-*.json 等,不含凭据值)',
553
+ 'export.category.pluginData': '其他插件在系统主目录下的数据(如 ~/.dsh-wallpaper-engine、~/.mnemon 的配置与状态;可再生成缓存绝不收录)',
554
+ 'export.category.pluginAssets': '其他插件的大体积文件(上传的壁纸、本地数据库等;默认不勾选)',
553
555
  'export.category.mcp': 'MCP 服务器组合配置(需重启生效)',
554
556
  'export.category.prompts': 'System Prompt / Plan Mode 提示',
555
557
  'export.category.skills': '用户技能文件(~/.dsh/skills)',
@@ -664,7 +666,7 @@ export const uiEn: Record<UiTextKey, string> = {
664
666
  'error.downloadFailed': 'Download failed: HTTP {status}',
665
667
  'error.exportTimeout': 'Export timed out ({minutes} min): the export did not complete, please retry; if it keeps timing out, check the DSH state',
666
668
  'error.syncTimeout': 'Sync request timed out ({minutes} min): check network and repository reachability, then retry',
667
- 'error.recoveryTimeout': 'Recovery request timed out ({minutes} min): the recovery did not complete, please retry; if it keeps timing out, check the DSH state',
669
+ 'error.recoveryTimeout': 'Recovery request timed out ({minutes} min): it may still be running in the background — refresh the preview to confirm, then retry if it did not finish',
668
670
  'error.settingsConflict.title': 'Configuration changed concurrently',
669
671
  'error.settingsConflict.action': 'This configuration on the target DSH was modified by another process during import. To avoid overwriting it, re-export or review it manually, then retry.',
670
672
  'error.notConfirmed.title': 'Import not confirmed',
@@ -734,7 +736,7 @@ export const uiEn: Record<UiTextKey, string> = {
734
736
  'sync.kind.skip': 'Skip',
735
737
  'sync.kind.conflict': 'Conflict',
736
738
  'sync.kind.install': 'Install',
737
- 'sync.kind.missingSecret': 'Secret',
739
+ 'sync.kind.missingSecret': 'Credential',
738
740
  'sync.kind.missingDependency': 'Dependency',
739
741
  'sync.kind.pathMapping': 'Path',
740
742
  'sync.kind.warning': 'Warning',
@@ -1095,8 +1097,10 @@ export const uiEn: Record<UiTextKey, string> = {
1095
1097
  'export.category.settings': 'Global DSH settings (namespace sections, redacted)',
1096
1098
  'export.category.providers': 'LLM providers / models / default model / base URL',
1097
1099
  'export.category.plugins': 'Installed plugin list and enablement (binaries excluded)',
1098
- 'export.category.pluginFiles': "Plugins' own config files (allowlist + plugin-config/, copied whole)",
1100
+ 'export.category.pluginFiles': "Other plugins' loose config files at the ~/.dsh root (allowlist + plugin-config/, copied whole)",
1099
1101
  'export.category.self': "This plugin's own config (sync / autosync / section selection / UI prefs / market; sync-*.json etc., no credential values)",
1102
+ 'export.category.pluginData': "Other plugins' data under the system home directory (e.g. the config/state of ~/.dsh-wallpaper-engine and ~/.mnemon; regenerable caches are never included)",
1103
+ 'export.category.pluginAssets': "Other plugins' large files (uploaded wallpapers, local databases, ...; not selected by default)",
1100
1104
  'export.category.mcp': 'MCP server composition (restart required)',
1101
1105
  'export.category.prompts': 'System prompt / Plan Mode prompts',
1102
1106
  'export.category.skills': 'Your skill files (~/.dsh/skills)',
@@ -53,6 +53,7 @@ import {
53
53
  rollbackRowHasSessionCursor,
54
54
  rollbackModeKey,
55
55
  rollbackModeSegments,
56
+ rollbackEntryVisible,
56
57
  rollbackOutcome,
57
58
  rollbackPointRows,
58
59
  rollbackPreviewGuidance,
@@ -817,3 +818,19 @@ test('E1 类型与引擎逐字同形(源码级):五字段 + 三种 status
817
818
  assert.match(chat, /type RollbackPreviewFile\b/, '复用必须来自 src/ui 的类型导入(不是又抄一份字段表)')
818
819
  })
819
820
 
821
+ /**
822
+ * issue #88:聊天内「↺ 回退」入口的可见性判据。三条口径缺一不可:
823
+ * ① **明确 false = 收起**(用户把这个功能关了,入口本身就不该存在);
824
+ * ② **明确 true = 显示**;
825
+ * ③ **字段缺失 = 按「默认开」** —— 老宿主的语义就是默认启用,此时藏掉入口会让用户
826
+ * 「有点但没有入口」(那比多画一颗按钮更糟)。
827
+ */
828
+ test('issue #88:rollbackEntryVisible —— false 收起 / true 显示 / 缺字段按默认开', () => {
829
+ assert.equal(rollbackEntryVisible({ available: true, rollbackEnabled: false }), false, '明确关闭必须收起')
830
+ assert.equal(rollbackEntryVisible({ available: true, rollbackEnabled: true }), true)
831
+ assert.equal(rollbackEntryVisible({ available: true }), true, '老宿主没有该字段 = 那一版默认开')
832
+ assert.equal(rollbackEntryVisible(null), false, '还没读到状态时不画入口(缺省关闭)')
833
+ assert.equal(rollbackEntryVisible(undefined), false)
834
+ })
835
+
836
+
@@ -147,6 +147,8 @@ export const ROLLBACK_CODES: readonly string[] = [
147
147
  'session-out-of-root',
148
148
  'session-too-large',
149
149
  'session-active',
150
+ // 子代理会话(origin=subagent):**不自动留点** —— 出现即代表该会话被有意跳过(宿主日志留痕)
151
+ 'subagent-session',
150
152
  'session-fork-unavailable',
151
153
  'fork-failed',
152
154
  // 自动留点(工作区快照)
@@ -320,12 +322,53 @@ export interface RollbackPointSummary {
320
322
  segments?: RollbackSegmentSummary[]
321
323
  }
322
324
 
325
+ /**
326
+ * 台账占用(issue #89 P2:可观测)。
327
+ * `bytes` = records/ + payloads/ 的实际字节数;`budgetBytes` = 触发淘汰的字节预算;
328
+ * `evicted` = 本进程内被预算淘汰的自动点累计数。缺省(老宿主没这个字段)= 界面不渲染这一行。
329
+ */
330
+ export interface RollbackLedgerStatus {
331
+ records: number
332
+ bytes: number
333
+ budgetBytes: number
334
+ evicted: number
335
+ /**
336
+ * 已超预算且**无处可淘汰**(典型:单条点本身就大于预算)。
337
+ * 老宿主没有这个字段 ⇒ `undefined` 与 `false` 同义,绝不因为读不到就报警。
338
+ */
339
+ overBudget?: boolean
340
+ }
341
+
323
342
  export interface RollbackStorageStatus {
324
343
  available: boolean
325
344
  root?: string
326
345
  detail?: string
327
346
  guidance?: RollbackGuidance[]
328
347
  requirements?: readonly string[]
348
+ /** 磁盘偏好:回合自动留点开关(缺省 / false = 关闭) */
349
+ rollbackEnabled?: boolean
350
+ /** 本进程此刻是否真的在留点(关闭后订阅已解绑)。与 rollbackEnabled 正常一致 */
351
+ rollbackActive?: boolean
352
+ /** 台账占用(老宿主没有这个字段) */
353
+ ledger?: RollbackLedgerStatus
354
+ }
355
+
356
+ /** 开关读 / 写的结果(POST 成功才会改进程 gate)。 */
357
+ export interface RollbackPrefsResult {
358
+ ok: boolean
359
+ rollbackEnabled: boolean
360
+ rollbackActive?: boolean
361
+ }
362
+
363
+ /** 台账清理结果(只删自动点;guard 与 protected 保留)。 */
364
+ export interface RollbackPurgeResult {
365
+ ok: boolean
366
+ code?: string
367
+ detail?: string
368
+ removedRecords?: number
369
+ removedPayloads?: number
370
+ removedObjects?: number
371
+ bytes?: number
329
372
  }
330
373
 
331
374
  export interface RollbackListResult {
@@ -585,6 +628,29 @@ export interface RollbackPointPort {
585
628
  rollbackPointFileDiff?(id: string, path: string): Promise<RollbackFileDiffResult>
586
629
  restoreRollbackPoint(id: string, opts: RollbackRewindOptions): Promise<RollbackRewindResult>
587
630
  removeRollbackPoint(id: string, userConfirmed: boolean): Promise<RollbackRemoveResult>
631
+ /**
632
+ * 回合自动留点开关(issue #88:它跟配置备份/同步是两件事,必须能关)。
633
+ * 可选实现:端口缺失时界面**不渲染**开关(绝不给一个点了没反应的入口)。
634
+ */
635
+ rollbackPrefs?(): Promise<RollbackPrefsResult>
636
+ setRollbackEnabled?(enabled: boolean): Promise<RollbackPrefsResult>
637
+ /**
638
+ * 清理台账(issue #89 P1/P2:把已经涨上去的占用收回来)。
639
+ * **只删自动点**;`includeGuards` 为 true 时连 guard 也删,protected 永不删。
640
+ */
641
+ purgeRollbackPoints?(opts: { includeGuards: boolean }): Promise<RollbackPurgeResult>
642
+ }
643
+
644
+ /**
645
+ * 聊天内「↺ 回退」入口是否可见(issue #88:关闭后**收起**入口,不是置灰)。
646
+ *
647
+ * 判据只有一条:**宿主回传的开关**。老宿主(storage 响应里没有 `rollbackEnabled` 字段)按
648
+ * 「未提供 = 功能默认开」处理 —— 那一版的语义就是默认启用,此时把入口藏起来会让用户
649
+ * 「有点但没有入口」。形状不符(不是布尔)同样按「未提供」处理,绝不因为一个坏字段藏掉入口。
650
+ */
651
+ export function rollbackEntryVisible(storage: RollbackStorageStatus | null | undefined): boolean {
652
+ if (storage === undefined || storage === null) return false
653
+ return storage.rollbackEnabled !== false
588
654
  }
589
655
 
590
656
  /* ------------------------------------------------------------ 内部小工具 */
@@ -21,6 +21,7 @@ export function makeManifest(): Manifest {
21
21
  settings: true, ui: false, providers: false, plugins: false, mcp: false, prompts: false,
22
22
  skills: false, agentPresets: false, agentInstructions: false, workspaces: false, pluginFiles: false,
23
23
  credentialsStatus: false, secrets: false, sessions: false, self: false,
24
+ pluginData: false, pluginAssets: false,
24
25
  },
25
26
  security: { containsSecrets: false, encrypted: false, encryption: null },
26
27
  };
@@ -106,7 +107,7 @@ export function makePlan(overrides: Partial<ImportPlan> = {}): ImportPlan {
106
107
  estimatedActions: {
107
108
  settings: 2, ui: 0, providers: 0, plugins: 1, mcp: 1, prompts: 1,
108
109
  skills: 0, agentPresets: 0, agentInstructions: 0, workspaces: 0, pluginFiles: 0,
109
- credentialsStatus: 0, secrets: 0, sessions: 0, self: 0,
110
+ credentialsStatus: 0, secrets: 0, sessions: 0, self: 0, pluginData: 0, pluginAssets: 0,
110
111
  },
111
112
  ...overrides,
112
113
  };
@@ -32,9 +32,11 @@
32
32
  * DSH shell 把 UI 基础库作为 **static module seed** 预置在模块表里(`dsh-web-frontend` 的
33
33
  * `staticModules`:react / react-dom / cordis / client-store / ui-slots / ui-primitives / dockkit),
34
34
  * 插件 bundle 工厂拿到的 `require` 命中 seed 即可用 —— 第三方插件 dsh-better-sidebar 同样如此。
35
- * 本插件只用它画**文件类型图标**(DSH 文件树同一颗 `FileTypeIcon`),且**不是顶层 require**:
36
- * 调用点在 `src/client/chat/RollbackFileIcon.tsx` 的 `dshFileTypeIcon()` 里,外裹 try/catch ——
37
- * 老宿主没有这条 seed 时回落本地极简图标,绝不会因「一颗图标」打挂整个浏览器半。
35
+ * 本插件用它做两件事:**文件类型图标**(DSH 文件树同一颗 `FileTypeIcon`)与**用户正文投影**
36
+ * (`projectUserText` —— 会话引用/技能渲染成 chip,G-39 差距①②,2026-10-15)。
37
+ * **都不是顶层 require**:唯一入口是 `src/client/common/host-primitives.ts` 的帧内惰取门
38
+ * (三态缓存 + try/catch,每个进程只试一次)—— 老宿主没有这条 seed 时回落本地实现
39
+ * (极简图标 / 纯文本投影 / 可见的底色兜底),绝不会因一颗图标或一段正文打挂整个浏览器半。
38
40
  */
39
41
  export const CLIENT_BUNDLE_RUNTIME_WHITELIST: readonly string[] = [
40
42
  'react',
@@ -150,6 +150,18 @@ test('D-12 重复行不误判(LCS 而非逐行对齐)', () => {
150
150
  assert.ok(r.added + r.removed <= 2, '相同行的移动不应产生大量伪变更');
151
151
  });
152
152
 
153
+ test('D-14 缺省预算够到「大文件改多处」:旧口径 400 会降级,缺省 2000 给最小脚本(2026-10-15)', () => {
154
+ const before = Array.from({ length: 1200 }, (_, i) => `line ${i}`).join('\n');
155
+ const after = Array.from({ length: 1200 }, (_, i) => (i % 3 === 0 ? `changed ${i}` : `line ${i}`)).join('\n');
156
+ const old = diffLines(before, after, { maxD: 400 });
157
+ assert.equal(old.degraded, true, '旧口径(400)在这份输入上会退化成整块替换 —— 那正是用户看到的提示');
158
+ const now = diffLines(before, after);
159
+ assert.equal(now.degraded, false, '缺省预算必须能求出最小编辑脚本');
160
+ assert.equal(now.added, 400);
161
+ assert.equal(now.removed, 400);
162
+ assert.equal(apply(before, now), norm(after), '非降级路径同样要能回放重建新侧');
163
+ });
164
+
153
165
  test('D-13 looksBinary:NUL 字节判定;纯文本与空串为 false', () => {
154
166
  assert.equal(looksBinary('plain text\nwith lines'), false);
155
167
  assert.equal(looksBinary(''), false);
@@ -11,6 +11,12 @@
11
11
  * (maxLines / maxD)时**降级为整块替换**(全删 + 全增),保证任何输入都有确定
12
12
  * 耗时的输出 —— 大文件不会卡死宿主。
13
13
  *
14
+ * 预算怎么定(2026-10-15 真机实测,本仓 rollback-point.ts 的一对版本):
15
+ * · 时间是 (N+M)·D 量级:D=4000、两侧合计 ~7500 行时 ≈ 70 ms(D=8000 ≈ 115 ms);
16
+ * · 内存是 O(D²)·4B(回溯要逐轮留 V 快照):D=2000 时 RSS +~30 MB、D=4000 +~100 MB、D=8000 +~175 MB。
17
+ * ⇒ 缺省取 2000(覆盖「大文件只改了几处」这一大类);单文件按需查看的调用方可以往上调,
18
+ * 到 4000 仍是一次点击可接受的开销;再大就该换线性空间算法,而不是继续加预算。
19
+ *
14
20
  * 行为约定:
15
21
  * - 行尾 \r\n 归一为 \n;文本末尾的换行不产生"空行"(行数 = 换行符语义)。
16
22
  * - 仅空白差异同样是差异(不做 ignoreWhitespace)。
@@ -53,13 +59,13 @@ export interface DiffOptions {
53
59
  context?: number;
54
60
  /** 参与 Myers 的变更区行数上限(两侧合计;超出 → 降级整块替换) */
55
61
  maxLines?: number;
56
- /** Myers 最大编辑距离 D(超出 → 降级整块替换) */
62
+ /** Myers 最大编辑距离 D(超出 → 降级整块替换);越大越接近最小编辑脚本,内存随 D² 增长(见文件头) */
57
63
  maxD?: number;
58
64
  }
59
65
 
60
66
  const DEFAULT_CONTEXT = 3;
61
- const DEFAULT_MAX_LINES = 6000;
62
- const DEFAULT_MAX_D = 400;
67
+ const DEFAULT_MAX_LINES = 20000;
68
+ const DEFAULT_MAX_D = 2000;
63
69
 
64
70
  /** 文本 → 行数组(CRLF 归一、尾换行不产生空行)。 */
65
71
  export function splitLines(text: string): string[] {