cursor-guard 4.9.1 → 4.9.8

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 (60) hide show
  1. package/README.md +130 -10
  2. package/README.zh-CN.md +130 -10
  3. package/ROADMAP.md +65 -8
  4. package/SKILL.md +32 -22
  5. package/package.json +3 -2
  6. package/references/config-reference.md +68 -7
  7. package/references/config-reference.zh-CN.md +68 -7
  8. package/references/cursor-guard.example.json +11 -7
  9. package/references/cursor-guard.schema.json +30 -7
  10. package/references/dashboard/public/app.js +73 -27
  11. package/references/dashboard/public/index.html +8 -7
  12. package/references/lib/auto-backup.js +40 -2
  13. package/references/lib/core/backups.js +46 -16
  14. package/references/lib/core/core.test.js +101 -22
  15. package/references/lib/core/dashboard.js +37 -23
  16. package/references/lib/core/doctor.js +19 -13
  17. package/references/lib/core/pre-warning.js +296 -0
  18. package/references/lib/core/snapshot.js +24 -2
  19. package/references/lib/core/status.js +15 -7
  20. package/references/lib/utils.js +46 -20
  21. package/references/mcp/mcp.test.js +60 -12
  22. package/references/mcp/server.js +72 -60
  23. package/references/quickstart.zh-CN.md +46 -21
  24. package/references/vscode-extension/build-vsix.js +4 -1
  25. package/references/vscode-extension/dist/LICENSE +65 -0
  26. package/references/vscode-extension/dist/{cursor-guard-ide-4.9.1.vsix → cursor-guard-ide-4.9.8.vsix} +0 -0
  27. package/references/vscode-extension/dist/dashboard/public/app.js +73 -27
  28. package/references/vscode-extension/dist/dashboard/public/index.html +8 -7
  29. package/references/vscode-extension/dist/extension.js +406 -5
  30. package/references/vscode-extension/dist/guard-version.json +1 -1
  31. package/references/vscode-extension/dist/lib/auto-backup.js +40 -2
  32. package/references/vscode-extension/dist/lib/core/backups.js +46 -16
  33. package/references/vscode-extension/dist/lib/core/dashboard.js +37 -23
  34. package/references/vscode-extension/dist/lib/core/doctor.js +19 -13
  35. package/references/vscode-extension/dist/lib/core/pre-warning.js +296 -0
  36. package/references/vscode-extension/dist/lib/core/snapshot.js +24 -2
  37. package/references/vscode-extension/dist/lib/core/status.js +15 -7
  38. package/references/vscode-extension/dist/lib/dashboard-manager.js +102 -52
  39. package/references/vscode-extension/dist/lib/locale.js +36 -0
  40. package/references/vscode-extension/dist/lib/sidebar-webview.js +1027 -281
  41. package/references/vscode-extension/dist/lib/status-bar.js +95 -68
  42. package/references/vscode-extension/dist/lib/tree-view.js +174 -114
  43. package/references/vscode-extension/dist/lib/utils.js +46 -20
  44. package/references/vscode-extension/dist/mcp/server.js +395 -31
  45. package/references/vscode-extension/dist/media/brand-placeholder.png +0 -0
  46. package/references/vscode-extension/dist/package.json +1 -1
  47. package/references/vscode-extension/dist/skill/ROADMAP.md +65 -8
  48. package/references/vscode-extension/dist/skill/SKILL.md +32 -22
  49. package/references/vscode-extension/dist/skill/config-reference.md +68 -7
  50. package/references/vscode-extension/dist/skill/config-reference.zh-CN.md +68 -7
  51. package/references/vscode-extension/dist/skill/cursor-guard.example.json +11 -7
  52. package/references/vscode-extension/dist/skill/cursor-guard.schema.json +30 -7
  53. package/references/vscode-extension/extension.js +406 -5
  54. package/references/vscode-extension/lib/dashboard-manager.js +102 -52
  55. package/references/vscode-extension/lib/locale.js +36 -0
  56. package/references/vscode-extension/lib/sidebar-webview.js +1027 -281
  57. package/references/vscode-extension/lib/status-bar.js +95 -68
  58. package/references/vscode-extension/lib/tree-view.js +174 -114
  59. package/references/vscode-extension/media/brand-placeholder.png +0 -0
  60. package/references/vscode-extension/package.json +1 -1
