patchwarden 1.5.0 → 1.5.1

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 (217) hide show
  1. package/README.en.md +43 -2
  2. package/README.md +43 -2
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2197
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/doctor.d.ts +18 -1
  44. package/dist/doctor.js +579 -348
  45. package/dist/goal/goalReport.d.ts +54 -0
  46. package/dist/goal/goalReport.js +204 -0
  47. package/dist/goal/goalStatus.d.ts +6 -0
  48. package/dist/goal/specKitImport.d.ts +63 -0
  49. package/dist/goal/specKitImport.js +220 -0
  50. package/dist/goal/subgoalSync.js +2 -1
  51. package/dist/httpServer.js +15 -12
  52. package/dist/index.js +7 -4
  53. package/dist/logging.d.ts +7 -1
  54. package/dist/logging.js +8 -0
  55. package/dist/runner/changeCapture.d.ts +3 -3
  56. package/dist/runner/changeCapture.js +63 -39
  57. package/dist/runner/cli.js +7 -6
  58. package/dist/runner/postTaskCleanup.js +26 -2
  59. package/dist/runner/runTask.js +245 -221
  60. package/dist/runner/simpleProcess.js +4 -4
  61. package/dist/runner/watch.js +17 -14
  62. package/dist/security/contentRedaction.d.ts +6 -0
  63. package/dist/security/contentRedaction.js +22 -0
  64. package/dist/smoke-test.js +257 -251
  65. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  66. package/dist/test/unit/apply-patch.test.js +225 -0
  67. package/dist/test/unit/create-task.test.d.ts +1 -0
  68. package/dist/test/unit/create-task.test.js +197 -0
  69. package/dist/test/unit/direct-guards.test.js +124 -9
  70. package/dist/test/unit/evidence-pack.test.js +95 -1
  71. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  72. package/dist/test/unit/get-task-status.test.js +174 -0
  73. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  74. package/dist/test/unit/get-task-summary.test.js +146 -0
  75. package/dist/test/unit/goal-report.test.d.ts +1 -0
  76. package/dist/test/unit/goal-report.test.js +159 -0
  77. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  78. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  79. package/dist/test/unit/path-guard.test.js +24 -0
  80. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  81. package/dist/test/unit/spec-kit-import.test.js +341 -0
  82. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  83. package/dist/test/unit/wait-for-task.test.js +144 -0
  84. package/dist/tools/auditTask.d.ts +8 -63
  85. package/dist/tools/auditTask.js +12 -8
  86. package/dist/tools/createDirectSession.d.ts +1 -1
  87. package/dist/tools/createDirectSession.js +2 -2
  88. package/dist/tools/createTask.d.ts +2 -2
  89. package/dist/tools/createTask.js +4 -4
  90. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  91. package/dist/tools/dispatch/coreDispatch.js +282 -0
  92. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  93. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  94. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  95. package/dist/tools/dispatch/directDispatch.js +115 -0
  96. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  97. package/dist/tools/dispatch/goalDispatch.js +91 -0
  98. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  99. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  100. package/dist/tools/dispatch/types.d.ts +23 -0
  101. package/dist/tools/dispatch/types.js +15 -0
  102. package/dist/tools/evidencePack.d.ts +6 -0
  103. package/dist/tools/evidencePack.js +213 -6
  104. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  105. package/dist/tools/finalizeDirectSession.js +3 -3
  106. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  107. package/dist/tools/goalSubgoalTask.js +2 -2
  108. package/dist/tools/healthCheck.js +3 -3
  109. package/dist/tools/registry.d.ts +3 -3
  110. package/dist/tools/registry.js +60 -503
  111. package/dist/tools/retryTask.d.ts +2 -2
  112. package/dist/tools/retryTask.js +2 -2
  113. package/dist/tools/runTaskLoop.js +4 -4
  114. package/dist/tools/safeViews.d.ts +2 -2
  115. package/dist/tools/safeViews.js +2 -2
  116. package/dist/tools/toolRegistry.js +22 -0
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/docs/CODE_WIKI.md +983 -0
  120. package/docs/agentseal-integration.md +150 -0
  121. package/docs/architecture.md +63 -0
  122. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  123. package/docs/dashboard-overview.md +86 -0
  124. package/docs/demo.md +8 -0
  125. package/docs/direct-session-workflow.md +98 -0
  126. package/docs/evidence-pack-schema.md +215 -0
  127. package/docs/execution-plan-2026-07-09.md +315 -0
  128. package/docs/lineage-evidence-pack-workflow.md +127 -0
  129. package/docs/mcp-inspector-testing.md +200 -0
  130. package/docs/open-source-application.md +162 -0
  131. package/docs/opencode-worker.md +151 -0
  132. package/docs/openhands-worker.md +181 -0
  133. package/docs/release-evidence.md +72 -0
  134. package/docs/roadmap-execution-and-acceptance.md +365 -0
  135. package/docs/spec-kit-integration.md +131 -0
  136. package/docs/task-safe-review-workflow.md +98 -0
  137. package/docs/threat-model.md +79 -0
  138. package/docs/user-feedback.md +40 -0
  139. package/docs/why-patchwarden.md +110 -0
  140. package/package.json +2 -2
  141. package/scripts/checks/control-center-smoke.js +356 -0
  142. package/scripts/checks/lifecycle-smoke.js +29 -23
  143. package/scripts/checks/mcp-smoke.js +3 -1
  144. package/scripts/generate-demo-gif.py +320 -0
  145. package/src/assessments/agentAssessor.ts +3 -3
  146. package/src/assessments/assessmentStore.ts +2 -2
  147. package/src/assessments/confirmCli.ts +5 -4
  148. package/src/config.ts +37 -0
  149. package/src/control/middleware/auth.ts +17 -0
  150. package/src/control/middleware/static.ts +71 -0
  151. package/src/control/routes/audit.ts +321 -0
  152. package/src/control/routes/evidence.ts +107 -0
  153. package/src/control/routes/lineage.ts +92 -0
  154. package/src/control/routes/policy.ts +81 -0
  155. package/src/control/routes/process.ts +204 -0
  156. package/src/control/routes/sessions.ts +251 -0
  157. package/src/control/routes/status.ts +325 -0
  158. package/src/control/routes/taskActions.ts +248 -0
  159. package/src/control/routes/tasks.ts +323 -0
  160. package/src/control/routes/workspace.ts +203 -0
  161. package/src/control/runtime.ts +472 -0
  162. package/src/control/server.ts +471 -0
  163. package/src/control/shared.ts +294 -0
  164. package/src/controlCenter.ts +7 -2347
  165. package/src/direct/directGuards.ts +28 -7
  166. package/src/doctor.ts +741 -481
  167. package/src/goal/goalReport.ts +271 -0
  168. package/src/goal/goalStatus.ts +6 -0
  169. package/src/goal/specKitImport.ts +355 -0
  170. package/src/goal/subgoalSync.ts +4 -2
  171. package/src/httpServer.ts +17 -14
  172. package/src/index.ts +7 -4
  173. package/src/logging.ts +10 -1
  174. package/src/runner/changeCapture.ts +70 -42
  175. package/src/runner/cli.ts +7 -6
  176. package/src/runner/postTaskCleanup.ts +26 -2
  177. package/src/runner/runTask.ts +325 -223
  178. package/src/runner/simpleProcess.ts +4 -4
  179. package/src/runner/watch.ts +17 -14
  180. package/src/security/contentRedaction.ts +29 -0
  181. package/src/smoke-test.ts +252 -250
  182. package/src/test/unit/apply-patch.test.ts +293 -0
  183. package/src/test/unit/create-task.test.ts +255 -0
  184. package/src/test/unit/direct-guards.test.ts +178 -8
  185. package/src/test/unit/evidence-pack.test.ts +110 -1
  186. package/src/test/unit/get-task-status.test.ts +203 -0
  187. package/src/test/unit/get-task-summary.test.ts +173 -0
  188. package/src/test/unit/goal-report.test.ts +189 -0
  189. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  190. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  191. package/src/test/unit/path-guard.test.ts +30 -0
  192. package/src/test/unit/spec-kit-import.test.ts +429 -0
  193. package/src/test/unit/wait-for-task.test.ts +176 -0
  194. package/src/tools/auditTask.ts +99 -59
  195. package/src/tools/createDirectSession.ts +3 -3
  196. package/src/tools/createTask.ts +7 -7
  197. package/src/tools/dispatch/coreDispatch.ts +374 -0
  198. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  199. package/src/tools/dispatch/directDispatch.ts +167 -0
  200. package/src/tools/dispatch/goalDispatch.ts +127 -0
  201. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  202. package/src/tools/dispatch/types.ts +24 -0
  203. package/src/tools/evidencePack.ts +291 -6
  204. package/src/tools/finalizeDirectSession.ts +4 -4
  205. package/src/tools/goalSubgoalTask.ts +2 -2
  206. package/src/tools/healthCheck.ts +3 -3
  207. package/src/tools/registry.ts +68 -628
  208. package/src/tools/retryTask.ts +2 -2
  209. package/src/tools/runTaskLoop.ts +4 -4
  210. package/src/tools/safeViews.ts +2 -2
  211. package/src/tools/toolRegistry.ts +22 -0
  212. package/src/version.ts +1 -1
  213. package/ui/pages/audit.html +192 -3
  214. package/ui/pages/dashboard.html +959 -44
  215. package/ui/pages/direct-sessions.html +505 -53
  216. package/ui/pages/task-detail.html +456 -438
  217. package/ui/pages/tasks.html +598 -61
