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
@@ -1,39 +1,73 @@
1
1
  'use strict';
2
2
 
3
3
  const vscode = require('vscode');
4
+ const { getLocale, setLocale } = require('./locale');
4
5
 
5
6
  class SidebarDashboardProvider {
6
- constructor(poller) {
7
+ constructor(poller, context) {
7
8
  this._poller = poller;
9
+ this._extensionUri = context?.extensionUri;
10
+ this._localeStorage = context?.globalState;
11
+ this._locale = getLocale(this._localeStorage);
8
12
  this._view = null;
9
13
  this._sub = poller.onChange(data => this._push(data));
10
14
  }
11
15
 
12
16
  resolveWebviewView(webviewView) {
13
17
  this._view = webviewView;
14
- webviewView.webview.options = { enableScripts: true };
15
- webviewView.webview.html = _getHtml();
18
+ const webview = webviewView.webview;
19
+ webview.options = { enableScripts: true };
16
20
 
17
- webviewView.webview.onDidReceiveMessage(msg => {
18
- if (msg.cmd === 'ready') this._push(this._poller.data);
21
+ let brandInnerHtml = '';
22
+ if (this._extensionUri) {
23
+ const logoUri = webview.asWebviewUri(
24
+ vscode.Uri.joinPath(this._extensionUri, 'media', 'brand-placeholder.png')
25
+ );
26
+ brandInnerHtml =
27
+ '<img class="cg-brand-mark-img" src="' +
28
+ escHtmlAttr(logoUri.toString()) +
29
+ '" alt="" draggable="false" />';
30
+ }
31
+
32
+ webview.html = _getHtml(brandInnerHtml);
33
+
34
+ webviewView.webview.onDidReceiveMessage(async msg => {
35
+ if (msg.cmd === 'ready') {
36
+ this._postLocale();
37
+ this._push(this._poller.data);
38
+ }
39
+ if (msg.cmd === 'setLocale') {
40
+ this._locale = await setLocale(this._localeStorage, msg.locale);
41
+ this._postLocale();
42
+ }
19
43
  if (msg.cmd === 'exec') vscode.commands.executeCommand(msg.command);
20
44
  });
21
45
 
22
46
  webviewView.onDidChangeVisibility(() => {
23
- if (webviewView.visible) this._push(this._poller.data);
47
+ if (webviewView.visible) {
48
+ this._postLocale();
49
+ this._push(this._poller.data);
50
+ }
24
51
  });
25
52
  }
26
53
 
54
+ _postLocale() {
55
+ if (!this._view) return;
56
+ this._view.webview.postMessage({ type: 'locale', locale: this._locale });
57
+ }
58
+
27
59
  _push(data) {
28
60
  if (!this._view?.visible) return;
61
+
29
62
  const payload = {};
30
- for (const [id, p] of data) {
63
+ for (const [id, project] of data) {
31
64
  payload[id] = {
32
- name: p.name || id,
33
- dashboard: p.dashboard,
34
- backups: (p.backups || []).slice(0, 5),
65
+ name: project.name || id,
66
+ dashboard: project.dashboard,
67
+ backups: (project.backups || []).slice(0, 5),
35
68
  };
36
69
  }
70
+
37
71
  this._view.webview.postMessage({ type: 'update', data: payload });
38
72
  }
39
73
 
@@ -42,218 +76,883 @@ class SidebarDashboardProvider {
42
76
  }
43
77
  }
44
78
 
45
- function _getHtml() {
79
+ function escHtmlAttr(value) {
80
+ return String(value)
81
+ .replace(/&/g, '&amp;')
82
+ .replace(/"/g, '&quot;');
83
+ }
84
+
85
+ function _getHtml(brandInnerHtml) {
86
+ brandInnerHtml = brandInnerHtml || '';
87
+ const brandMarkClass =
88
+ 'cg-brand-mark' + (brandInnerHtml ? ' cg-brand-mark--has-img' : '');
46
89
  return `<!DOCTYPE html>
47
90
  <html lang="en">
48
91
  <head>
49
92
  <meta charset="UTF-8">
50
93
  <style>
51
94
  :root {
52
- --bg: #1e1e2e;
53
- --surface: #282838;
54
- --border: #383850;
55
- --text: #cdd6f4;
56
- --dim: #6c7086;
57
- --green: #a6e3a1;
58
- --red: #f38ba8;
59
- --yellow: #f9e2af;
60
- --blue: #89b4fa;
61
- --purple: #cba6f7;
62
- --orange: #fab387;
63
- --teal: #94e2d5;
64
- --radius: 8px;
65
- }
66
- * { margin: 0; padding: 0; box-sizing: border-box; }
95
+ --surface: var(--vscode-sideBar-background, #1f2430);
96
+ --surface-2: var(--vscode-editorWidget-background, #252a38);
97
+ --border: var(--vscode-widget-border, rgba(120, 130, 160, 0.22));
98
+ --text: var(--vscode-foreground, #e8eaf0);
99
+ --muted: var(--vscode-descriptionForeground, #9aa4bd);
100
+ --green: var(--vscode-testing-iconPassed, #89d18a);
101
+ --yellow: var(--vscode-editorWarning-foreground, #e4c06a);
102
+ --red: var(--vscode-testing-iconFailed, #f0a0a0);
103
+ --orange: var(--vscode-charts-orange, #f0b070);
104
+ --blue: var(--vscode-textLink-foreground, #8eb6ff);
105
+ --radius: 12px;
106
+ --radius-lg: 14px;
107
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
108
+ --shadow-soft: 0 4px 14px rgba(0, 0, 0, 0.12);
109
+ --accent: var(--blue);
110
+ --glow-green: color-mix(in srgb, var(--green) 35%, transparent);
111
+ --glow-blue: color-mix(in srgb, var(--blue) 28%, transparent);
112
+ }
113
+
114
+ * { box-sizing: border-box; }
67
115
  body {
68
- font: 12px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
116
+ margin: 0;
117
+ padding: 0;
118
+ font: 13px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
69
119
  color: var(--text);
70
120
  background: transparent;
71
- padding: 10px;
121
+ -webkit-font-smoothing: antialiased;
72
122
  }
73
123
 
74
- /* ── Big status indicator ── */
75
- .status-hero {
76
- text-align: center;
77
- padding: 14px 10px;
78
- border-radius: var(--radius);
79
- margin-bottom: 10px;
124
+ .cg-shell {
125
+ position: relative;
126
+ padding: 10px 10px 18px;
127
+ min-height: 100%;
128
+ background:
129
+ radial-gradient(120% 80% at 0% -20%, var(--glow-blue), transparent 55%),
130
+ radial-gradient(90% 60% at 100% 0%, var(--glow-green), transparent 45%);
80
131
  }
81
- .status-hero.protected {
82
- background: rgba(166,227,161,0.1);
83
- border: 1px solid rgba(166,227,161,0.3);
132
+
133
+ .cg-brand {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 10px;
137
+ margin-bottom: 14px;
138
+ padding: 10px 12px;
139
+ border-radius: var(--radius-lg);
140
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
141
+ background: color-mix(in srgb, var(--surface-2) 75%, transparent);
142
+ box-shadow: var(--shadow-soft);
143
+ backdrop-filter: blur(8px);
84
144
  }
85
- .status-hero.alert {
86
- background: rgba(243,139,168,0.12);
87
- border: 1px solid rgba(243,139,168,0.4);
145
+
146
+ .cg-brand-mark {
147
+ width: 36px;
148
+ height: 36px;
149
+ border-radius: 10px;
150
+ flex-shrink: 0;
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ overflow: hidden;
155
+ background: linear-gradient(135deg, color-mix(in srgb, var(--blue) 55%, #1a1a2e), color-mix(in srgb, var(--green) 40%, #1a1a2e));
156
+ box-shadow:
157
+ 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent) inset,
158
+ 0 4px 12px color-mix(in srgb, var(--blue) 25%, transparent);
88
159
  }
89
- .status-hero.stopped {
90
- background: rgba(249,226,175,0.1);
91
- border: 1px solid rgba(249,226,175,0.3);
160
+
161
+ .cg-brand-mark--has-img {
162
+ background: color-mix(in srgb, var(--surface-2) 88%, var(--text));
163
+ padding: 4px;
164
+ box-shadow:
165
+ 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent) inset,
166
+ 0 2px 10px color-mix(in srgb, var(--blue) 18%, transparent);
92
167
  }
93
- .status-hero.critical {
94
- background: rgba(243,139,168,0.15);
95
- border: 1px solid var(--red);
168
+
169
+ .cg-brand-mark-img {
170
+ width: 100%;
171
+ height: 100%;
172
+ object-fit: contain;
173
+ display: block;
174
+ pointer-events: none;
175
+ user-select: none;
96
176
  }
97
- .status-icon { font-size: 28px; display: block; margin-bottom: 4px; }
98
- .status-text { font-size: 15px; font-weight: 700; }
99
- .status-sub { font-size: 11px; color: var(--dim); margin-top: 2px; }
100
177
 
101
- /* ── Alert card ── */
102
- .alert-card {
103
- background: rgba(243,139,168,0.1);
104
- border: 1px solid rgba(243,139,168,0.35);
105
- border-radius: var(--radius);
106
- padding: 10px 12px;
107
- margin-bottom: 10px;
178
+ .cg-brand-text {
179
+ display: flex;
180
+ flex-direction: column;
181
+ gap: 2px;
182
+ min-width: 0;
183
+ flex: 1;
108
184
  }
109
- .alert-card .title { color: var(--red); font-weight: 700; font-size: 12px; }
110
- .alert-card .detail { color: var(--dim); font-size: 11px; margin-top: 3px; }
111
- .alert-card .actions { margin-top: 6px; display: flex; gap: 6px; }
112
- .alert-card .btn-sm {
113
- font-size: 10px; padding: 3px 8px; border-radius: 4px;
114
- border: 1px solid var(--border); background: var(--surface);
115
- color: var(--text); cursor: pointer;
185
+
186
+ .cg-brand-title {
187
+ font-size: 14px;
188
+ font-weight: 800;
189
+ letter-spacing: -0.03em;
190
+ line-height: 1.2;
191
+ background: linear-gradient(90deg, var(--text), color-mix(in srgb, var(--text) 72%, var(--blue)));
192
+ -webkit-background-clip: text;
193
+ background-clip: text;
194
+ color: transparent;
116
195
  }
117
- .alert-card .btn-sm:hover { border-color: var(--blue); color: var(--blue); }
118
196
 
119
- /* ── Quick stats ── */
120
- .stats-card {
121
- background: var(--surface);
122
- border: 1px solid var(--border);
123
- border-radius: var(--radius);
124
- padding: 10px 12px;
125
- margin-bottom: 10px;
197
+ .cg-brand-meta {
198
+ display: flex;
199
+ flex-direction: column;
200
+ gap: 3px;
201
+ min-width: 0;
202
+ margin-top: 1px;
203
+ }
204
+
205
+ .cg-brand-sub {
206
+ font-weight: 600;
207
+ opacity: 0.88;
208
+ }
209
+
210
+ .cg-brand-sub--project {
211
+ font-size: 11px;
212
+ letter-spacing: 0.02em;
213
+ color: color-mix(in srgb, var(--text) 92%, var(--muted));
214
+ overflow: hidden;
215
+ text-overflow: ellipsis;
216
+ white-space: nowrap;
126
217
  }
127
- .stats-card .label-sm {
128
- font-size: 9px; font-weight: 700; text-transform: uppercase;
129
- letter-spacing: 0.8px; color: var(--dim); margin-bottom: 6px;
218
+
219
+ .cg-brand-sub--backup {
220
+ font-size: 10px;
221
+ letter-spacing: 0.1em;
222
+ text-transform: uppercase;
223
+ color: var(--muted);
130
224
  }
131
- .stat-row {
225
+
226
+ .cg-brand-backup-prefix {
227
+ font-weight: 600;
228
+ color: var(--muted);
229
+ margin-right: 2px;
230
+ }
231
+
232
+ #cg-brand-backup .backup-age[data-backup-ts] {
233
+ color: var(--green);
234
+ font-weight: 700;
235
+ letter-spacing: 0.06em;
236
+ }
237
+
238
+ .cg-brand-tools {
132
239
  display: flex;
133
- justify-content: space-between;
134
240
  align-items: center;
135
- padding: 3px 0;
241
+ justify-content: flex-end;
242
+ flex-shrink: 0;
243
+ }
244
+
245
+ .lang-btn {
246
+ border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
247
+ border-radius: 999px;
248
+ background: color-mix(in srgb, var(--surface-2) 85%, transparent);
249
+ color: var(--text);
250
+ padding: 6px 11px;
251
+ font: inherit;
136
252
  font-size: 11px;
253
+ font-weight: 700;
254
+ letter-spacing: 0.03em;
255
+ cursor: pointer;
256
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
137
257
  }
138
- .stat-row .name { color: var(--dim); }
139
- .stat-row .val { font-weight: 600; color: var(--text); }
140
- .stat-row .val.green { color: var(--green); }
141
- .stat-row .val.blue { color: var(--blue); }
142
- .stat-row .val.yellow { color: var(--yellow); }
143
258
 
144
- /* ── Action buttons ── */
145
- .actions-section {
146
- margin-top: 8px;
259
+ .lang-btn:hover {
260
+ border-color: color-mix(in srgb, var(--blue) 55%, var(--border));
261
+ color: var(--blue);
262
+ box-shadow: var(--shadow-soft);
147
263
  }
148
- .actions-grid {
149
- display: grid;
150
- grid-template-columns: 1fr 1fr;
151
- gap: 6px;
264
+
265
+ .lang-btn:focus-visible {
266
+ outline: 2px solid color-mix(in srgb, var(--blue) 70%, transparent);
267
+ outline-offset: 2px;
152
268
  }
153
- .action-btn {
154
- padding: 8px 6px;
155
- font-size: 11px;
156
- font-weight: 600;
269
+
270
+ .empty {
271
+ padding: 26px 12px;
157
272
  text-align: center;
273
+ color: var(--muted);
274
+ }
275
+
276
+ .hero {
277
+ position: relative;
278
+ margin-bottom: 12px;
279
+ padding: 16px 14px;
280
+ border-radius: var(--radius-lg);
158
281
  border: 1px solid var(--border);
159
- border-radius: var(--radius);
160
- background: var(--surface);
161
- color: var(--text);
282
+ background: color-mix(in srgb, var(--vscode-editor-background, #1e1e1e) 92%, transparent);
283
+ box-shadow: var(--shadow);
284
+ overflow: hidden;
285
+ }
286
+
287
+ .hero::before {
288
+ content: "";
289
+ position: absolute;
290
+ left: 0;
291
+ top: 0;
292
+ right: 0;
293
+ height: 3px;
294
+ opacity: 0.85;
295
+ background: linear-gradient(90deg, var(--muted), var(--muted));
296
+ pointer-events: none;
297
+ }
298
+
299
+ .hero.protected::before {
300
+ background: linear-gradient(90deg, var(--green), var(--blue));
301
+ box-shadow: 0 0 16px var(--glow-green);
302
+ }
303
+
304
+ .hero.risk::before {
305
+ background: linear-gradient(90deg, var(--orange), var(--yellow));
306
+ }
307
+
308
+ .hero.alert::before,
309
+ .hero.critical::before {
310
+ background: linear-gradient(90deg, var(--red), var(--orange));
311
+ }
312
+
313
+ .hero.stopped::before {
314
+ background: linear-gradient(90deg, var(--yellow), var(--muted));
315
+ }
316
+
317
+ .hero-top {
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: space-between;
321
+ gap: 8px;
322
+ }
323
+
324
+ .hero-top .hero-kicker {
325
+ margin: 0;
326
+ }
327
+
328
+ .cg-pulse-dot {
329
+ width: 8px;
330
+ height: 8px;
331
+ border-radius: 50%;
332
+ background: var(--green);
333
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 45%, transparent);
334
+ animation: cg-pulse 2.2s ease-out infinite;
335
+ flex-shrink: 0;
336
+ }
337
+
338
+ @keyframes cg-pulse {
339
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 45%, transparent); opacity: 1; }
340
+ 70% { box-shadow: 0 0 0 8px transparent; opacity: 0.85; }
341
+ 100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
342
+ }
343
+
344
+ @media (prefers-reduced-motion: reduce) {
345
+ .cg-pulse-dot { animation: none; box-shadow: 0 0 6px color-mix(in srgb, var(--green) 35%, transparent); }
346
+ .card { transition: none; }
347
+ .card-chevron { transition: none; }
348
+ .card-head { transition: none; }
349
+ .btn { transition: none; }
350
+ .lang-btn { transition: none; }
351
+ }
352
+
353
+ .hero.risk {
354
+ border-color: rgba(244, 179, 110, 0.45);
355
+ background: rgba(244, 179, 110, 0.12);
356
+ }
357
+
358
+ .hero.alert {
359
+ border-color: rgba(242, 159, 159, 0.45);
360
+ background: rgba(242, 159, 159, 0.12);
361
+ }
362
+
363
+ .hero.stopped {
364
+ border-color: rgba(245, 213, 133, 0.45);
365
+ background: rgba(245, 213, 133, 0.10);
366
+ }
367
+
368
+ .hero.critical {
369
+ border-color: rgba(242, 159, 159, 0.60);
370
+ background: rgba(242, 159, 159, 0.16);
371
+ }
372
+
373
+ .hero.protected {
374
+ border-color: rgba(154, 215, 162, 0.45);
375
+ background: rgba(154, 215, 162, 0.10);
376
+ }
377
+
378
+ .hero-kicker {
379
+ font-size: 10px;
380
+ letter-spacing: 0.1em;
381
+ text-transform: uppercase;
382
+ color: var(--muted);
383
+ font-weight: 600;
384
+ opacity: 0.92;
385
+ }
386
+
387
+ .hero-title {
388
+ margin-top: 8px;
389
+ font-size: 18px;
390
+ font-weight: 800;
391
+ letter-spacing: -0.03em;
392
+ line-height: 1.2;
393
+ }
394
+
395
+ .hero-sub {
396
+ margin-top: 6px;
397
+ color: var(--muted);
398
+ font-size: 12px;
399
+ line-height: 1.45;
400
+ opacity: 0.95;
401
+ }
402
+
403
+ .card {
404
+ position: relative;
405
+ margin-bottom: 12px;
406
+ padding: 12px 14px;
407
+ border-radius: var(--radius-lg);
408
+ border: 1px solid color-mix(in srgb, var(--border) 90%, transparent);
409
+ background: linear-gradient(
410
+ 165deg,
411
+ color-mix(in srgb, var(--surface-2) 100%, var(--text)) 0%,
412
+ color-mix(in srgb, var(--surface-2) 96%, transparent) 100%
413
+ );
414
+ box-shadow: var(--shadow);
415
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
416
+ }
417
+
418
+ .card:hover {
419
+ border-color: color-mix(in srgb, var(--border) 70%, var(--blue));
420
+ box-shadow: var(--shadow-soft);
421
+ }
422
+
423
+ .card.risk-card {
424
+ border-color: rgba(244, 179, 110, 0.45);
425
+ }
426
+
427
+ .card.alert-card {
428
+ border-color: rgba(242, 159, 159, 0.45);
429
+ }
430
+
431
+ .card.cg-collapsible {
432
+ padding-top: 0;
433
+ padding-bottom: 12px;
434
+ }
435
+
436
+ .card-head {
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: space-between;
440
+ gap: 10px;
441
+ width: calc(100% + 28px);
442
+ margin: 0 -14px 0 -14px;
443
+ padding: 12px 14px;
444
+ border: none;
445
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
446
+ background: transparent;
447
+ color: inherit;
162
448
  cursor: pointer;
163
- transition: all 0.15s;
449
+ text-align: left;
450
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
451
+ transition: background 0.15s ease;
452
+ }
453
+
454
+ .card-head:hover {
455
+ background: color-mix(in srgb, var(--text) 5%, transparent);
456
+ }
457
+
458
+ .card.cg-collapsible.is-collapsed .card-head {
459
+ border-bottom-color: transparent;
460
+ }
461
+
462
+ .card-head .card-title {
463
+ margin: 0;
464
+ padding: 0;
465
+ border: none;
466
+ flex: 1;
467
+ min-width: 0;
164
468
  }
165
- .action-btn:hover { border-color: var(--blue); color: var(--blue); background: rgba(137,180,250,0.08); }
166
- .action-btn.primary {
167
- border-color: rgba(137,180,250,0.3);
168
- background: rgba(137,180,250,0.08);
469
+
470
+ .card-title {
471
+ margin-bottom: 10px;
472
+ font-size: 10px;
473
+ font-weight: 700;
474
+ letter-spacing: 0.1em;
475
+ text-transform: uppercase;
476
+ color: var(--muted);
477
+ padding-bottom: 8px;
478
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
169
479
  }
170
- .action-btn.full { grid-column: 1 / -1; }
171
- .action-btn .icon { margin-right: 3px; }
172
480
 
173
- /* ── Scope display ── */
174
- .scope-summary {
481
+ .card-chevron {
482
+ display: flex;
483
+ align-items: center;
484
+ justify-content: center;
485
+ width: 22px;
486
+ height: 22px;
487
+ border-radius: 6px;
488
+ font-size: 11px;
489
+ line-height: 1;
490
+ color: var(--muted);
491
+ background: color-mix(in srgb, var(--text) 6%, transparent);
492
+ transition: transform 0.2s ease, background 0.15s ease;
493
+ flex-shrink: 0;
494
+ }
495
+
496
+ .card-head:hover .card-chevron {
497
+ background: color-mix(in srgb, var(--text) 10%, transparent);
498
+ color: var(--text);
499
+ }
500
+
501
+ .card.is-collapsed .card-chevron {
502
+ transform: rotate(-90deg);
503
+ }
504
+
505
+ .card-panel {
506
+ padding-top: 12px;
507
+ }
508
+
509
+ .card-panel .actions {
510
+ margin-top: 10px;
511
+ }
512
+
513
+ .card.is-collapsed .card-panel {
514
+ display: none;
515
+ }
516
+
517
+ .row {
518
+ display: flex;
519
+ justify-content: space-between;
520
+ align-items: flex-start;
521
+ gap: 12px;
522
+ padding: 5px 0;
523
+ }
524
+
525
+ .row-name {
526
+ color: var(--muted);
527
+ }
528
+
529
+ .row-value {
530
+ text-align: right;
531
+ font-weight: 600;
532
+ font-variant-numeric: tabular-nums;
533
+ }
534
+
535
+ .row-value.green { color: var(--green); }
536
+ .row-value.blue { color: var(--blue); }
537
+ .row-value.yellow { color: var(--yellow); }
538
+ .row-value.orange { color: var(--orange); }
539
+ .row-value.red { color: var(--red); }
540
+
541
+ .pill-wrap {
175
542
  display: flex;
176
543
  flex-wrap: wrap;
177
544
  gap: 6px;
178
545
  margin-bottom: 8px;
179
546
  }
180
- .scope-chip {
547
+
548
+ .pill {
549
+ padding: 4px 10px;
550
+ border-radius: 999px;
181
551
  font-size: 11px;
182
552
  font-weight: 600;
183
- padding: 3px 8px;
184
- border-radius: 10px;
185
- }
186
- .scope-chip.protected {
187
- background: rgba(166,227,161,0.12);
188
- color: var(--green);
189
- }
190
- .scope-chip.excluded {
191
- background: rgba(243,139,168,0.12);
192
- color: var(--red);
553
+ border: 1px solid transparent;
193
554
  }
194
- .scope-chip.total {
195
- background: rgba(108,112,134,0.15);
196
- color: var(--dim);
197
- }
198
- .scope-block { margin-bottom: 6px; }
199
- .scope-label {
555
+
556
+ .pill.green { background: rgba(154, 215, 162, 0.12); color: var(--green); }
557
+ .pill.red { background: rgba(242, 159, 159, 0.12); color: var(--red); }
558
+ .pill.orange { background: rgba(244, 179, 110, 0.12); color: var(--orange); }
559
+ .pill.dim { background: rgba(154, 164, 189, 0.12); color: var(--muted); }
560
+
561
+ .tag-group { margin-top: 8px; }
562
+ .tag-label {
563
+ margin-bottom: 4px;
200
564
  font-size: 10px;
201
565
  font-weight: 700;
566
+ letter-spacing: 0.06em;
202
567
  text-transform: uppercase;
203
- letter-spacing: 0.5px;
204
- display: block;
205
- margin-bottom: 4px;
568
+ color: var(--muted);
206
569
  }
207
- .scope-label.green { color: var(--green); }
208
- .scope-label.red { color: var(--red); }
209
- .scope-tags {
570
+
571
+ .tag-list {
210
572
  display: flex;
211
573
  flex-wrap: wrap;
212
574
  gap: 4px;
213
575
  }
214
- .scope-tag {
215
- font-size: 10px;
216
- padding: 2px 6px;
217
- border-radius: 4px;
218
- font-family: 'Cascadia Code', 'Fira Code', monospace;
576
+
577
+ .tag {
219
578
  max-width: 100%;
220
579
  overflow: hidden;
221
580
  text-overflow: ellipsis;
222
581
  white-space: nowrap;
582
+ padding: 4px 8px;
583
+ border-radius: 8px;
584
+ border: 1px solid var(--border);
585
+ font: 10px/1.45 ui-monospace, Consolas, "Cascadia Code", monospace;
223
586
  }
224
- .scope-tag.green {
225
- background: rgba(166,227,161,0.1);
587
+
588
+ .tag.green {
226
589
  color: var(--green);
227
- border: 1px solid rgba(166,227,161,0.2);
590
+ border-color: rgba(154, 215, 162, 0.3);
591
+ background: rgba(154, 215, 162, 0.08);
228
592
  }
229
- .scope-tag.red {
230
- background: rgba(243,139,168,0.08);
593
+
594
+ .tag.red {
231
595
  color: var(--red);
232
- border: 1px solid rgba(243,139,168,0.2);
596
+ border-color: rgba(242, 159, 159, 0.3);
597
+ background: rgba(242, 159, 159, 0.08);
598
+ }
599
+
600
+ .tag.dim {
601
+ color: var(--muted);
233
602
  }
234
- .scope-tag.dim {
235
- background: rgba(108,112,134,0.1);
236
- color: var(--dim);
603
+
604
+ .cg-actions-wrap {
605
+ margin-top: 4px;
606
+ padding-top: 14px;
607
+ border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
608
+ }
609
+
610
+ .actions {
611
+ display: grid;
612
+ grid-template-columns: 1fr 1fr;
613
+ gap: 8px;
614
+ margin-top: 0;
615
+ }
616
+
617
+ .btn {
237
618
  border: 1px solid var(--border);
619
+ border-radius: var(--radius);
620
+ background: color-mix(in srgb, var(--surface-2) 88%, var(--text));
621
+ color: var(--text);
622
+ padding: 9px 8px;
623
+ font: inherit;
624
+ font-weight: 600;
625
+ font-size: 12px;
626
+ cursor: pointer;
627
+ transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
628
+ }
629
+
630
+ .btn:hover {
631
+ border-color: color-mix(in srgb, var(--blue) 55%, var(--border));
632
+ color: var(--blue);
633
+ box-shadow: var(--shadow-soft);
634
+ }
635
+
636
+ .btn:focus-visible {
637
+ outline: 2px solid color-mix(in srgb, var(--blue) 70%, transparent);
638
+ outline-offset: 2px;
639
+ }
640
+
641
+ .btn.primary {
642
+ background: linear-gradient(
643
+ 165deg,
644
+ color-mix(in srgb, var(--blue) 22%, var(--surface-2)),
645
+ color-mix(in srgb, var(--blue) 10%, var(--surface-2))
646
+ );
647
+ border-color: color-mix(in srgb, var(--blue) 45%, var(--border));
238
648
  }
239
649
 
240
- .empty-state {
241
- text-align: center; padding: 24px 10px;
242
- color: var(--dim); font-size: 12px;
650
+ .btn.primary:hover {
651
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--blue) 25%, transparent), var(--shadow-soft);
652
+ }
653
+
654
+ .btn.full {
655
+ grid-column: 1 / -1;
243
656
  }
244
657
  </style>
245
658
  </head>
246
659
  <body>
247
- <div id="root">
248
- <div class="empty-state">Waiting for data...</div>
660
+ <div class="cg-shell">
661
+ <header class="cg-brand" aria-label="Cursor Guard">
662
+ <div class="${brandMarkClass}" aria-hidden="true">${brandInnerHtml}</div>
663
+ <div class="cg-brand-text">
664
+ <span class="cg-brand-title" id="cg-brand-title">Cursor Guard</span>
665
+ <div class="cg-brand-meta">
666
+ <span class="cg-brand-sub cg-brand-sub--project" id="cg-brand-project">-</span>
667
+ <div class="cg-brand-sub cg-brand-sub--backup" id="cg-brand-backup">
668
+ <span class="cg-brand-backup-prefix" id="cg-brand-backup-prefix" hidden>Last backup </span><span id="cg-brand-backup-age" class="backup-age">-</span>
669
+ </div>
670
+ </div>
671
+ </div>
672
+ <div class="cg-brand-tools">
673
+ <button id="lang-toggle" class="lang-btn" type="button">中文</button>
674
+ </div>
675
+ </header>
676
+ <div id="root">
677
+ <div class="empty">Waiting for data...</div>
678
+ </div>
249
679
  </div>
250
680
  <script>
251
681
  const vscode = acquireVsCodeApi();
682
+ const root = document.getElementById('root');
683
+ const brandTitle = document.getElementById('cg-brand-title');
684
+ const brandProject = document.getElementById('cg-brand-project');
685
+ const brandBackupPrefix = document.getElementById('cg-brand-backup-prefix');
686
+ const brandBackupAge = document.getElementById('cg-brand-backup-age');
687
+ const langToggle = document.getElementById('lang-toggle');
688
+ const savedState = vscode.getState() || {};
689
+ let _locale = savedState.locale || ((navigator.language || '').toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US');
252
690
  let _alertExpiresAt = 0;
691
+ let _projects = {};
692
+
693
+ const I18N = {
694
+ 'en-US': {
695
+ 'chrome.title': 'Cursor Guard',
696
+ 'chrome.switch': '\u4e2d\u6587',
697
+ 'state.waiting': 'Waiting for data...',
698
+ 'state.loading': 'Loading...',
699
+ 'state.empty': 'No projects detected.<br>Add .cursor-guard.json to get started.',
700
+ 'brand.noWorkspace': 'No workspace',
701
+ 'brand.addConfig': 'Add .cursor-guard.json',
702
+ 'brand.loadingBackup': 'Loading backup...',
703
+ 'brand.noGitBackup': 'No Git backup yet',
704
+ 'brand.backupPrefix': 'Last backup',
705
+ 'hero.pre.kicker': 'Pre-Warning',
706
+ 'hero.pre.title': 'Delete Risk',
707
+ 'hero.pre.subtitle': 'Review pending destructive edit',
708
+ 'hero.alert.kicker': 'Change Alert',
709
+ 'hero.alert.subtitle': 'Abnormal change velocity detected',
710
+ 'hero.protection.kicker': 'Protection',
711
+ 'hero.protection.stopped': 'Watcher Stopped',
712
+ 'hero.protection.stoppedSub': 'Start watcher to enable continuous protection',
713
+ 'hero.health.kicker': 'Health',
714
+ 'hero.health.critical': 'Critical Issue',
715
+ 'hero.health.check': 'Check diagnostics',
716
+ 'hero.protection.safe': 'Protected',
717
+ 'hero.protection.safeSub': 'Watcher running and backups healthy',
718
+ 'card.deletionRisk': 'Deletion Risk',
719
+ 'card.activeAlert': 'Active Alert',
720
+ 'card.quickStats': 'Quick Stats',
721
+ 'card.protectionScope': 'Protection Scope',
722
+ 'row.file': 'File',
723
+ 'row.risk': 'Risk',
724
+ 'row.methodsRemoved': 'Methods removed',
725
+ 'row.summary': 'Summary',
726
+ 'row.window': 'Window',
727
+ 'row.files': 'Files',
728
+ 'row.threshold': 'Threshold',
729
+ 'row.expires': 'Expires',
730
+ 'row.watcher': 'Watcher',
731
+ 'row.health': 'Health',
732
+ 'row.lastBackup': 'Last backup',
733
+ 'row.gitBackups': 'Git backups',
734
+ 'row.shadowCopies': 'Shadow copies',
735
+ 'row.diskFree': 'Disk free',
736
+ 'status.watcher.running': 'Running',
737
+ 'status.watcher.stale': 'Stale Lock',
738
+ 'status.watcher.stopped': 'Stopped',
739
+ 'status.health.healthy': 'Healthy',
740
+ 'status.health.warning': 'Warning',
741
+ 'status.health.critical': 'Critical',
742
+ 'pill.protected': '{n} protected',
743
+ 'pill.excluded': '{n} excluded',
744
+ 'pill.total': '{n} total',
745
+ 'tag.protect': 'Protect',
746
+ 'tag.ignore': 'Ignore',
747
+ 'tag.more': '+{n} more',
748
+ 'actions.openDashboard': 'Open Dashboard',
749
+ 'actions.restore': 'Restore',
750
+ 'actions.viewDetails': 'View Details',
751
+ 'actions.snapshot': 'Snapshot',
752
+ 'actions.watcherOn': 'Stop Watcher',
753
+ 'actions.watcherOff': 'Start Watcher',
754
+ 'actions.doctor': 'Doctor',
755
+ 'stats.never': 'never',
756
+ 'misc.unknown': 'Unknown',
757
+ 'misc.na': 'N/A',
758
+ 'time.secondsAgo': '{n}s ago',
759
+ 'time.minutesAgo': '{m}m {s}s ago',
760
+ 'time.hoursAgo': '{h}h {m}m ago',
761
+ 'time.daysAgo': '{d}d ago',
762
+ 'time.seconds': '{n}s',
763
+ 'time.minutes': '{m}m {s}s',
764
+ 'alert.filesChangedFast': '{count} files changed fast'
765
+ },
766
+ 'zh-CN': {
767
+ 'chrome.title': 'Cursor Guard',
768
+ 'chrome.switch': 'EN',
769
+ 'state.waiting': '绛夊緟鏁版嵁涓?..',
770
+ 'state.loading': '鍔犺浇涓?..',
771
+ 'state.empty': '鏈娴嬪埌椤圭洰銆?br>娣诲姞 .cursor-guard.json 鍚庡嵆鍙惎鐢ㄣ€?,
772
+ 'hero.pre.kicker': '浜嬪厛棰勮',
773
+ 'hero.pre.title': '鍒犻櫎椋庨櫓',
774
+ 'hero.pre.subtitle': '璇峰厛妫€鏌ヨ繖娆$牬鍧忔€х紪杈?,
775
+ 'hero.alert.kicker': '鍙樻洿鍛婅',
776
+ 'hero.alert.subtitle': '妫€娴嬪埌寮傚父楂橀鏂囦欢鍙樻洿',
777
+ 'hero.protection.kicker': '淇濇姢鐘舵€?,
778
+ 'hero.protection.stopped': 'Watcher 鏈繍琛?,
779
+ 'hero.protection.stoppedSub': '鍚姩 watcher 浠ュ紑鍚寔缁繚鎶?,
780
+ 'hero.health.kicker': '鍋ュ悍鐘舵€?,
781
+ 'hero.health.critical': '涓ラ噸闂',
782
+ 'hero.health.check': '璇锋鏌ヨ瘖鏂粨鏋?,
783
+ 'hero.protection.safe': '淇濇姢涓?,
784
+ 'hero.protection.safeSub': 'Watcher 姝e湪杩愯锛屽浠界姸鎬佸仴搴?,
785
+ 'card.deletionRisk': '鍒犻櫎椋庨櫓',
786
+ 'card.activeAlert': '娲昏穬鍛婅',
787
+ 'card.quickStats': '蹇€熸瑙?,
788
+ 'card.protectionScope': '淇濇姢鑼冨洿',
789
+ 'row.file': '鏂囦欢',
790
+ 'row.risk': '椋庨櫓',
791
+ 'row.methodsRemoved': '绉婚櫎鐨勬柟娉曟暟',
792
+ 'row.summary': '鎽樿',
793
+ 'row.window': '绐楀彛',
794
+ 'row.files': '鏂囦欢鏁?,
795
+ 'row.threshold': '闃堝€?,
796
+ 'row.expires': '鍓╀綑鏃堕棿',
797
+ 'row.watcher': '\u76d1\u63a7',
798
+ 'row.health': '\u5065\u5eb7',
799
+ 'row.lastBackup': '涓婃澶囦唤',
800
+ 'row.gitBackups': 'Git 澶囦唤鏁?,
801
+ 'row.shadowCopies': 'Shadow 澶囦唤鏁?,
802
+ 'row.diskFree': '鍓╀綑纾佺洏',
803
+ 'status.watcher.running': '\u8fd0\u884c\u4e2d',
804
+ 'status.watcher.stale': '\u9501\u6b8b\u7559',
805
+ 'status.watcher.stopped': '\u5df2\u505c\u6b62',
806
+ 'status.health.healthy': '\u5065\u5eb7',
807
+ 'status.health.warning': '\u8b66\u544a',
808
+ 'status.health.critical': '\u4e25\u91cd',
809
+ 'pill.protected': '{n} 涓彈淇濇姢',
810
+ 'pill.excluded': '{n} 涓帓闄?,
811
+ 'pill.total': '{n} 涓€昏',
812
+ 'tag.protect': '淇濇姢',
813
+ 'tag.ignore': '蹇界暐',
814
+ 'tag.more': '+{n} 涓洿澶?,
815
+ 'actions.openDashboard': '鎵撳紑鐪嬫澘',
816
+ 'actions.restore': '鎭㈠',
817
+ 'actions.viewDetails': '鏌ョ湅璇︽儏',
818
+ 'actions.snapshot': '绔嬪嵆蹇収',
819
+ 'actions.watcherOn': '\u505c\u6b62 Watcher',
820
+ 'actions.watcherOff': '\u542f\u52a8 Watcher',
821
+ 'actions.doctor': '璇婃柇',
822
+ 'brand.noWorkspace': '\u65e0\u5de5\u4f5c\u533a',
823
+ 'brand.addConfig': '\u6dfb\u52a0 .cursor-guard.json',
824
+ 'brand.loadingBackup': '\u5907\u4efd\u4fe1\u606f\u52a0\u8f7d\u4e2d...',
825
+ 'brand.noGitBackup': '\u6682\u65e0 Git \u5907\u4efd',
826
+ 'brand.backupPrefix': '\u4e0a\u6b21\u5907\u4efd',
827
+ 'stats.never': '\u4ece\u672a',
828
+ 'misc.unknown': '\u672a\u77e5',
829
+ 'misc.na': 'N/A',
830
+ 'time.secondsAgo': '{n} 绉掑墠',
831
+ 'time.minutesAgo': '{m} 鍒?{s} 绉掑墠',
832
+ 'time.hoursAgo': '{h} 灏忔椂 {m} 鍒嗗墠',
833
+ 'time.daysAgo': '{d} 澶╁墠',
834
+ 'time.seconds': '{n} 绉?,
835
+ 'time.minutes': '{m} 鍒?{s} 绉?,
836
+ 'alert.filesChangedFast': '{count} 涓枃浠跺揩閫熷彉鏇?
837
+ }
838
+ };
839
+
840
+ function t(key, params) {
841
+ const dict = I18N[_locale] || I18N['en-US'];
842
+ let value = dict[key] || I18N['en-US'][key] || key;
843
+ for (const [name, replacement] of Object.entries(params || {})) {
844
+ value = value.replaceAll('{' + name + '}', String(replacement));
845
+ }
846
+ return value;
847
+ }
848
+
849
+ function setLocale(locale, opts) {
850
+ _locale = locale === 'zh-CN' ? 'zh-CN' : 'en-US';
851
+ document.documentElement.lang = _locale === 'zh-CN' ? 'zh-CN' : 'en';
852
+ vscode.setState({ locale: _locale });
853
+ if (!opts || opts.syncHost !== false) {
854
+ vscode.postMessage({ cmd: 'setLocale', locale: _locale });
855
+ }
856
+ updateChrome();
857
+ if (!opts || opts.render !== false) {
858
+ render(_projects);
859
+ }
860
+ }
861
+
862
+ function toggleLocale() {
863
+ setLocale(_locale === 'zh-CN' ? 'en-US' : 'zh-CN');
864
+ }
865
+
866
+ function updateChrome() {
867
+ document.documentElement.lang = _locale === 'zh-CN' ? 'zh-CN' : 'en';
868
+ brandTitle.textContent = t('chrome.title');
869
+ langToggle.textContent = t('chrome.switch');
870
+ updateBrandBar(_projects);
871
+ }
872
+
873
+ function formatRelativeAge(ms) {
874
+ const sec = Math.floor((Date.now() - ms) / 1000);
875
+ if (sec < 60) return t('time.secondsAgo', { n: sec });
876
+ if (sec < 3600) return t('time.minutesAgo', { m: Math.floor(sec / 60), s: sec % 60 });
877
+ if (sec < 86400) return t('time.hoursAgo', { h: Math.floor(sec / 3600), m: Math.floor((sec % 3600) / 60) });
878
+ return t('time.daysAgo', { d: Math.floor(sec / 86400) });
879
+ }
880
+
881
+ function formatCountdown(seconds) {
882
+ if (seconds > 60) return t('time.minutes', { m: Math.floor(seconds / 60), s: seconds % 60 });
883
+ return t('time.seconds', { n: seconds });
884
+ }
253
885
 
254
- window.addEventListener('message', e => {
255
- if (e.data.type === 'update') render(e.data.data);
886
+ function displayCount(value) {
887
+ return value == null ? '?' : String(value);
888
+ }
889
+
890
+ function pickPrimaryProject(projects) {
891
+ const ids = Object.keys(projects || {});
892
+ for (let i = 0; i < ids.length; i++) {
893
+ const id = ids[i];
894
+ if (projects[id] && projects[id].dashboard) return { id, project: projects[id], ids };
895
+ }
896
+ if (ids.length) return { id: ids[0], project: projects[ids[0]], ids };
897
+ return null;
898
+ }
899
+
900
+ function updateBrandBar(projects) {
901
+ const primary = pickPrimaryProject(projects || {});
902
+
903
+ if (!primary) {
904
+ brandProject.textContent = t('brand.noWorkspace');
905
+ brandProject.removeAttribute('title');
906
+ brandBackupPrefix.hidden = true;
907
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
908
+ brandBackupAge.removeAttribute('data-backup-ts');
909
+ brandBackupAge.textContent = t('brand.addConfig');
910
+ return;
911
+ }
912
+
913
+ const project = primary.project || {};
914
+ const dashboard = project.dashboard;
915
+ let name = project.name || primary.id;
916
+ if (primary.ids.length > 1) {
917
+ name += ' +' + (primary.ids.length - 1);
918
+ }
919
+ brandProject.textContent = name;
920
+ brandProject.title = name;
921
+
922
+ if (!dashboard) {
923
+ brandBackupPrefix.hidden = true;
924
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
925
+ brandBackupAge.removeAttribute('data-backup-ts');
926
+ brandBackupAge.textContent = t('brand.loadingBackup');
927
+ return;
928
+ }
929
+
930
+ const gitTs = dashboard.lastBackup?.git?.timestamp;
931
+ if (gitTs) {
932
+ const ts = new Date(gitTs).getTime();
933
+ brandBackupPrefix.hidden = false;
934
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
935
+ brandBackupAge.dataset.backupTs = String(ts);
936
+ brandBackupAge.textContent = formatRelativeAge(ts);
937
+ } else {
938
+ brandBackupPrefix.hidden = true;
939
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
940
+ brandBackupAge.removeAttribute('data-backup-ts');
941
+ brandBackupAge.textContent = t('brand.noGitBackup');
942
+ }
943
+ }
944
+
945
+ window.addEventListener('message', event => {
946
+ if (event.data.type === 'locale') {
947
+ setLocale(event.data.locale, { syncHost: false });
948
+ return;
949
+ }
950
+ if (event.data.type === 'update') render(event.data.data);
256
951
  });
952
+
953
+ langToggle.addEventListener('click', toggleLocale);
954
+ updateChrome();
955
+ root.innerHTML = '<div class="empty">' + t('state.waiting') + '</div>';
257
956
  vscode.postMessage({ cmd: 'ready' });
258
957
 
259
958
  setInterval(() => {
@@ -261,187 +960,234 @@ setInterval(() => {
261
960
  const el = document.querySelector('.alert-countdown');
262
961
  if (el) {
263
962
  const remain = Math.max(0, Math.ceil((_alertExpiresAt - Date.now()) / 1000));
264
- if (remain <= 0) { el.textContent = '0s'; _alertExpiresAt = 0; }
265
- else { el.textContent = remain > 60 ? Math.floor(remain / 60) + 'm ' + (remain % 60) + 's' : remain + 's'; }
963
+ if (remain <= 0) {
964
+ el.textContent = formatCountdown(0);
965
+ _alertExpiresAt = 0;
966
+ } else {
967
+ el.textContent = formatCountdown(remain);
968
+ }
266
969
  }
267
970
  }
268
- const ageEl = document.querySelector('.backup-age[data-backup-ts]');
269
- if (ageEl) {
971
+
972
+ document.querySelectorAll('.backup-age[data-backup-ts]').forEach(ageEl => {
270
973
  const ts = parseInt(ageEl.dataset.backupTs, 10);
271
- if (ts) {
272
- const sec = Math.floor((Date.now() - ts) / 1000);
273
- if (sec < 60) ageEl.textContent = sec + 's ago';
274
- else if (sec < 3600) ageEl.textContent = Math.floor(sec / 60) + 'm ' + (sec % 60) + 's ago';
275
- else if (sec < 86400) ageEl.textContent = Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm ago';
276
- else ageEl.textContent = Math.floor(sec / 86400) + 'd ago';
277
- }
278
- }
974
+ if (!ts) return;
975
+ ageEl.textContent = formatRelativeAge(ts);
976
+ });
279
977
  }, 1000);
280
978
 
281
979
  function render(projects) {
282
- const ids = Object.keys(projects);
980
+ _projects = projects || {};
981
+ const ids = Object.keys(_projects);
283
982
  if (ids.length === 0) {
284
- root.innerHTML = '<div class="empty-state">No projects detected.<br>Add .cursor-guard.json to get started.</div>';
983
+ root.innerHTML = '<div class="empty">' + t('state.empty') + '</div>';
984
+ updateBrandBar(_projects);
285
985
  return;
286
986
  }
987
+
287
988
  let html = '';
288
989
  for (const id of ids) {
289
- const p = projects[id];
290
- const d = p.dashboard;
291
- if (!d) { html += '<div class="empty-state">Loading...</div>'; continue; }
292
- html += renderProject(d);
990
+ const project = _projects[id];
991
+ const dashboard = project.dashboard;
992
+ if (!dashboard) {
993
+ html += '<div class="empty">' + esc(t('state.loading')) + '</div>';
994
+ continue;
995
+ }
996
+ html += renderProject(dashboard);
293
997
  }
294
- html += renderActions(projects);
998
+ html += renderActions(_projects);
295
999
  root.innerHTML = html;
1000
+ updateBrandBar(_projects);
296
1001
 
297
1002
  const alertCard = root.querySelector('.alert-card[data-expires]');
298
1003
  _alertExpiresAt = alertCard ? parseInt(alertCard.dataset.expires, 10) || 0 : 0;
299
1004
 
300
1005
  root.querySelectorAll('[data-cmd]').forEach(btn => {
301
- btn.addEventListener('click', () => vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd }));
1006
+ btn.addEventListener('click', () => {
1007
+ vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd });
1008
+ });
302
1009
  });
303
1010
  }
304
1011
 
305
- function renderProject(d) {
306
- const wOk = d.watcher?.running;
307
- const hasAlert = d.alerts?.active;
308
- const health = d.health?.status || 'unknown';
309
- const isCritical = health === 'critical';
310
- let h = '';
311
-
312
- // ── Big status hero ──
313
- if (hasAlert) {
314
- const fc = d.alerts.latest?.fileCount || '?';
315
- h += '<div class="status-hero alert">';
316
- h += '<span class="status-icon">🔴</span>';
317
- h += '<span class="status-text">' + fc + ' files alert</span>';
318
- h += '<span class="status-sub">Abnormal change velocity detected</span>';
319
- h += '</div>';
320
- } else if (!wOk) {
321
- h += '<div class="status-hero stopped">';
322
- h += '<span class="status-icon">🟡</span>';
323
- h += '<span class="status-text">Watcher Stopped</span>';
324
- h += '<span class="status-sub">Start watcher to enable protection</span>';
325
- h += '</div>';
326
- } else if (isCritical) {
327
- h += '<div class="status-hero critical">';
328
- h += '<span class="status-icon">🔴</span>';
329
- h += '<span class="status-text">Critical Issue</span>';
330
- h += '<span class="status-sub">' + esc(d.health.issues?.[0] || 'Check diagnostics') + '</span>';
331
- h += '</div>';
1012
+ function renderProject(dashboard) {
1013
+ const watcherRunning = dashboard.watcher?.running;
1014
+ const latestPreWarning = dashboard.preWarnings?.active ? dashboard.preWarnings.latest : null;
1015
+ const preWarning = latestPreWarning?.mode === 'dashboard' ? latestPreWarning : null;
1016
+ const alert = dashboard.alerts?.active ? dashboard.alerts.latest : null;
1017
+ const health = dashboard.health?.status || 'unknown';
1018
+ const critical = health === 'critical';
1019
+ let html = '';
1020
+
1021
+ if (preWarning) {
1022
+ html += hero('risk', t('hero.pre.kicker'), t('hero.pre.title'), preWarning.summary || t('hero.pre.subtitle'));
1023
+ } else if (alert) {
1024
+ html += hero('alert', t('hero.alert.kicker'), t('alert.filesChangedFast', { count: displayCount(alert.fileCount) }), t('hero.alert.subtitle'));
1025
+ } else if (!watcherRunning) {
1026
+ html += hero('stopped', t('hero.protection.kicker'), t('hero.protection.stopped'), t('hero.protection.stoppedSub'));
1027
+ } else if (critical) {
1028
+ html += hero('critical', t('hero.health.kicker'), t('hero.health.critical'), dashboard.health.issues?.[0] || t('hero.health.check'));
332
1029
  } else {
333
- h += '<div class="status-hero protected">';
334
- h += '<span class="status-icon">🟢</span>';
335
- h += '<span class="status-text">Protected</span>';
336
- h += '<span class="status-sub">Watcher running · All systems OK</span>';
337
- h += '</div>';
1030
+ html += hero('protected', t('hero.protection.kicker'), t('hero.protection.safe'), t('hero.protection.safeSub'), { live: true });
338
1031
  }
339
1032
 
340
- // ── Alert detail card (only when active) ──
341
- if (hasAlert) {
342
- const a = d.alerts.latest;
343
- const expiresTs = a.expiresAt ? new Date(a.expiresAt).getTime() : 0;
1033
+ if (preWarning) {
1034
+ html += '<div class="card risk-card">';
1035
+ html += '<div class="card-title">' + esc(t('card.deletionRisk')) + '</div>';
1036
+ html += row(t('row.file'), esc(preWarning.file || 'Unknown'), 'orange');
1037
+ html += row(t('row.risk'), esc(String(preWarning.riskPercent || '?')) + '%', 'orange');
1038
+ if (preWarning.removedMethodCount) {
1039
+ html += row(t('row.methodsRemoved'), esc(String(preWarning.removedMethodCount)), 'red');
1040
+ }
1041
+ html += row(t('row.summary'), esc(preWarning.summary || t('hero.pre.subtitle')), 'orange');
1042
+ html += '<div class="actions">';
1043
+ html += '<button class="btn" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.openDashboard')) + '</button>';
1044
+ html += '<button class="btn" data-cmd="cursorGuard.quickRestore">' + esc(t('actions.restore')) + '</button>';
1045
+ html += '</div>';
1046
+ html += '</div>';
1047
+ }
1048
+
1049
+ if (alert) {
1050
+ const expiresTs = alert.expiresAt ? new Date(alert.expiresAt).getTime() : 0;
344
1051
  const remain = expiresTs ? Math.max(0, Math.ceil((expiresTs - Date.now()) / 1000)) : 0;
345
- const display = remain > 60 ? Math.floor(remain/60) + 'm ' + (remain%60) + 's' : remain + 's';
346
- h += '<div class="alert-card" data-expires="' + expiresTs + '">';
347
- h += '<div class="title">\u26a0 ' + (a.fileCount||'?') + ' files in ' + (a.windowSeconds||'?') + 's</div>';
348
- h += '<div class="detail">Threshold: ' + (a.threshold||'?') + ' \xb7 Expires: <span class="alert-countdown">' + display + '</span></div>';
349
- h += '<div class="actions">';
350
- h += '<button class="btn-sm" data-cmd="cursorGuard.openDashboard">View Details</button>';
351
- h += '</div>';
352
- h += '</div>';
1052
+ const display = formatCountdown(remain);
1053
+ html += '<div class="card alert-card" data-expires="' + expiresTs + '">';
1054
+ html += '<div class="card-title">' + esc(t('card.activeAlert')) + '</div>';
1055
+ html += row(t('row.window'), (alert.windowSeconds || '?') + 's', 'red');
1056
+ html += row(t('row.files'), String(alert.fileCount || '?'), 'red');
1057
+ html += row(t('row.threshold'), String(alert.threshold || '?'), 'yellow');
1058
+ html += row(t('row.expires'), '<span class="alert-countdown">' + esc(display) + '</span>', 'yellow', true);
1059
+ html += '<div class="actions">';
1060
+ html += '<button class="btn" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.viewDetails')) + '</button>';
1061
+ html += '</div>';
1062
+ html += '</div>';
353
1063
  }
354
1064
 
355
- // ── Quick stats ──
356
- const gitC = d.counts?.git?.commits || 0;
357
- const shadowC = d.counts?.shadow?.snapshots || 0;
358
- const lastGitTs = d.lastBackup?.git?.timestamp || '';
359
- const lastGit = d.lastBackup?.git?.relativeTime || 'never';
360
- const freeGB = d.disk?.freeGB;
1065
+ const gitCount = dashboard.counts?.git?.commits || 0;
1066
+ const shadowCount = dashboard.counts?.shadow?.snapshots || 0;
1067
+ const lastGitTs = dashboard.lastBackup?.git?.timestamp || '';
1068
+ const lastGit = dashboard.lastBackup?.git?.relativeTime || t('stats.never');
1069
+ const freeGB = dashboard.disk?.freeGB;
361
1070
  const freeDisplay = typeof freeGB === 'number' ? freeGB.toFixed(1) + ' GB' : 'N/A';
362
- const diskWarn = d.disk?.warning;
1071
+ const diskWarn = dashboard.disk?.warning;
1072
+ const watcherInfo = watcherStateInfo(dashboard);
1073
+ const healthInfo = healthStateInfo(dashboard);
363
1074
 
364
- h += '<div class="stats-card">';
365
- h += '<div class="label-sm">Quick Stats</div>';
1075
+ html += '<div class="card">';
1076
+ html += '<div class="card-title">' + esc(t('card.quickStats')) + '</div>';
1077
+ html += row(t('row.watcher'), watcherInfo.label, watcherInfo.tone);
1078
+ html += row(t('row.health'), healthInfo.label, healthInfo.tone);
366
1079
  if (lastGitTs) {
367
- h += '<div class="stat-row"><span class="name">Last backup</span><span class="val green backup-age" data-backup-ts="' + new Date(lastGitTs).getTime() + '">' + esc(lastGit) + '</span></div>';
1080
+ html += '<div class="row"><span class="row-name">' + esc(t('row.lastBackup')) + '</span><span class="row-value green backup-age" data-backup-ts="' + new Date(lastGitTs).getTime() + '">' + esc(formatRelativeAge(new Date(lastGitTs).getTime())) + '</span></div>';
368
1081
  } else {
369
- h += statRow('Last backup', lastGit, 'green');
1082
+ html += row(t('row.lastBackup'), lastGit, 'green');
1083
+ }
1084
+ html += row(t('row.gitBackups'), String(gitCount), 'blue');
1085
+ if (shadowCount > 0) html += row(t('row.shadowCopies'), String(shadowCount), 'blue');
1086
+ html += row(t('row.diskFree'), freeDisplay, diskWarn ? 'yellow' : 'green');
1087
+ html += '</div>';
1088
+
1089
+ const scope = dashboard.protectionScope || {};
1090
+ const protect = scope.protect || [];
1091
+ const ignore = scope.ignore || [];
1092
+
1093
+ html += '<div class="card">';
1094
+ html += '<div class="card-title">' + esc(t('card.protectionScope')) + '</div>';
1095
+ html += '<div class="pill-wrap">';
1096
+ html += '<span class="pill green">' + esc(t('pill.protected', { n: String(scope.fileCount || 0) })) + '</span>';
1097
+ if ((scope.excludedCount || 0) > 0) {
1098
+ html += '<span class="pill red">' + esc(t('pill.excluded', { n: String(scope.excludedCount || 0) })) + '</span>';
370
1099
  }
371
- h += statRow('Git backups', gitC, 'blue');
372
- if (shadowC > 0) h += statRow('Shadow copies', shadowC, 'blue');
373
- h += statRow('Disk free', freeDisplay, diskWarn ? 'yellow' : 'green');
374
- h += '</div>';
375
-
376
- // ── Protection Scope ──
377
- const scope = d.protectionScope || {};
378
- const pCount = scope.fileCount || 0;
379
- const exCount = scope.excludedCount || 0;
380
- const totalF = scope.totalFiles || 0;
381
- const protectPats = scope.protect || [];
382
- const ignorePats = scope.ignore || [];
383
-
384
- h += '<div class="stats-card">';
385
- h += '<div class="label-sm">Protection Scope</div>';
386
- h += '<div class="scope-summary">';
387
- h += '<span class="scope-chip protected">\u{1f6e1}\ufe0f ' + pCount + ' protected</span>';
388
- if (exCount > 0) h += '<span class="scope-chip excluded">\u{1f6ab} ' + exCount + ' excluded</span>';
389
- h += '<span class="scope-chip total">' + totalF + ' total</span>';
390
- h += '</div>';
391
-
392
- if (protectPats.length > 0) {
393
- h += '<div class="scope-block">';
394
- h += '<span class="scope-label green">Protect (' + protectPats.length + ')</span>';
395
- h += '<div class="scope-tags">';
396
- const showP = protectPats.slice(0, 6);
397
- for (const p of showP) h += '<span class="scope-tag green">' + esc(p) + '</span>';
398
- if (protectPats.length > 6) h += '<span class="scope-tag dim">+' + (protectPats.length - 6) + ' more</span>';
399
- h += '</div></div>';
1100
+ html += '<span class="pill dim">' + esc(t('pill.total', { n: String(scope.totalFiles || 0) })) + '</span>';
1101
+ html += '</div>';
1102
+
1103
+ if (protect.length > 0) {
1104
+ html += renderTags(t('tag.protect'), protect, 'green');
400
1105
  }
1106
+ if (ignore.length > 0) {
1107
+ html += renderTags(t('tag.ignore'), ignore, 'red');
1108
+ }
1109
+ html += '</div>';
401
1110
 
402
- if (ignorePats.length > 0) {
403
- h += '<div class="scope-block">';
404
- h += '<span class="scope-label red">Ignore (' + ignorePats.length + ')</span>';
405
- h += '<div class="scope-tags">';
406
- const showI = ignorePats.slice(0, 6);
407
- for (const ig of showI) h += '<span class="scope-tag red">' + esc(ig) + '</span>';
408
- if (ignorePats.length > 6) h += '<span class="scope-tag dim">+' + (ignorePats.length - 6) + ' more</span>';
409
- h += '</div></div>';
1111
+ return html;
1112
+ }
1113
+
1114
+ function renderTags(label, values, tone) {
1115
+ let html = '<div class="tag-group">';
1116
+ html += '<div class="tag-label">' + esc(label) + ' (' + values.length + ')</div>';
1117
+ html += '<div class="tag-list">';
1118
+ const shown = values.slice(0, 6);
1119
+ for (const value of shown) {
1120
+ html += '<span class="tag ' + tone + '">' + esc(value) + '</span>';
1121
+ }
1122
+ if (values.length > 6) {
1123
+ html += '<span class="tag dim">' + esc(t('tag.more', { n: values.length - 6 })) + '</span>';
410
1124
  }
1125
+ html += '</div></div>';
1126
+ return html;
1127
+ }
411
1128
 
412
- h += '</div>';
1129
+ function watcherStateInfo(dashboard) {
1130
+ const watcher = dashboard?.watcher || {};
1131
+ if (watcher.running) return { label: t('status.watcher.running'), tone: 'green' };
1132
+ if (watcher.stale) return { label: t('status.watcher.stale'), tone: 'yellow' };
1133
+ return { label: t('status.watcher.stopped'), tone: 'red' };
1134
+ }
413
1135
 
414
- return h;
1136
+ function healthStateInfo(dashboard) {
1137
+ const health = dashboard?.health?.status || 'warning';
1138
+ if (health === 'critical') return { label: t('status.health.critical'), tone: 'red' };
1139
+ if (health === 'healthy') return { label: t('status.health.healthy'), tone: 'green' };
1140
+ return { label: t('status.health.warning'), tone: 'yellow' };
415
1141
  }
416
1142
 
417
1143
  function renderActions(projects) {
418
- const ids = Object.keys(projects);
419
- const d = ids.length > 0 ? projects[ids[0]]?.dashboard : null;
420
- const wOk = d?.watcher?.running;
1144
+ const primary = pickPrimaryProject(projects || {});
1145
+ const dashboard = primary?.project?.dashboard || null;
1146
+ const watcherRunning = dashboard?.watcher?.running;
421
1147
 
422
- let h = '<div class="actions-section"><div class="actions-grid">';
423
- h += '<button class="action-btn primary" data-cmd="cursorGuard.snapshotNow"><span class="icon">📸</span>Snapshot</button>';
424
- h += '<button class="action-btn" data-cmd="cursorGuard.quickRestore"><span class="icon">⏪</span>Restore</button>';
1148
+ let html = '<div class="cg-actions-wrap"><div class="actions">';
1149
+ html += '<button class="btn primary" data-cmd="cursorGuard.snapshotNow">' + esc(t('actions.snapshot')) + '</button>';
1150
+ html += '<button class="btn" data-cmd="cursorGuard.quickRestore">' + esc(t('actions.restore')) + '</button>';
1151
+ html += watcherRunning
1152
+ ? '<button class="btn" data-cmd="cursorGuard.stopWatcher">' + esc(t('actions.watcherOn')) + '</button>'
1153
+ : '<button class="btn" data-cmd="cursorGuard.startWatcher">' + esc(t('actions.watcherOff')) + '</button>';
1154
+ html += '<button class="btn" data-cmd="cursorGuard.doctor">' + esc(t('actions.doctor')) + '</button>';
1155
+ html += '<button class="btn primary full" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.openDashboard')) + '</button>';
1156
+ html += '</div></div>';
1157
+ return html;
1158
+ }
425
1159
 
426
- if (wOk) {
427
- h += '<button class="action-btn" data-cmd="cursorGuard.stopWatcher"><span class="icon">🟢</span>Watcher ON</button>';
428
- } else {
429
- h += '<button class="action-btn" data-cmd="cursorGuard.startWatcher"><span class="icon">⚪</span>Watcher OFF</button>';
430
- }
431
- h += '<button class="action-btn" data-cmd="cursorGuard.doctor"><span class="icon">🔍</span>Doctor</button>';
1160
+ function hero(tone, kicker, title, subtitle, opts) {
1161
+ const pulse = opts && opts.live ? '<span class="cg-pulse-dot" title="Watcher running"></span>' : '';
1162
+ let html = '<div class="hero ' + tone + '">';
1163
+ html += '<div class="hero-top">';
1164
+ html += '<div class="hero-kicker">' + esc(kicker) + '</div>';
1165
+ html += pulse;
1166
+ html += '</div>';
1167
+ html += '<div class="hero-title">' + esc(title) + '</div>';
1168
+ html += '<div class="hero-sub">' + esc(subtitle) + '</div>';
1169
+ html += '</div>';
1170
+ return html;
1171
+ }
432
1172
 
433
- h += '<button class="action-btn full primary" data-cmd="cursorGuard.openDashboard"><span class="icon">📊</span>Open Dashboard</button>';
434
- h += '</div></div>';
435
- return h;
1173
+ function row(name, value, tone, rawValue) {
1174
+ return '<div class="row"><span class="row-name">' + esc(name) + '</span><span class="row-value ' + tone + '">' + (rawValue ? value : esc(String(value))) + '</span></div>';
436
1175
  }
437
1176
 
438
- function statRow(name, val, cls) {
439
- return '<div class="stat-row"><span class="name">' + name + '</span><span class="val ' + cls + '">' + esc(String(val)) + '</span></div>';
1177
+ function esc(value) {
1178
+ return String(value)
1179
+ .replace(/&/g, '&amp;')
1180
+ .replace(/</g, '&lt;')
1181
+ .replace(/>/g, '&gt;')
1182
+ .replace(/"/g, '&quot;');
440
1183
  }
441
- function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
442
1184
  </script>
443
1185
  </body>
444
1186
  </html>`;
445
1187
  }
446
1188
 
447
1189
  module.exports = { SidebarDashboardProvider };
1190
+
1191
+
1192
+
1193
+