@@ -60,9 +60,16 @@ const I18N = {
60
60
  'alert.action.deleted': 'Deleted',
61
61
  'alert.action.renamed': 'Renamed',
62
62
  'alert.breakdown': '{added} added, {modified} modified, {deleted} deleted',
63
- 'alert.suggestion': 'Check recent changes and consider creating a manual snapshot',
64
- 'alert.viewFiles': 'View file details ({n} files)',
65
- 'modal.alertFiles': 'Alert File Details',
63
+ 'alert.suggestion': 'Check recent changes and consider creating a manual snapshot',
64
+ 'alert.viewFiles': 'View file details ({n} files)',
65
+ 'preWarning.title': 'Pre-Warning',
66
+ 'preWarning.none': 'No destructive edit risk detected',
67
+ 'preWarning.active': 'Delete Risk',
68
+ 'preWarning.file': 'File',
69
+ 'preWarning.risk': 'Risk',
70
+ 'preWarning.methods': 'Methods removed',
71
+ 'preWarning.suggestion': 'Review this deletion before applying or restore from the latest snapshot',
72
+ 'modal.alertFiles': 'Alert File Details',
66
73
  'modal.col.restore': 'Restore',
67
74
  'modal.copyRestore': 'Copy cmd',
68
75
  'modal.restorePreDelete':'Restore pre-delete',
@@ -170,8 +177,9 @@ const I18N = {
170
177
  'issue.disk_critically_low': 'Disk space critically low ({gb} GB free)',
171
178
  'issue.disk_low': 'Disk space low ({gb} GB free)',
172
179
  'issue.git_backup_stale': 'Last git backup is stale ({rel})',
173
- 'issue.active_alert': 'Active alert: {type} — {count} files in {window}s',
174
- 'issue.alert_high_velocity': 'High volume of file changes detected. Consider reviewing recent modifications and creating a manual snapshot.',
180
+ 'issue.active_alert': 'Active alert: {type} — {count} files in {window}s',
181
+ 'issue.pre_warning_active': 'Pre-warning active: {summary}',
182
+ 'issue.alert_high_velocity': 'High volume of file changes detected. Consider reviewing recent modifications and creating a manual snapshot.',
175
183
 
176
184
  'check.Git installed': 'Git installed',
177
185
  'check.Git repository': 'Git repository',
@@ -288,9 +296,16 @@ const I18N = {
288
296
  'alert.action.deleted': '删除',
289
297
  'alert.action.renamed': '重命名',
290
298
  'alert.breakdown': '新增 {added} · 修改 {modified} · 删除 {deleted}',
291
- 'alert.suggestion': '建议检查近期变更,并考虑手动创建快照',
292
- 'alert.viewFiles': '查看文件详情({n} 个文件)',
293
- 'modal.alertFiles': '告警文件详情',
299
+ 'alert.suggestion': '建议检查近期变更,并考虑手动创建快照',
300
+ 'alert.viewFiles': '查看文件详情({n} 个文件)',
301
+ 'preWarning.title': '事先预警',
302
+ 'preWarning.none': '未检测到破坏性编辑风险',
303
+ 'preWarning.active': '删除风险',
304
+ 'preWarning.file': '文件',
305
+ 'preWarning.risk': '风险',
306
+ 'preWarning.methods': '移除的方法数',
307
+ 'preWarning.suggestion': '建议在应用前检查这次删除,或直接从最新快照恢复',
308
+ 'modal.alertFiles': '告警文件详情',
294
309
  'modal.col.restore': '恢复',
295
310
  'modal.copyRestore': '复制命令',
296
311
  'modal.restorePreDelete':'恢复删除前',
@@ -399,7 +414,8 @@ const I18N = {
399
414
  'issue.disk_low': '磁盘空间不足({gb} GB 可用)',
400
415
  'issue.git_backup_stale': '最近 Git 备份已过时({rel})',
401
416
  'issue.active_alert': '活跃告警:{type}——{count} 个文件在 {window} 秒内变更',
402
- 'issue.alert_high_velocity': '检测到大量文件变更,建议检查最近修改并手动创建快照。',
417
+ 'issue.pre_warning_active': '事先预警生效:{summary}',
418
+ 'issue.alert_high_velocity': '检测到大量文件变更,建议检查最近修改并手动创建快照。',
403
419
 
404
420
  'check.Git installed': 'Git 安装状态',
405
421
  'check.Git repository': 'Git 仓库',
@@ -557,8 +573,9 @@ const ISSUE_PATTERNS = [
557
573
  { re: /^Disk space low \((.+?) GB free\)$/, key: 'issue.disk_low', extract: ['gb'] },
558
574
  { re: /^Last git backup is stale \((.+?)\)$/, key: 'issue.git_backup_stale', extract: ['rel'] },
559
575
  { re: /^Active alert: (.+?) — (\d+) files in (\d+)s$/, key: 'issue.active_alert', extract: ['type', 'count', 'window'] },
560
- { re: /^High volume of file changes/, key: 'issue.alert_high_velocity' },
561
- ];
576
+ { re: /^Pre-warning active: (.+)$/, key: 'issue.pre_warning_active', extract: ['summary'] },
577
+ { re: /^High volume of file changes/, key: 'issue.alert_high_velocity' },
578
+ ];
562
579
 
563
580
  function translateIssue(text) {
564
581
  for (const p of ISSUE_PATTERNS) {
@@ -850,13 +867,14 @@ function renderAll() {
850
867
 
851
868
  /* ── Rendering: Overview ──────────────────────────────────── */
852
869
 
853
- function renderOverview(d) {
854
- renderHealthCard(d.health);
855
- renderGitBackupCard(d.lastBackup);
856
- renderShadowBackupCard(d.lastBackup);
857
- renderWatcherCard(d.watcher);
858
- renderAlertCard(d.alerts);
859
- }
870
+ function renderOverview(d) {
871
+ renderHealthCard(d.health);
872
+ renderGitBackupCard(d.lastBackup);
873
+ renderShadowBackupCard(d.lastBackup);
874
+ renderPreWarningCard(d.preWarnings);
875
+ renderWatcherCard(d.watcher);
876
+ renderAlertCard(d.alerts);
877
+ }
860
878
 
861
879
  function renderHealthCard(health) {
862
880
  const el = $('#card-health');
@@ -889,9 +907,9 @@ function renderGitBackupCard(lastBackup) {
889
907
  `;
890
908
  }
891
909
 
892
- function renderShadowBackupCard(lastBackup) {
893
- const el = $('#card-shadow-backup');
894
- const s = lastBackup?.shadow;
910
+ function renderShadowBackupCard(lastBackup) {
911
+ const el = $('#card-shadow-backup');
912
+ const s = lastBackup?.shadow;
895
913
  if (!s) {
896
914
  el.innerHTML = `<div class="card-label">${t('shadowBackup.title')}</div><div class="card-empty">${t('shadowBackup.none')}</div>`;
897
915
  return;
@@ -899,12 +917,40 @@ function renderShadowBackupCard(lastBackup) {
899
917
  el.innerHTML = `
900
918
  <div class="card-label">${t('shadowBackup.title')}</div>
901
919
  <div class="card-value">${esc(relativeTime(s.timestamp))}</div>
902
- <div class="card-detail text-muted text-sm">${esc(formatTime(s.timestamp))}</div>
903
- `;
904
- }
905
-
906
- function renderWatcherCard(watcher) {
907
- const el = $('#card-watcher');
920
+ <div class="card-detail text-muted text-sm">${esc(formatTime(s.timestamp))}</div>
921
+ `;
922
+ }
923
+
924
+ function renderPreWarningCard(preWarnings) {
925
+ const el = $('#card-prewarning');
926
+ if (!preWarnings?.active) {
927
+ el.innerHTML = `
928
+ <div class="card-label">${t('preWarning.title')}</div>
929
+ <div class="card-status"><span class="status-dot status-healthy"></span><span>${t('preWarning.none')}</span></div>
930
+ `;
931
+ return;
932
+ }
933
+
934
+ const warning = preWarnings.latest || {};
935
+ const file = warning.file || '-';
936
+ const risk = warning.riskPercent !== undefined ? `${warning.riskPercent}%` : '-';
937
+ const methods = warning.removedMethodCount || 0;
938
+
939
+ el.innerHTML = `
940
+ <div class="card-label">${t('preWarning.title')}</div>
941
+ <div class="card-status"><span class="status-dot status-warning"></span><span class="status-text status-warning">${t('preWarning.active')}</span></div>
942
+ <div class="alert-details">
943
+ <div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.file')}</span><span class="text-mono">${esc(file)}</span></div>
944
+ <div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.risk')}</span><span>${esc(risk)}</span></div>
945
+ ${methods > 0 ? `<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.methods')}</span><span>${methods}</span></div>` : ''}
946
+ <div class="alert-detail-row alert-breakdown text-sm">${esc(warning.summary || t('preWarning.suggestion'))}</div>
947
+ <div class="alert-detail-row alert-suggestion text-sm text-muted">${t('preWarning.suggestion')}</div>
948
+ </div>
949
+ `;
950
+ }
951
+
952
+ function renderWatcherCard(watcher) {
953
+ const el = $('#card-watcher');
908
954
  let st = 'stopped';
909
955
  if (watcher?.running) st = 'running';
910
956
  else if (watcher?.stale) st = 'stale';
@@ -44,13 +44,14 @@
44
44
  <section id="screen-overview" class="screen">
45
45
  <h2 class="section-title" data-i18n="overview.title">Overview</h2>
46
46
  <div id="overview-grid" class="card-grid">
47
- <div id="card-health" class="card card-health"><div class="skeleton-block"></div></div>
48
- <div id="card-git-backup" class="card"><div class="skeleton-block"></div></div>
49
- <div id="card-shadow-backup" class="card"><div class="skeleton-block"></div></div>
50
- <div id="card-watcher" class="card"><div class="skeleton-block"></div></div>
51
- <div id="card-alert" class="card"><div class="skeleton-block"></div></div>
52
- </div>
53
- </section>
47
+ <div id="card-health" class="card card-health"><div class="skeleton-block"></div></div>
48
+ <div id="card-git-backup" class="card"><div class="skeleton-block"></div></div>
49
+ <div id="card-shadow-backup" class="card"><div class="skeleton-block"></div></div>
50
+ <div id="card-prewarning" class="card"><div class="skeleton-block"></div></div>
51
+ <div id="card-watcher" class="card"><div class="skeleton-block"></div></div>
52
+ <div id="card-alert" class="card"><div class="skeleton-block"></div></div>
53
+ </div>
54
+ </section>
54
55
 
55
56
  <!-- Screen 2: Backups & Recovery ─────────────────────── -->
56
57
  <section id="screen-backups" class="screen">
@@ -11,18 +11,377 @@ const { Poller } = require('./lib/poller');
11
11
  const { SidebarDashboardProvider } = require('./lib/sidebar-webview');
12
12
  const { autoSetup } = require('./lib/auto-setup');
13
13
  const { guardPath } = require('./lib/paths');
14
+ const { getLocale } = require('./lib/locale');
15
+
16
+ let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider, _localeStorage;
17
+ const _preWarningBaselines = new Map();
18
+ const _preWarningTimers = new Map();
19
+ const _preWarningFingerprints = new Map();
20
+ const PRE_WARNING_DEBOUNCE_MS = 200;
21
+ const PRE_WARNING_POPUP_COOLDOWN_MS = 5000;
22
+ const PRE_WARNING_AUTO_CONTINUE_MS = 2000;
23
+ const PRE_WARNING_POPUP_TICK_MS = 250;
24
+ const PRE_WARNING_I18N = {
25
+ 'en-US': {
26
+ 'diff.title': 'Cursor Guard Pre-Warning: {file}',
27
+ 'picker.title': 'Cursor Guard Review · auto-continue in {time}',
28
+ 'picker.placeholder': 'Risky deletion detected in {file} · {summary}',
29
+ 'picker.methods': 'methods',
30
+ 'action.undo.label': '$(discard) Undo Change',
31
+ 'action.undo.description': 'Revert this deletion now',
32
+ 'action.undo.detail': 'Safest option. Restore the latest edit batch immediately.',
33
+ 'action.diff.label': '$(diff) View Diff',
34
+ 'action.diff.description': 'Inspect before and after',
35
+ 'action.diff.detail': 'Open a diff view and keep the current edit for now.',
36
+ 'action.keep.label': '$(check) Keep Changes',
37
+ 'action.keep.description': 'Accept this edit',
38
+ 'action.keep.detail': 'Dismiss this warning and continue working.',
39
+ 'status.diffOpen': '$(shield) Cursor Guard review open for {file}',
40
+ 'summary.methods.one': '{n} method removed',
41
+ 'summary.methods.other': '{n} methods removed',
42
+ 'summary.lines.one': '{n} line deleted',
43
+ 'summary.lines.other': '{n} lines deleted',
44
+ 'summary.risk': 'risk {n}%',
45
+ },
46
+ 'zh-CN': {
47
+ 'diff.title': 'Cursor Guard 预警对比:{file}',
48
+ 'picker.title': 'Cursor Guard \u9884\u8b66 - {time} \u540e\u81ea\u52a8\u7ee7\u7eed',
49
+ 'picker.placeholder': '\u68c0\u6d4b\u5230\u5220\u9664\u98ce\u9669\uff1a{file} - {summary}',
50
+ 'picker.methods': '方法',
51
+ 'action.undo.label': '$(discard) 撤销此次修改',
52
+ 'action.undo.description': '立即回退这次删除',
53
+ 'action.undo.detail': '这是最安全的选择,会马上撤销刚刚这一批编辑。',
54
+ 'action.diff.label': '$(diff) 查看 Diff',
55
+ 'action.diff.description': '先对比再决定',
56
+ 'action.diff.detail': '打开前后差异视图,暂时保留当前修改。',
57
+ 'action.keep.label': '$(check) 保留修改',
58
+ 'action.keep.description': '接受这次编辑',
59
+ 'action.keep.detail': '关闭这次预警并继续当前流程。',
60
+ 'status.diffOpen': '$(shield) Cursor Guard 已打开预警对比:{file}',
61
+ 'summary.methods.one': '删除了 {n} 个方法',
62
+ 'summary.methods.other': '删除了 {n} 个方法',
63
+ 'summary.lines.one': '删除了 {n} 行',
64
+ 'summary.lines.other': '删除了 {n} 行',
65
+ 'summary.risk': '风险 {n}%',
66
+ },
67
+ };
68
+
69
+ function _docKey(document) {
70
+ return document.uri.toString();
71
+ }
72
+
73
+ function _uiLocale() {
74
+ return getLocale(_localeStorage);
75
+ }
76
+
77
+ function _tp(key, params) {
78
+ const locale = _uiLocale();
79
+ const dict = PRE_WARNING_I18N[locale] || PRE_WARNING_I18N['en-US'];
80
+ let value = dict[key] || PRE_WARNING_I18N['en-US'][key] || key;
81
+ for (const [name, replacement] of Object.entries(params || {})) {
82
+ value = value.replaceAll(`{${name}}`, String(replacement));
83
+ }
84
+ return value;
85
+ }
86
+
87
+ function _pluralKey(baseKey, count) {
88
+ return `${baseKey}.${count === 1 ? 'one' : 'other'}`;
89
+ }
90
+
91
+ function _buildPreWarningSummary(warning) {
92
+ const parts = [];
93
+ if (warning.removedMethodCount > 0) {
94
+ parts.push(_tp(_pluralKey('summary.methods', warning.removedMethodCount), {
95
+ n: warning.removedMethodCount,
96
+ }));
97
+ }
98
+ if (warning.deletedLines > 0) {
99
+ parts.push(_tp(_pluralKey('summary.lines', warning.deletedLines), {
100
+ n: warning.deletedLines,
101
+ }));
102
+ }
103
+ parts.push(_tp('summary.risk', { n: warning.riskPercent || 0 }));
104
+ return parts.join(_uiLocale() === 'zh-CN' ? ',' : ', ');
105
+ }
106
+
107
+ function _getPreWarningDeps() {
108
+ return {
109
+ loadConfig: require(guardPath('lib', 'utils')).loadConfig,
110
+ ...require(guardPath('lib', 'core', 'pre-warning')),
111
+ };
112
+ }
113
+
114
+ function _getProjectInfo(document) {
115
+ if (!document || document.uri.scheme !== 'file') return null;
116
+ const folder = vscode.workspace.getWorkspaceFolder(document.uri);
117
+ if (!folder) return null;
118
+ const projectPath = folder.uri.fsPath;
119
+ const relPath = path.relative(projectPath, document.uri.fsPath).replace(/\\/g, '/');
120
+ if (!relPath || relPath.startsWith('..')) return null;
121
+ return { projectPath, relPath };
122
+ }
123
+
124
+ function _seedBaseline(document) {
125
+ if (!document || document.uri.scheme !== 'file') return;
126
+ _preWarningBaselines.set(_docKey(document), document.getText());
127
+ }
128
+
129
+ function _clearDocPreWarningState(document) {
130
+ const key = _docKey(document);
131
+ const timer = _preWarningTimers.get(key);
132
+ if (timer) clearTimeout(timer);
133
+ _preWarningTimers.delete(key);
134
+ _preWarningBaselines.delete(key);
135
+ _preWarningFingerprints.delete(key);
136
+ }
137
+
138
+ function _isLikelyDeleteBatch(event) {
139
+ const deletedChars = event.contentChanges.reduce((sum, change) => {
140
+ return sum + Math.max(0, change.rangeLength - (change.text || '').length);
141
+ }, 0);
142
+ const deletedLines = event.contentChanges.reduce((sum, change) => {
143
+ return sum + Math.max(0, change.range.end.line - change.range.start.line);
144
+ }, 0);
145
+ return deletedChars >= 8
146
+ || deletedLines > 0
147
+ || event.contentChanges.some(change => change.rangeLength >= 20 || (change.rangeLength > 0 && !change.text));
148
+ }
149
+
150
+ async function _openPreWarningDiff(document, previousText) {
151
+ const beforeDoc = await vscode.workspace.openTextDocument({
152
+ language: document.languageId,
153
+ content: previousText,
154
+ });
155
+ await vscode.commands.executeCommand(
156
+ 'vscode.diff',
157
+ beforeDoc.uri,
158
+ document.uri,
159
+ _tp('diff.title', { file: path.basename(document.uri.fsPath) })
160
+ );
161
+ }
162
+
163
+ function _formatPreWarningCountdown(remainingMs) {
164
+ return `${Math.max(1, Math.ceil(remainingMs / 1000))}s`;
165
+ }
166
+
167
+ async function _showPreWarningPopup(document, relPath, previousText, warning, projectPath) {
168
+ const { clearPreWarning } = _getPreWarningDeps();
169
+ const summary = _buildPreWarningSummary(warning);
170
+ const methodHint = Array.isArray(warning.removedMethods) && warning.removedMethods.length > 0
171
+ ? warning.removedMethods.slice(0, 3).map(m => `${m.name}:${m.lineNumber}`).join(', ')
172
+ : '';
173
+ const summaryLine = methodHint
174
+ ? `${summary} - ${_tp('picker.methods')}: ${methodHint}`
175
+ : summary;
176
+
177
+ return new Promise((resolve) => {
178
+ const picker = vscode.window.createQuickPick();
179
+ const items = [
180
+ {
181
+ label: '$(discard) Undo Change',
182
+ description: 'Revert this deletion now',
183
+ detail: 'Safest option. Restore the latest edit batch immediately.',
184
+ action: 'undo',
185
+ },
186
+ {
187
+ label: '$(diff) View Diff',
188
+ description: 'Inspect before and after',
189
+ detail: 'Open a diff view and keep the current edit for now.',
190
+ action: 'diff',
191
+ },
192
+ {
193
+ label: '$(check) Keep Changes',
194
+ description: 'Accept this edit',
195
+ detail: 'Dismiss this warning and continue working.',
196
+ action: 'keep',
197
+ },
198
+ ];
199
+
200
+ let settled = false;
201
+ let remainingMs = PRE_WARNING_AUTO_CONTINUE_MS;
202
+ let timer = null;
203
+
204
+ const finish = async (action) => {
205
+ if (settled) return;
206
+ settled = true;
207
+ if (timer) clearInterval(timer);
208
+ try { picker.hide(); } catch { /* ignore */ }
209
+ picker.dispose();
210
+
211
+ if (action === 'undo') {
212
+ await vscode.window.showTextDocument(document, { preview: false, preserveFocus: false });
213
+ await vscode.commands.executeCommand('undo');
214
+ clearPreWarning(projectPath, relPath);
215
+ if (poller) poller.forceRefresh();
216
+ resolve('undo');
217
+ return;
218
+ }
219
+
220
+ if (action === 'diff') {
221
+ clearPreWarning(projectPath, relPath);
222
+ if (poller) poller.forceRefresh();
223
+ await _openPreWarningDiff(document, previousText);
224
+ vscode.window.setStatusBarMessage(
225
+ _tp('status.diffOpen', { file: path.basename(relPath) }),
226
+ 2500
227
+ );
228
+ resolve('diff');
229
+ return;
230
+ }
231
+
232
+ clearPreWarning(projectPath, relPath);
233
+ if (poller) poller.forceRefresh();
234
+ resolve(action);
235
+ };
236
+
237
+ const updatePicker = () => {
238
+ picker.title = _tp('picker.title', {
239
+ time: _formatPreWarningCountdown(remainingMs),
240
+ });
241
+ picker.placeholder = _tp('picker.placeholder', {
242
+ file: relPath,
243
+ summary: summaryLine,
244
+ });
245
+ picker.items = [
246
+ {
247
+ label: _tp('action.undo.label'),
248
+ description: _tp('action.undo.description'),
249
+ detail: _tp('action.undo.detail'),
250
+ action: 'undo',
251
+ },
252
+ {
253
+ label: _tp('action.diff.label'),
254
+ description: _tp('action.diff.description'),
255
+ detail: _tp('action.diff.detail'),
256
+ action: 'diff',
257
+ },
258
+ {
259
+ label: _tp('action.keep.label'),
260
+ description: _tp('action.keep.description'),
261
+ detail: _tp('action.keep.detail'),
262
+ action: 'keep',
263
+ },
264
+ ];
265
+ picker.matchOnDescription = true;
266
+ picker.matchOnDetail = true;
267
+ picker.ignoreFocusOut = false;
268
+ };
269
+
270
+ picker.onDidAccept(() => {
271
+ const selected = picker.selectedItems[0];
272
+ finish(selected?.action || 'keep').catch(() => resolve());
273
+ });
274
+ picker.onDidHide(() => {
275
+ if (!settled) {
276
+ finish('timeout').catch(() => resolve());
277
+ }
278
+ });
279
+
280
+ updatePicker();
281
+ picker.show();
282
+
283
+ timer = setInterval(() => {
284
+ remainingMs -= PRE_WARNING_POPUP_TICK_MS;
285
+ if (remainingMs <= 0) {
286
+ finish('timeout').catch(() => resolve());
287
+ return;
288
+ }
289
+ updatePicker();
290
+ }, PRE_WARNING_POPUP_TICK_MS);
291
+ });
292
+ }
293
+
294
+ async function _assessPreWarning(document) {
295
+ if (!document || document.isClosed || document.uri.scheme !== 'file') return;
296
+
297
+ const info = _getProjectInfo(document);
298
+ if (!info) return;
14
299
 
15
- let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider;
300
+ const key = _docKey(document);
301
+ const previousText = _preWarningBaselines.get(key) ?? document.getText();
302
+ const deps = _getPreWarningDeps();
303
+ const { cfg } = deps.loadConfig(info.projectPath);
304
+
305
+ if (!deps.isPreWarningEnabled(cfg) || deps.shouldExcludePreWarning(info.relPath, cfg)) {
306
+ deps.clearPreWarning(info.projectPath, info.relPath);
307
+ _preWarningFingerprints.delete(key);
308
+ if (poller) poller.forceRefresh();
309
+ return;
310
+ }
311
+
312
+ const assessment = deps.assessDeletionRisk(previousText, document.getText(), {
313
+ threshold: cfg.pre_warning_threshold,
314
+ });
315
+
316
+ if (!assessment.triggered) {
317
+ deps.clearPreWarning(info.projectPath, info.relPath);
318
+ _preWarningFingerprints.delete(key);
319
+ if (poller) poller.forceRefresh();
320
+ return;
321
+ }
322
+
323
+ const warning = {
324
+ file: info.relPath,
325
+ mode: cfg.pre_warning_mode,
326
+ threshold: cfg.pre_warning_threshold,
327
+ deletedLines: assessment.deletedLines,
328
+ removedMethodCount: assessment.removedMethodCount,
329
+ removedMethods: assessment.removedMethods.slice(0, 10),
330
+ riskPercent: assessment.riskPercent,
331
+ summary: assessment.summary,
332
+ deletedLineSamples: assessment.deletedLineSamples,
333
+ };
334
+
335
+ const fingerprint = JSON.stringify([
336
+ warning.file,
337
+ warning.deletedLines,
338
+ warning.removedMethodCount,
339
+ warning.riskPercent,
340
+ warning.removedMethods.map(m => m.name).join(','),
341
+ ]);
342
+ const last = _preWarningFingerprints.get(key);
343
+
344
+ if (cfg.pre_warning_mode === 'popup') {
345
+ if (last?.suppressed) return;
346
+ if (last && last.fingerprint === fingerprint && Date.now() - last.at < PRE_WARNING_POPUP_COOLDOWN_MS) {
347
+ return;
348
+ }
349
+ }
350
+
351
+ deps.recordPreWarning(info.projectPath, warning, {
352
+ setActive: cfg.pre_warning_mode === 'dashboard',
353
+ });
354
+ if (poller) poller.forceRefresh();
355
+
356
+ if (cfg.pre_warning_mode === 'silent') {
357
+ _preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: false });
358
+ console.warn(`[cursor-guard] pre-warning ${info.relPath}: ${warning.summary}`);
359
+ return;
360
+ }
361
+
362
+ if (cfg.pre_warning_mode === 'popup') {
363
+ const action = await _showPreWarningPopup(document, info.relPath, previousText, warning, info.projectPath);
364
+ if (action === 'undo') {
365
+ _preWarningFingerprints.delete(key);
366
+ return;
367
+ }
368
+ _preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: true });
369
+ return;
370
+ }
371
+
372
+ _preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: false });
373
+ }
16
374
 
17
375
  async function activate(context) {
18
376
  await autoSetup(context, vscode);
377
+ _localeStorage = context.globalState;
19
378
 
20
379
  dashMgr = new DashboardManager();
21
380
  poller = new Poller(dashMgr);
22
381
  statusBar = new StatusBarController(poller);
23
382
  treeView = new GuardTreeView(poller, dashMgr);
24
383
  webviewProvider = new WebViewProvider(context, dashMgr);
25
- sidebarProvider = new SidebarDashboardProvider(poller);
384
+ sidebarProvider = new SidebarDashboardProvider(poller, context);
26
385
 
27
386
  context.subscriptions.push(
28
387
  vscode.window.registerWebviewViewProvider('cursorGuardDashboard', sidebarProvider),
@@ -127,7 +486,7 @@ async function activate(context) {
127
486
  const summary = b.summary ? b.summary.slice(0, 60) : '';
128
487
  return {
129
488
  label: `$(git-commit) ${time}`,
130
- description: `${b.type || 'auto'} · ${files}`,
489
+ description: `${b.type || 'auto'} - ${files}`,
131
490
  detail: summary,
132
491
  hash: b.commitHash,
133
492
  };
@@ -161,7 +520,7 @@ async function activate(context) {
161
520
  } else if (warned > 0) {
162
521
  vscode.window.showWarningMessage(`Cursor Guard: ${msg}`);
163
522
  } else {
164
- vscode.window.showInformationMessage(`Cursor Guard: ${msg} ✓`);
523
+ vscode.window.showInformationMessage(`Cursor Guard: ${msg}`);
165
524
  }
166
525
  } catch (e) {
167
526
  vscode.window.showErrorMessage(`Cursor Guard Doctor: ${e.message}`);
@@ -189,7 +548,6 @@ async function activate(context) {
189
548
  vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
190
549
  }
191
550
 
192
- // Event-driven UI refresh: FileSystemWatcher triggers immediate poller refresh
193
551
  let _fsRefreshTimer = null;
194
552
  const _scheduleRefresh = () => {
195
553
  if (_fsRefreshTimer) clearTimeout(_fsRefreshTimer);
@@ -201,7 +559,50 @@ async function activate(context) {
201
559
  fileWatcher.onDidDelete(_scheduleRefresh);
202
560
  context.subscriptions.push(fileWatcher);
203
561
 
562
+ for (const document of vscode.workspace.textDocuments) {
563
+ _seedBaseline(document);
564
+ }
565
+
204
566
  context.subscriptions.push(
567
+ vscode.workspace.onDidOpenTextDocument(document => {
568
+ _seedBaseline(document);
569
+ }),
570
+ vscode.workspace.onDidCloseTextDocument(document => {
571
+ const info = _getProjectInfo(document);
572
+ _clearDocPreWarningState(document);
573
+ if (info) {
574
+ const { clearPreWarning } = _getPreWarningDeps();
575
+ clearPreWarning(info.projectPath, info.relPath);
576
+ if (poller) poller.forceRefresh();
577
+ }
578
+ }),
579
+ vscode.workspace.onDidSaveTextDocument(document => {
580
+ const info = _getProjectInfo(document);
581
+ _seedBaseline(document);
582
+ _preWarningFingerprints.delete(_docKey(document));
583
+ if (info) {
584
+ const { clearPreWarning } = _getPreWarningDeps();
585
+ clearPreWarning(info.projectPath, info.relPath);
586
+ if (poller) poller.forceRefresh();
587
+ }
588
+ }),
589
+ vscode.workspace.onDidChangeTextDocument(event => {
590
+ const info = _getProjectInfo(event.document);
591
+ if (!info || !_isLikelyDeleteBatch(event)) return;
592
+
593
+ const key = _docKey(event.document);
594
+ const existing = _preWarningTimers.get(key);
595
+ if (existing) clearTimeout(existing);
596
+
597
+ const timer = setTimeout(() => {
598
+ _preWarningTimers.delete(key);
599
+ _assessPreWarning(event.document).catch(err => {
600
+ console.warn(`[cursor-guard] pre-warning failed for ${info.relPath}: ${err.message}`);
601
+ });
602
+ }, PRE_WARNING_DEBOUNCE_MS);
603
+
604
+ _preWarningTimers.set(key, timer);
605
+ }),
205
606
  vscode.workspace.onDidChangeWorkspaceFolders(async () => {
206
607
  const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
207
608
  if (restarted && !poller._timer) poller.start();
@@ -1 +1 @@
1
- {"version":"4.9.1"}
1
+ {"version":"4.9.8"}