@@ -101,6 +101,76 @@ html.dark {
101
101
  .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
102
102
  .pw-spin { animation: pw-spin 1s linear infinite; }
103
103
  @keyframes pw-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
104
+ /* Toast notifications */
105
+ .pw-toast {
106
+ pointer-events: auto;
107
+ display: flex;
108
+ align-items: flex-start;
109
+ gap: 8px;
110
+ padding: 10px 12px;
111
+ border-radius: 8px;
112
+ border: 1px solid var(--pw-border);
113
+ background-color: var(--pw-bg-surface);
114
+ box-shadow: var(--pw-shadow-float);
115
+ font-family: var(--pw-font-sans);
116
+ font-size: 12px;
117
+ color: var(--pw-text-primary);
118
+ animation: pw-toast-in 200ms ease;
119
+ max-width: 360px;
120
+ }
121
+ .pw-toast.pw-toast-success { border-left: 3px solid var(--pw-state-success); }
122
+ .pw-toast.pw-toast-warning { border-left: 3px solid var(--pw-state-warning); }
123
+ .pw-toast.pw-toast-error { border-left: 3px solid var(--pw-state-error); }
124
+ .pw-toast.pw-toast-info { border-left: 3px solid var(--pw-state-info); }
125
+ .pw-toast .pw-toast-title { font-weight: 600; margin-bottom: 2px; }
126
+ .pw-toast .pw-toast-msg { color: var(--pw-text-secondary); word-break: break-word; }
127
+ .pw-toast .pw-toast-close { margin-left: auto; cursor: pointer; color: var(--pw-text-tertiary); font-size: 14px; line-height: 1; padding: 0 2px; }
128
+ .pw-toast .pw-toast-close:hover { color: var(--pw-text-primary); }
129
+ @keyframes pw-toast-in {
130
+ from { opacity: 0; transform: translateX(20px); }
131
+ to { opacity: 1; transform: translateX(0); }
132
+ }
133
+ .pw-toast.pw-toast-leaving { animation: pw-toast-out 200ms ease forwards; }
134
+ @keyframes pw-toast-out {
135
+ to { opacity: 0; transform: translateX(20px); }
136
+ }
137
+ /* Quick action buttons */
138
+ .pw-quick-btn {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ gap: 4px;
142
+ padding: 3px 8px;
143
+ border-radius: 4px;
144
+ border: 1px solid var(--pw-border-subtle);
145
+ background-color: var(--pw-bg-hover);
146
+ color: var(--pw-text-secondary);
147
+ cursor: pointer;
148
+ transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
149
+ text-decoration: none;
150
+ font-size: 11px;
151
+ font-family: var(--pw-font-sans);
152
+ white-space: nowrap;
153
+ }
154
+ .pw-quick-btn:hover {
155
+ background-color: var(--pw-bg-active);
156
+ color: var(--pw-text-primary);
157
+ }
158
+ .pw-quick-btn:disabled {
159
+ opacity: 0.5;
160
+ cursor: not-allowed;
161
+ }
162
+ .pw-quick-btn.pw-quick-danger:hover {
163
+ background-color: var(--pw-state-error-bg);
164
+ color: var(--pw-state-error);
165
+ border-color: var(--pw-state-error);
166
+ }
167
+ .pw-quick-btn.pw-quick-accent {
168
+ color: var(--pw-accent);
169
+ border-color: var(--pw-accent-muted);
170
+ }
171
+ .pw-quick-btn.pw-quick-accent:hover {
172
+ background-color: var(--pw-accent-muted);
173
+ }
104
174
  </style>
105
175
  </head>
106
176
  <body class="flex h-screen overflow-hidden">
@@ -261,6 +331,20 @@ html.dark {
261
331
  <!-- ===== Session Detail ===== -->
262
332
  <div id="session-detail" class="hidden flex flex-col gap-6"></div>
263
333
 
334
+ <!-- ===== Toast Container ===== -->
335
+ <div id="toast-container" class="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-sm pointer-events-none"></div>
336
+
337
+ <!-- ===== SAFE MODAL ===== -->
338
+ <div id="safe-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: rgba(0,0,0,0.6);">
339
+ <div class="rounded-lg w-full max-w-2xl max-h-[80vh] flex flex-col" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); box-shadow: var(--pw-shadow-modal);">
340
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
341
+ <h3 id="safe-modal-title" class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">—</h3>
342
+ <button id="safe-modal-close" class="text-lg leading-none" style="color: var(--pw-text-tertiary);" type="button">×</button>
343
+ </div>
344
+ <div id="safe-modal-body" class="flex-1 overflow-auto px-4 py-3 text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono); white-space: pre-wrap; word-break: break-all;"></div>
345
+ </div>
346
+ </div>
347
+
264
348
  </main>
265
349
  </div>
266
350
 
@@ -288,6 +372,7 @@ html.dark {
288
372
  'use strict';
289
373
 
290
374
  var API_URL = '/api/direct-sessions';
375
+ var controlToken = null;
291
376
 
292
377
  function $(id) { return document.getElementById(id); }
293
378
  function hide(el) { if (el) el.classList.add('hidden'); }
@@ -316,6 +401,13 @@ html.dark {
316
401
  + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
317
402
  }
318
403
 
404
+ function shortId(sid) {
405
+ if (!sid) return '—';
406
+ var s = String(sid);
407
+ if (s.length <= 20) return s;
408
+ return s.slice(0, 12) + '…' + s.slice(-6);
409
+ }
410
+
319
411
  function showError(msg) {
320
412
  var banner = $('error-banner');
321
413
  var message = $('error-message');
@@ -377,7 +469,8 @@ html.dark {
377
469
  success: { fg: 'var(--pw-state-success)', bg: 'var(--pw-state-success-bg)' },
378
470
  warning: { fg: 'var(--pw-state-warning)', bg: 'var(--pw-state-warning-bg)' },
379
471
  error: { fg: 'var(--pw-state-error)', bg: 'var(--pw-state-error-bg)' },
380
- muted: { fg: 'var(--pw-text-tertiary)', bg: 'var(--pw-bg-hover)' }
472
+ muted: { fg: 'var(--pw-text-tertiary)', bg: 'var(--pw-bg-hover)' },
473
+ info: { fg: 'var(--pw-state-info)', bg: 'var(--pw-state-info-bg)' }
381
474
  };
382
475
 
383
476
  function badgeHtml(text, color) {
@@ -385,7 +478,131 @@ html.dark {
385
478
  return '<span class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: ' + c.fg + '; background-color: ' + c.bg + ';">' + escapeHtml(text) + '</span>';
386
479
  }
387
480
 
388
- // ---------- Render: sessions list ----------
481
+ // ---------- Toast notifications ----------
482
+ function showToast(kind, title, msg) {
483
+ var container = $('toast-container');
484
+ if (!container) return;
485
+ var el = document.createElement('div');
486
+ el.className = 'pw-toast pw-toast-' + (kind || 'info');
487
+ el.innerHTML =
488
+ '<div class="min-w-0 flex-1">'
489
+ + '<div class="pw-toast-title">' + escapeHtml(title || '') + '</div>'
490
+ + (msg ? '<div class="pw-toast-msg">' + escapeHtml(msg) + '</div>' : '')
491
+ + '</div>'
492
+ + '<span class="pw-toast-close" title="关闭">×</span>';
493
+ var closeBtn = el.querySelector('.pw-toast-close');
494
+ if (closeBtn) closeBtn.addEventListener('click', function () { dismissToast(el); });
495
+ container.appendChild(el);
496
+ setTimeout(function () { dismissToast(el); }, 8000);
497
+ }
498
+
499
+ function dismissToast(el) {
500
+ if (!el || el.classList.contains('pw-toast-leaving')) return;
501
+ el.classList.add('pw-toast-leaving');
502
+ setTimeout(function () {
503
+ if (el.parentNode) el.parentNode.removeChild(el);
504
+ }, 220);
505
+ }
506
+
507
+ // ---------- Clipboard + Safe Modal ----------
508
+ function copyToClipboard(text, label) {
509
+ if (!text) return;
510
+ if (navigator.clipboard && navigator.clipboard.writeText) {
511
+ navigator.clipboard.writeText(text).then(function () {
512
+ showToast('success', '已复制', (label || '内容') + '已复制到剪贴板');
513
+ }).catch(function () {
514
+ showToast('error', '复制失败', '请手动复制');
515
+ });
516
+ } else {
517
+ showToast('warning', '复制不支持', '浏览器不支持剪贴板 API');
518
+ }
519
+ }
520
+
521
+ function showSafeModal(title, data) {
522
+ var modal = $('safe-modal');
523
+ var titleEl = $('safe-modal-title');
524
+ var bodyEl = $('safe-modal-body');
525
+ if (!modal || !titleEl || !bodyEl) return;
526
+ titleEl.textContent = title || '—';
527
+ var content;
528
+ if (data && typeof data === 'object') {
529
+ content = JSON.stringify(data, null, 2);
530
+ } else {
531
+ content = String(data || '');
532
+ }
533
+ bodyEl.textContent = content;
534
+ modal.classList.remove('hidden');
535
+ }
536
+
537
+ function closeSafeModal() {
538
+ var modal = $('safe-modal');
539
+ if (modal) modal.classList.add('hidden');
540
+ }
541
+
542
+ // ---------- Control token + POST actions ----------
543
+ async function loadToken() {
544
+ try {
545
+ const res = await fetch('/control-token.json');
546
+ if (!res.ok) throw new Error('HTTP ' + res.status);
547
+ const data = await res.json();
548
+ controlToken = data.token || null;
549
+ } catch (err) {
550
+ controlToken = null;
551
+ console.error('loadToken failed:', err);
552
+ }
553
+ }
554
+
555
+ async function postAction(url) {
556
+ if (!controlToken) await loadToken();
557
+ const res = await fetch(url, {
558
+ method: 'POST',
559
+ headers: { 'X-PatchWarden-Control-Token': controlToken || '' }
560
+ });
561
+ let data = null;
562
+ try { data = await res.json(); } catch (_) { /* ignore */ }
563
+ if (!res.ok) {
564
+ const msg = (data && (data.error || data.reason || data.message)) || ('HTTP ' + res.status);
565
+ throw new Error(msg);
566
+ }
567
+ return data;
568
+ }
569
+
570
+ // ---------- Session classification ----------
571
+ function classifySession(s) {
572
+ var nowMs = Date.now();
573
+ var expiresMs = s.expires_at ? Date.parse(s.expires_at) : NaN;
574
+ var expired = Number.isFinite(expiresMs) && expiresMs < nowMs;
575
+ if (expired) return 'expired';
576
+ if (s.audited) return 'audited';
577
+ if (s.finalized) return 'finalized';
578
+ return 'active';
579
+ }
580
+
581
+ function verificationStatusText(vs) {
582
+ if (!vs || typeof vs !== 'object') return '—';
583
+ // verification_summary is the last run object: { command, exit_code, passed, timed_out, ... }
584
+ if (vs.passed === true) return 'passed';
585
+ if (vs.timed_out === true) return 'timed_out';
586
+ if (vs.passed === false) return 'failed';
587
+ if (vs.exit_code != null) return String(vs.exit_code);
588
+ return '—';
589
+ }
590
+
591
+ function verificationStatusColor(vs) {
592
+ if (!vs || typeof vs !== 'object') return 'muted';
593
+ if (vs.passed === true) return 'success';
594
+ if (vs.timed_out === true || vs.passed === false) return 'error';
595
+ return 'muted';
596
+ }
597
+
598
+ // ---------- Render: sessions list (grouped) ----------
599
+ var GROUP_LABELS = {
600
+ active: { zh: '进行中', en: 'Active', color: 'info' },
601
+ finalized: { zh: '已 Finalize', en: 'Finalized', color: 'success' },
602
+ audited: { zh: '已审计', en: 'Audited', color: 'success' },
603
+ expired: { zh: '已过期', en: 'Expired', color: 'warning' }
604
+ };
605
+
389
606
  function renderSessions(sessions) {
390
607
  var listEl = $('sessions-list');
391
608
  listEl.innerHTML = '';
@@ -394,69 +611,292 @@ html.dark {
394
611
  return;
395
612
  }
396
613
 
397
- var html = '';
614
+ var groups = { active: [], finalized: [], audited: [], expired: [] };
398
615
  for (var i = 0; i < sessions.length; i++) {
399
616
  var s = sessions[i] || {};
400
- var sid = s.session_id || '—';
401
- var finalized = !!s.finalized;
402
- var audited = !!s.audited;
403
- var changedTotal = s.changed_files_total != null ? s.changed_files_total : '—';
404
- var decision = s.audit_decision || null;
405
- var decisionBadge = decision ? badgeHtml(decision, verdictColor(decision)) : '<span class="text-xs px-2 py-0.5 rounded-md" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover);">not audited</span>';
406
- var stateBadge = finalized
407
- ? badgeHtml('finalized', 'success')
408
- : badgeHtml('open', 'warning');
409
-
410
- var titleText = s.title || s.repo_path || sid;
411
-
412
- html +=
413
- '<div class="rounded-lg p-4 cursor-pointer transition-colors session-card" data-session-id="' + escapeHtml(sid) + '" '
414
- + 'onmouseover="this.style.backgroundColor=\'var(--pw-bg-hover)\'" '
415
- + 'onmouseout="this.style.backgroundColor=\'var(--pw-bg-surface)\'" '
416
- + 'style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">'
417
- + '<div class="flex items-start justify-between gap-3 mb-3">'
418
- + '<div class="min-w-0 flex-1">'
419
- + '<div class="flex items-center gap-2 mb-1">'
420
- + '<h3 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">' + escapeHtml(titleText) + '</h3>'
421
- + '</div>'
422
- + '<code class="text-xs break-all" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);">' + escapeHtml(sid) + '</code>'
423
- + '</div>'
424
- + '<div class="flex items-center gap-1.5 shrink-0 flex-wrap justify-end">'
425
- + stateBadge + decisionBadge
426
- + '</div>'
427
- + '</div>'
428
- + '<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">'
429
- + '<div class="flex flex-col gap-0.5 min-w-0">'
430
- + '<span class="text-xs" style="color: var(--pw-text-tertiary);">repo_path</span>'
431
- + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(s.repo_path || '—') + '</span>'
432
- + '</div>'
433
- + '<div class="flex flex-col gap-0.5 min-w-0">'
434
- + '<span class="text-xs" style="color: var(--pw-text-tertiary);">changed_files_total</span>'
435
- + '<span class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(String(changedTotal)) + '</span>'
436
- + '</div>'
437
- + '<div class="flex flex-col gap-0.5 min-w-0">'
438
- + '<span class="text-xs" style="color: var(--pw-text-tertiary);">created_at</span>'
439
- + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(fmtTimeShort(s.created_at)) + '</span>'
440
- + '</div>'
441
- + '<div class="flex flex-col gap-0.5 min-w-0">'
442
- + '<span class="text-xs" style="color: var(--pw-text-tertiary);">expires_at</span>'
443
- + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(fmtTimeShort(s.expires_at)) + '</span>'
444
- + '</div>'
445
- + '</div>'
446
- + '</div>';
617
+ var cls = classifySession(s);
618
+ groups[cls].push(s);
619
+ }
620
+
621
+ var html = '';
622
+ // Active section (expanded)
623
+ if (groups.active.length > 0) {
624
+ html += renderGroupHeader('active', groups.active.length, false);
625
+ html += renderSessionCards(groups.active, false);
626
+ }
627
+ // Finalized section (expanded)
628
+ if (groups.finalized.length > 0) {
629
+ html += renderGroupHeader('finalized', groups.finalized.length, false);
630
+ html += renderSessionCards(groups.finalized, false);
631
+ }
632
+ // Audited section (expanded)
633
+ if (groups.audited.length > 0) {
634
+ html += renderGroupHeader('audited', groups.audited.length, false);
635
+ html += renderSessionCards(groups.audited, false);
447
636
  }
637
+ // Expired section (collapsed via <details>)
638
+ if (groups.expired.length > 0) {
639
+ html += renderExpiredGroup(groups.expired);
640
+ }
641
+
448
642
  listEl.innerHTML = html;
449
643
  showList();
644
+ wireActionButtons();
645
+ wireSessionCards();
646
+ }
647
+
648
+ function renderGroupHeader(key, count, isExpired) {
649
+ var meta = GROUP_LABELS[key] || GROUP_LABELS.active;
650
+ var c = COLOR_VARS[meta.color] || COLOR_VARS.muted;
651
+ return '<div class="flex items-center gap-2 mt-2 mb-1 px-1">'
652
+ + '<span class="w-2 h-2 rounded-full" style="background-color: ' + c.fg + ';"></span>'
653
+ + '<h2 class="text-sm font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">' + escapeHtml(meta.zh) + '</h2>'
654
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">(' + count + ')</span>'
655
+ + '</div>';
656
+ }
657
+
658
+ function renderExpiredGroup(sessions) {
659
+ var meta = GROUP_LABELS.expired;
660
+ var c = COLOR_VARS[meta.color] || COLOR_VARS.muted;
661
+ var headerHtml =
662
+ '<summary class="flex items-center gap-2 cursor-pointer px-3 py-2" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans;">'
663
+ + '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><polyline points="6 9 12 15 18 9"/></svg>'
664
+ + '<span class="w-2 h-2 rounded-full" style="background-color: ' + c.fg + ';"></span>'
665
+ + '<span class="text-sm font-semibold">' + escapeHtml(meta.zh) + '</span>'
666
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">(' + sessions.length + ')</span>'
667
+ + '<span class="ml-auto text-xs" style="color: var(--pw-text-tertiary);">点击展开 / 折叠</span>'
668
+ + '</summary>';
669
+ return '<details class="rounded-lg mt-2" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">'
670
+ + headerHtml
671
+ + '<div class="px-3 pb-3 pt-1 flex flex-col gap-3">'
672
+ + renderSessionCards(sessions, true)
673
+ + '</div>'
674
+ + '</details>';
675
+ }
676
+
677
+ function renderSessionCards(sessions, isExpired) {
678
+ var html = '';
679
+ for (var i = 0; i < sessions.length; i++) {
680
+ html += renderSessionCard(sessions[i] || {}, isExpired);
681
+ }
682
+ return html;
683
+ }
684
+
685
+ function renderSessionCard(s, isExpired) {
686
+ var sid = s.session_id || '—';
687
+ var finalized = !!s.finalized;
688
+ var audited = !!s.audited;
689
+ var changedTotal = s.changed_files_total != null ? s.changed_files_total : '—';
690
+ var decision = s.audit_decision || null;
691
+ var decisionBadge = decision
692
+ ? badgeHtml(decision, verdictColor(decision))
693
+ : '<span class="text-xs px-2 py-0.5 rounded-md" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover);">not audited</span>';
694
+ var stateBadge = finalized
695
+ ? badgeHtml('finalized', 'success')
696
+ : (isExpired ? badgeHtml('expired', 'warning') : badgeHtml('open', 'warning'));
697
+ var auditedBadge = audited ? badgeHtml('audited', 'success') : '';
698
+ var titleText = s.title || s.repo_path || sid;
699
+
700
+ // Verification status summary
701
+ var vs = s.verification_summary || null;
702
+ var vsText = verificationStatusText(vs);
703
+ var vsColor = verificationStatusColor(vs);
704
+ var vsBadge = badgeHtml(vsText, vsColor);
705
+
706
+ // Quick action buttons
707
+ var actions = '';
708
+ // safe_direct_summary
709
+ actions += quickBtnHtml('summary', 'safe_direct_summary', sid, '查看安全摘要', false, false);
710
+ // run_direct_verification_bundle — informational button (links to tasks page)
711
+ actions += quickBtnHtml('verify-bundle', 'verify', sid, 'run_direct_verification_bundle:通过任务页创建验证任务(该操作需在任务页发起)', false, false);
712
+ // safe_finalize_direct_session (disabled if already finalized)
713
+ actions += quickBtnHtml('finalize', 'safe_finalize_direct_session', sid, 'Finalize 会话', finalized, true);
714
+ // safe_audit_direct_session (disabled if already audited)
715
+ actions += quickBtnHtml('audit', 'safe_audit_direct_session', sid, '审计会话', audited, true);
716
+ // copy session_id
717
+ actions += quickBtnHtml('copy', 'copy session_id', sid, '复制 session_id', false, false);
718
+ // hide (expired only)
719
+ if (isExpired) {
720
+ actions += quickBtnHtml('hide', 'hide', sid, '从列表隐藏', false, false, true);
721
+ }
722
+
723
+ return '<div class="rounded-lg p-4 transition-colors session-card" data-session-id="' + escapeHtml(sid) + '" '
724
+ + 'onmouseover="this.style.backgroundColor=\'var(--pw-bg-hover)\'" '
725
+ + 'onmouseout="this.style.backgroundColor=\'var(--pw-bg-surface)\'" '
726
+ + 'style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); cursor: pointer;">'
727
+ + '<div class="flex items-start justify-between gap-3 mb-3">'
728
+ + '<div class="min-w-0 flex-1">'
729
+ + '<div class="flex items-center gap-2 mb-1">'
730
+ + '<h3 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">' + escapeHtml(titleText) + '</h3>'
731
+ + '</div>'
732
+ + '<div class="flex items-center gap-2 min-w-0">'
733
+ + '<code class="text-xs truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);" title="' + escapeHtml(sid) + '">' + escapeHtml(shortId(sid)) + '</code>'
734
+ + '</div>'
735
+ + '</div>'
736
+ + '<div class="flex items-center gap-1.5 shrink-0 flex-wrap justify-end">'
737
+ + stateBadge + auditedBadge + decisionBadge
738
+ + '</div>'
739
+ + '</div>'
740
+ + '<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs mb-3">'
741
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
742
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">repo_path</span>'
743
+ + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);" title="' + escapeHtml(s.repo_path || '') + '">' + escapeHtml(s.repo_path || '—') + '</span>'
744
+ + '</div>'
745
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
746
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">changed_files_total</span>'
747
+ + '<span class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(String(changedTotal)) + '</span>'
748
+ + '</div>'
749
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
750
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">created_at</span>'
751
+ + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(fmtTimeShort(s.created_at)) + '</span>'
752
+ + '</div>'
753
+ + '<div class="flex flex-col gap-0.5 min-w-0">'
754
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">expires_at</span>'
755
+ + '<span class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">' + escapeHtml(fmtTimeShort(s.expires_at)) + '</span>'
756
+ + '</div>'
757
+ + '</div>'
758
+ + '<div class="flex items-center gap-2 mb-3 text-xs">'
759
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">verification:</span>'
760
+ + vsBadge
761
+ + '</div>'
762
+ + '<div class="flex items-center gap-1.5 flex-wrap" onclick="event.stopPropagation();">'
763
+ + actions
764
+ + '</div>'
765
+ + '</div>';
766
+ }
767
+
768
+ function quickBtnHtml(action, label, sid, title, disabled, accent, danger) {
769
+ var cls = 'pw-quick-btn';
770
+ if (accent) cls += ' pw-quick-accent';
771
+ if (danger) cls += ' pw-quick-danger';
772
+ var disAttr = disabled ? ' disabled' : '';
773
+ return '<button type="button" class="' + cls + '" data-action="' + escapeHtml(action) + '" data-session-id="' + escapeHtml(sid) + '" title="' + escapeHtml(title + ' (' + label + ')') + '"' + disAttr + '>'
774
+ + escapeHtml(label)
775
+ + '</button>';
776
+ }
777
+
778
+ // ---------- Action button wiring ----------
779
+ function wireActionButtons() {
780
+ var btns = document.querySelectorAll('#sessions-list .pw-quick-btn[data-action]');
781
+ for (var i = 0; i < btns.length; i++) {
782
+ var btn = btns[i];
783
+ // Avoid double-binding on re-render.
784
+ if (btn.__pwBound) continue;
785
+ btn.__pwBound = true;
786
+ btn.addEventListener('click', function (ev) {
787
+ ev.preventDefault();
788
+ ev.stopPropagation();
789
+ handleActionClick(ev.currentTarget);
790
+ });
791
+ }
792
+ }
450
793
 
451
- var cards = listEl.querySelectorAll('.session-card');
794
+ function wireSessionCards() {
795
+ var cards = $('sessions-list').querySelectorAll('.session-card');
452
796
  for (var j = 0; j < cards.length; j++) {
453
- cards[j].addEventListener('click', function (ev) {
797
+ var card = cards[j];
798
+ if (card.__pwBound) continue;
799
+ card.__pwBound = true;
800
+ card.addEventListener('click', function (ev) {
801
+ // Ignore clicks that originated on a button/details summary.
802
+ if (ev.target.closest('.pw-quick-btn') || ev.target.closest('summary')) return;
454
803
  var sid2 = ev.currentTarget.getAttribute('data-session-id');
455
804
  if (sid2) loadSessionDetail(sid2);
456
805
  });
457
806
  }
458
807
  }
459
808
 
809
+ function handleActionClick(btn) {
810
+ var action = btn.getAttribute('data-action');
811
+ var sid = btn.getAttribute('data-session-id');
812
+ if (!action || !sid) return;
813
+ if (action === 'summary') {
814
+ loadSafeSummary(sid);
815
+ } else if (action === 'finalize') {
816
+ finalizeSession(sid, btn);
817
+ } else if (action === 'audit') {
818
+ auditSession(sid, btn);
819
+ } else if (action === 'copy') {
820
+ copyToClipboard(sid, 'session_id');
821
+ } else if (action === 'hide') {
822
+ hideSession(sid, btn);
823
+ } else if (action === 'verify-bundle') {
824
+ // Informational: navigate to tasks page where verification tasks are created.
825
+ showToast('info', '跳转任务页', 'run_direct_verification_bundle 需在任务页发起');
826
+ setTimeout(function () { window.location.href = '/pages/tasks.html'; }, 600);
827
+ }
828
+ }
829
+
830
+ function loadSafeSummary(sessionId) {
831
+ var url = '/api/direct-sessions/' + encodeURIComponent(sessionId) + '/summary';
832
+ fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
833
+ .then(function (res) { return res.json(); })
834
+ .then(function (data) {
835
+ showSafeModal('safe_direct_summary · ' + shortId(sessionId), data);
836
+ })
837
+ .catch(function (err) {
838
+ showToast('error', '加载失败', err.message || '未知错误');
839
+ });
840
+ }
841
+
842
+ function withBusy(btn, fn) {
843
+ var orig = btn.innerHTML;
844
+ btn.disabled = true;
845
+ btn.innerHTML = '<span class="inline-block w-3 h-3 align-middle border-2 border-current border-t-transparent rounded-full pw-spin"></span>';
846
+ Promise.resolve()
847
+ .then(fn)
848
+ .catch(function (err) {
849
+ showToast('error', '操作失败', err.message || '未知错误');
850
+ })
851
+ .finally(function () {
852
+ btn.disabled = false;
853
+ btn.innerHTML = orig;
854
+ });
855
+ }
856
+
857
+ function finalizeSession(sessionId, btn) {
858
+ withBusy(btn, function () {
859
+ return postAction('/api/direct-sessions/' + encodeURIComponent(sessionId) + '/finalize')
860
+ .then(function (data) {
861
+ if (data && data.error) {
862
+ showToast('error', 'Finalize 失败', data.error);
863
+ return;
864
+ }
865
+ showToast('success', '已 Finalize', '会话 ' + shortId(sessionId) + ' 已完成 finalize');
866
+ loadSessions();
867
+ });
868
+ });
869
+ }
870
+
871
+ function auditSession(sessionId, btn) {
872
+ withBusy(btn, function () {
873
+ return postAction('/api/direct-sessions/' + encodeURIComponent(sessionId) + '/audit')
874
+ .then(function (data) {
875
+ if (data && data.error) {
876
+ showToast('error', '审计失败', data.error);
877
+ return;
878
+ }
879
+ var decision = (data && data.decision) || '完成';
880
+ showToast('success', '审计完成', '会话 ' + shortId(sessionId) + ' 审计结果: ' + decision);
881
+ loadSessions();
882
+ });
883
+ });
884
+ }
885
+
886
+ function hideSession(sessionId, btn) {
887
+ withBusy(btn, function () {
888
+ return postAction('/api/direct-sessions/' + encodeURIComponent(sessionId) + '/hide')
889
+ .then(function (data) {
890
+ if (data && data.error) {
891
+ showToast('error', '隐藏失败', data.error);
892
+ return;
893
+ }
894
+ showToast('success', '已隐藏', '会话 ' + shortId(sessionId) + ' 已从列表隐藏');
895
+ loadSessions();
896
+ });
897
+ });
898
+ }
899
+
460
900
  // ---------- Render: session detail ----------
461
901
  function renderSessionDetail(data) {
462
902
  var el = $('session-detail');
@@ -473,7 +913,7 @@ html.dark {
473
913
  var vs = s.verification_summary || null;
474
914
  var vsHtml = '—';
475
915
  if (vs && typeof vs === 'object') {
476
- var vsStatus = vs.status || vs.verdict || '—';
916
+ var vsStatus = vs.status || vs.verdict || verificationStatusText(vs) || '—';
477
917
  var vsCmds = Array.isArray(vs.commands) ? vs.commands.map(function (c) { return JSON.stringify(c); }).join(' ') : '';
478
918
  vsHtml = '<span style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(String(vsStatus)) + '</span>'
479
919
  + (vsCmds ? '<div class="mt-1 text-xs break-all" style="color: var(--pw-text-tertiary);">' + escapeHtml(vsCmds) + '</div>' : '');
@@ -644,6 +1084,18 @@ html.dark {
644
1084
  var retryBtn = $('retry-btn');
645
1085
  if (retryBtn) retryBtn.addEventListener('click', loadSessions);
646
1086
 
1087
+ var modalClose = $('safe-modal-close');
1088
+ if (modalClose) modalClose.addEventListener('click', closeSafeModal);
1089
+ // Click on modal backdrop closes it.
1090
+ var safeModal = $('safe-modal');
1091
+ if (safeModal) safeModal.addEventListener('click', function (ev) {
1092
+ if (ev.target === safeModal) closeSafeModal();
1093
+ });
1094
+ // ESC closes modal.
1095
+ document.addEventListener('keydown', function (ev) {
1096
+ if (ev.key === 'Escape') closeSafeModal();
1097
+ });
1098
+
647
1099
  // Initial load
648
1100
  loadSessions();
649
1101
  })();