@goodandready/dsh-approval-gate 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,548 @@
1
+ // lib/client.js
2
+ // Browser client half for DeepSeek Harness: Approval Gate UI (#74, #75, #82, #86, #60, #49, #84, #83, #116).
3
+ // Self-registering module for window.__ModuleLoader__.
4
+ // Canonical languages: English (en) and Chinese (zh).
5
+
6
+ window.__ModuleLoader__.load({
7
+ id: "@goodandready/dsh-approval-gate",
8
+ factory: (require) => {
9
+ var module = { exports: {} };
10
+ const React = require("react");
11
+ const h = React.createElement;
12
+
13
+ const NS = "dsh-approval-gate";
14
+ const PKG = "@goodandready/dsh-approval-gate";
15
+
16
+ // ----------------------------------------------------------- localizations
17
+ const en = {
18
+ title: "Approval Gate",
19
+ tabTitle: "Approvals",
20
+ timelineTitle: "Session Audit Timeline",
21
+ filterAll: "All",
22
+ filterBlocked: "Blocked",
23
+ filterAsked: "Asked",
24
+ filterAllowed: "Allowed",
25
+ noEvents: "No security gate events in this session yet.",
26
+ viewDiff: "View Diff",
27
+ diffModalTitle: "Unified Diff Viewer",
28
+ close: "Close",
29
+ autoTimeoutIn: "Auto-decision in {seconds}s",
30
+ timerPaused: "Timer paused",
31
+ pauseTimer: "Pause",
32
+ resumeTimer: "Resume",
33
+ turnSummary: "Round Summary",
34
+ settingsTitle: "Approval Gate Settings",
35
+ settingsSubtitle: "Security rules, path whitelists and learned rules management",
36
+ rulesEditorTitle: "Rules & Whitelists",
37
+ whitelistPaths: "Path Whitelist",
38
+ addPath: "Add Path",
39
+ learnedRulesTitle: "Learned Routine Rules",
40
+ learnedRulesDesc: "Safe operations auto-learned after repeated manual confirmations",
41
+ revoke: "Revoke",
42
+ noLearnedRules: "No learned rules in active session.",
43
+ toastAutoApproved: "Auto-approved: ",
44
+ modeStrict: "Strict",
45
+ modeHybrid: "Hybrid",
46
+ modeAuto: "Auto",
47
+ modeManual: "Manual",
48
+ };
49
+
50
+ const zh = {
51
+ title: "安全审批网关",
52
+ tabTitle: "审批记录",
53
+ timelineTitle: "会话安全审计时间线",
54
+ filterAll: "全部",
55
+ filterBlocked: "已拦截",
56
+ filterAsked: "待确认",
57
+ filterAllowed: "已放行",
58
+ noEvents: "当前会话暂无安全拦截或审批记录。",
59
+ viewDiff: "查看差异",
60
+ diffModalTitle: "统一差异比对器",
61
+ close: "关闭",
62
+ autoTimeoutIn: "倒计时 {seconds} 秒后自动判定",
63
+ timerPaused: "倒计时已暂停",
64
+ pauseTimer: "暂停",
65
+ resumeTimer: "继续",
66
+ turnSummary: "轮次总结",
67
+ settingsTitle: "审批网关设置",
68
+ settingsSubtitle: "安全规则、路径白名单及自动学习规则管理",
69
+ rulesEditorTitle: "自定义规则与白名单",
70
+ whitelistPaths: "路径白名单",
71
+ addPath: "添加路径",
72
+ learnedRulesTitle: "已学习规则管理",
73
+ learnedRulesDesc: "经过多次手动确认后自动放行的安全例行操作",
74
+ revoke: "撤回",
75
+ noLearnedRules: "当前会话尚无已学习规则。",
76
+ toastAutoApproved: "已自动放行:",
77
+ modeStrict: "严格",
78
+ modeHybrid: "混合",
79
+ modeAuto: "自动",
80
+ modeManual: "手动",
81
+ };
82
+
83
+ // ----------------------------------------------------------- CSS Styles
84
+ const css = `
85
+ .ag-view-tab { display: flex; flex-direction: column; height: 100%; padding: 12px; font-family: inherit; color: var(--dsw-alias-label-primary); }
86
+ .ag-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
87
+ .ag-title { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); }
88
+ .ag-filters { display: flex; gap: 6px; }
89
+ .ag-btn-filter { font-size: 11px; padding: 3px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l2); background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; }
90
+ .ag-btn-filter.ag-active { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-weight: 600; }
91
+
92
+ .ag-turn-card { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; }
93
+ .ag-turn-status { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
94
+ .ag-turn-status.ag-clean { color: #22c55e; }
95
+ .ag-turn-status.ag-warnings { color: #eab308; }
96
+ .ag-turn-status.ag-blocked { color: #ef4444; }
97
+ .ag-turn-desc { font-size: 11px; color: var(--dsw-alias-label-secondary); }
98
+
99
+ .ag-events-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
100
+ .ag-event-item { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 8px; padding: 8px 10px; font-size: 12px; }
101
+ .ag-event-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
102
+ .ag-badge { font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; text-transform: uppercase; }
103
+ .ag-badge-allow { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
104
+ .ag-badge-deny { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
105
+ .ag-badge-ask { background: rgba(234, 179, 8, 0.15); color: #eab308; }
106
+ .ag-badge-preempted { background: rgba(168, 85, 247, 0.15); color: #a855f7; }
107
+
108
+ .ag-tool-name { font-weight: 600; color: var(--dsw-alias-label-primary); }
109
+ .ag-time { font-size: 10px; color: var(--dsw-alias-label-tertiary); }
110
+ .ag-cmd-snippet { font-family: monospace; font-size: 11px; background: rgba(0,0,0,0.1); padding: 4px 6px; border-radius: 4px; margin: 4px 0; word-break: break-all; }
111
+ .ag-event-footer { display: flex; justify-content: space-between; align-items: center; font-size: 10px; color: var(--dsw-alias-label-tertiary); margin-top: 4px; }
112
+ .ag-diff-btn { background: none; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); padding: 2px 6px; border-radius: 4px; cursor: pointer; font-size: 10px; }
113
+
114
+ /* Diff Viewer Modal #74, #75 */
115
+ .ag-diff-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 10000; }
116
+ .ag-diff-modal { width: 85%; max-width: 900px; height: 80%; background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
117
+ .ag-diff-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--dsw-alias-border-l2); }
118
+ .ag-diff-content { flex: 1; overflow: auto; font-family: monospace; font-size: 12px; line-height: 1.5; background: var(--dsw-alias-bg-layer-2); padding: 8px 0; }
119
+ .ag-diff-line { display: flex; padding: 0 8px; }
120
+ .ag-diff-line-add { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
121
+ .ag-diff-line-del { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
122
+ .ag-diff-num { width: 36px; flex: none; text-align: right; padding-right: 8px; color: var(--dsw-alias-label-tertiary); user-select: none; }
123
+ .ag-diff-sign { width: 16px; flex: none; text-align: center; font-weight: bold; user-select: none; }
124
+ .ag-diff-text { flex: 1; white-space: pre-wrap; word-break: break-all; }
125
+ .ag-diff-fold { padding: 4px 12px; background: rgba(0,0,0,0.2); border-top: 1px dashed var(--dsw-alias-border-l2); border-bottom: 1px dashed var(--dsw-alias-border-l2); text-align: center; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 11px; margin: 4px 0; }
126
+ .ag-diff-fold:hover { background: rgba(0,0,0,0.3); color: var(--dsw-alias-label-primary); }
127
+
128
+ /* Countdown Timer #60 */
129
+ .ag-countdown-box { display: flex; align-items: center; gap: 8px; font-size: 11px; color: #eab308; margin-top: 6px; }
130
+ .ag-countdown-badge { background: rgba(234, 179, 8, 0.2); border: 1px solid rgba(234, 179, 8, 0.4); padding: 2px 6px; border-radius: 4px; font-weight: 600; }
131
+ .ag-pause-btn { background: none; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); border-radius: 4px; padding: 1px 6px; cursor: pointer; font-size: 10px; }
132
+
133
+ /* Settings Card #49, #84 */
134
+ .ag-card { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 12px; list-style: none; margin-bottom: 12px; }
135
+ .ag-card-head { appearance: none; width: 100%; font: inherit; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 14px 16px; }
136
+ .ag-card-title { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }
137
+ .ag-card-sub { color: var(--dsw-alias-label-secondary); font-size: 13px; }
138
+ .ag-card-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 12px 0 16px; }
139
+ .ag-chev { margin-left: auto; flex: none; color: var(--dsw-alias-label-tertiary); transition: transform .16s; font-size: 12px; }
140
+ .ag-chev-open { transform: rotate(180deg); }
141
+
142
+ .ag-sec-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); margin: 12px 0 6px; }
143
+ .ag-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 6px; }
144
+ .ag-table th { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); font-weight: 500; }
145
+ .ag-table td { padding: 6px 8px; border-bottom: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); }
146
+ .ag-btn-revoke { background: rgba(239, 68, 68, 0.15); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 4px; padding: 2px 8px; cursor: pointer; font-size: 11px; }
147
+ .ag-input { height: 30px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); border-radius: 6px; padding: 0 8px; font-size: 12px; }
148
+
149
+ /* Floating Toast #83 */
150
+ .ag-toast { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background: var(--dsw-alias-bg-layer-3); border: 1px solid #22c55e; color: var(--dsw-alias-label-primary); border-radius: 20px; padding: 6px 14px; font-size: 12px; display: flex; align-items: center; gap: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.3); z-index: 9999; animation: agFade .2s ease-in; }
151
+ @keyframes agFade { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
152
+
153
+ /* Toolbar Badge #116 */
154
+ .ag-mode-badge { font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 10px; text-transform: uppercase; letter-spacing: 0.5px; cursor: default; }
155
+ .ag-mode-auto { background: rgba(34, 197, 94, 0.2); color: #22c55e; border: 1px solid rgba(34, 197, 94, 0.4); }
156
+ .ag-mode-hybrid { background: rgba(234, 179, 8, 0.2); color: #eab308; border: 1px solid rgba(234, 179, 8, 0.4); }
157
+ .ag-mode-strict { background: rgba(59, 130, 246, 0.2); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.4); }
158
+ .ag-mode-manual { background: rgba(156, 163, 175, 0.2); color: #9ca3af; border: 1px solid rgba(156, 163, 175, 0.4); }
159
+ `;
160
+
161
+ function injectStyles() {
162
+ if (document.querySelector('style[data-dsh-plugin="dsh-approval-gate"]')) return;
163
+ const s = document.createElement("style");
164
+ s.setAttribute("data-dsh-plugin", "dsh-approval-gate");
165
+ s.textContent = css;
166
+ document.head.appendChild(s);
167
+ }
168
+
169
+ // ----------------------------------------------------------- Countdown Component (#60)
170
+ function CountdownBadge({ initialSeconds, onExpire }) {
171
+ const [remaining, setRemaining] = React.useState(initialSeconds || 30);
172
+ const [paused, setPaused] = React.useState(false);
173
+
174
+ React.useEffect(() => {
175
+ if (paused || remaining <= 0) return;
176
+ const timer = setInterval(() => {
177
+ setRemaining((prev) => {
178
+ if (prev <= 1) {
179
+ clearInterval(timer);
180
+ if (typeof onExpire === "function") onExpire();
181
+ return 0;
182
+ }
183
+ return prev - 1;
184
+ });
185
+ }, 1000);
186
+ return () => clearInterval(timer);
187
+ }, [paused, remaining, onExpire]);
188
+
189
+ return h("div", { className: "ag-countdown-box" },
190
+ h("span", { className: "ag-countdown-badge" },
191
+ paused ? "⏸️ Timer paused" : `⏳ Auto-decision in ${remaining}s`
192
+ ),
193
+ h("button", {
194
+ className: "ag-pause-btn",
195
+ onClick: () => setPaused(!paused),
196
+ }, paused ? "Resume" : "Pause")
197
+ );
198
+ }
199
+
200
+ // ----------------------------------------------------------- Floating Toast Component (#83)
201
+ function AutoApprovalToast({ message, onDismiss }) {
202
+ React.useEffect(() => {
203
+ const t = setTimeout(() => { if (onDismiss) onDismiss(); }, 3000);
204
+ return () => clearTimeout(t);
205
+ }, [onDismiss]);
206
+
207
+ if (!message) return null;
208
+ return h("div", { className: "ag-toast", onClick: onDismiss },
209
+ h("span", null, "⚡ Auto-approved:"),
210
+ h("strong", null, message)
211
+ );
212
+ }
213
+
214
+ // ----------------------------------------------------------- Gate Mode Badge Component (#116)
215
+ function GateModeBadge() {
216
+ const [mode, setMode] = React.useState("hybrid");
217
+
218
+ React.useEffect(() => {
219
+ fetch("/dsh-approval-gate/mode")
220
+ .then((r) => r.json())
221
+ .then((d) => { if (d && d.mode) setMode(d.mode); })
222
+ .catch(() => {});
223
+ }, []);
224
+
225
+ const cls = `ag-mode-badge ag-mode-${mode}`;
226
+ return h("span", { className: cls, title: `Approval Gate mode: ${mode}` }, `🛡️ ${mode}`);
227
+ }
228
+
229
+ // ----------------------------------------------------------- Diff Panel Modal (#74, #75)
230
+ function DiffPanelModal({ diffData, onClose }) {
231
+ const [expandedFolds, setExpandedFolds] = React.useState({});
232
+ if (!diffData) return null;
233
+ const hunks = diffData.hunks || [];
234
+
235
+ const toggleFold = (foldKey) => {
236
+ setExpandedFolds((prev) => ({ ...prev, [foldKey]: !prev[foldKey] }));
237
+ };
238
+
239
+ return h("div", { className: "ag-diff-overlay", onClick: onClose },
240
+ h("div", { className: "ag-diff-modal", onClick: (e) => e.stopPropagation() },
241
+ h("div", { className: "ag-diff-header" },
242
+ h("span", { className: "ag-title" }, `Unified Diff: ${diffData.filename || "file"}`),
243
+ h("button", { className: "ag-diff-btn", onClick: onClose }, "✕ Close")
244
+ ),
245
+ h("div", { className: "ag-diff-content" },
246
+ hunks.map((hunk, hIdx) =>
247
+ h("div", { key: `hunk_${hIdx}` },
248
+ h("div", { className: "ag-diff-fold", style: { cursor: "default", fontWeight: "600" } }, hunk.header),
249
+ (hunk.items || []).map((item, iIdx) => {
250
+ if (item.type === "fold") {
251
+ const foldKey = `${hIdx}_${iIdx}`;
252
+ const isExp = Boolean(expandedFolds[foldKey]);
253
+ return h("div", { key: foldKey },
254
+ h("div", {
255
+ className: "ag-diff-fold",
256
+ onClick: () => toggleFold(foldKey),
257
+ }, isExp ? "▴ Collapse unchanged lines" : `▾ Click to expand ${item.count} unchanged lines`),
258
+ isExp && (item.foldedLines || []).map((fl, fIdx) =>
259
+ h("div", { key: `fl_${fIdx}`, className: "ag-diff-line" },
260
+ h("span", { className: "ag-diff-num" }, fl.oldLine || ""),
261
+ h("span", { className: "ag-diff-num" }, fl.newLine || ""),
262
+ h("span", { className: "ag-diff-sign" }, " "),
263
+ h("span", { className: "ag-diff-text" }, fl.text)
264
+ )
265
+ )
266
+ );
267
+ }
268
+ const isAdd = item.type === "add";
269
+ const isDel = item.type === "del";
270
+ const lineClass = isAdd ? "ag-diff-line ag-diff-line-add" : (isDel ? "ag-diff-line ag-diff-line-del" : "ag-diff-line");
271
+ return h("div", { key: `line_${hIdx}_${iIdx}`, className: lineClass },
272
+ h("span", { className: "ag-diff-num" }, item.oldLine || ""),
273
+ h("span", { className: "ag-diff-num" }, item.newLine || ""),
274
+ h("span", { className: "ag-diff-sign" }, isAdd ? "+" : (isDel ? "-" : " ")),
275
+ h("span", { className: "ag-diff-text" }, item.text)
276
+ );
277
+ })
278
+ )
279
+ )
280
+ )
281
+ )
282
+ );
283
+ }
284
+
285
+ // ----------------------------------------------------------- Settings Card Component (#49, #84)
286
+ function ApprovalGateSettingsCard({ ctx }) {
287
+ const [open, setOpen] = React.useState(false);
288
+ const [learnedRules, setLearnedRules] = React.useState([]);
289
+ const [whitelist, setWhitelist] = React.useState([]);
290
+ const [newPath, setNewPath] = React.useState("");
291
+
292
+ const fetchSettings = React.useCallback(async () => {
293
+ try {
294
+ const lrRes = await fetch("/dsh-approval-gate/learned-rules");
295
+ if (lrRes.ok) {
296
+ const lrData = await lrRes.json();
297
+ setLearnedRules(lrData.rules || []);
298
+ }
299
+ const rRes = await fetch("/dsh-approval-gate/rules");
300
+ if (rRes.ok) {
301
+ const rData = await rRes.json();
302
+ setWhitelist(rData.whitelistPaths || []);
303
+ }
304
+ } catch (_) {}
305
+ }, []);
306
+
307
+ React.useEffect(() => {
308
+ if (open) fetchSettings();
309
+ }, [open, fetchSettings]);
310
+
311
+ const revokeRule = async (key) => {
312
+ try {
313
+ await fetch("/dsh-approval-gate/learned-rules/revoke", {
314
+ method: "POST",
315
+ headers: { "Content-Type": "application/json" },
316
+ body: JSON.stringify({ key }),
317
+ });
318
+ setLearnedRules((prev) => prev.filter((r) => r.key !== key));
319
+ } catch (_) {}
320
+ };
321
+
322
+ const addPath = async () => {
323
+ if (!newPath.trim()) return;
324
+ const updated = [...whitelist, newPath.trim()];
325
+ setWhitelist(updated);
326
+ setNewPath("");
327
+ try {
328
+ await fetch("/dsh-approval-gate/rules", {
329
+ method: "POST",
330
+ headers: { "Content-Type": "application/json" },
331
+ body: JSON.stringify({ whitelistPaths: updated }),
332
+ });
333
+ } catch (_) {}
334
+ };
335
+
336
+ return h("div", { className: "ag-card" },
337
+ h("button", {
338
+ className: "ag-card-head",
339
+ onClick: () => setOpen(!open),
340
+ "aria-expanded": open,
341
+ },
342
+ h("div", null,
343
+ h("div", { className: "ag-card-title" }, "🛡️ Approval Gate"),
344
+ h("div", { className: "ag-card-sub" }, "Rules editor, path whitelists and learned rules manager")
345
+ ),
346
+ h("span", { className: `ag-chev ${open ? "ag-chev-open" : ""}` }, "▼")
347
+ ),
348
+ open && h("div", { className: "ag-card-body" },
349
+ // Rules & Whitelists Editor (#49)
350
+ h("div", { className: "ag-sec-title" }, "Path Whitelist"),
351
+ h("div", { style: { display: "flex", gap: "6px", marginBottom: "8px" } },
352
+ h("input", {
353
+ className: "ag-input",
354
+ placeholder: "/workspace/temp",
355
+ value: newPath,
356
+ onChange: (e) => setNewPath(e.target.value),
357
+ style: { flex: 1 },
358
+ }),
359
+ h("button", { className: "ag-diff-btn", onClick: addPath }, "+ Add")
360
+ ),
361
+ whitelist.length > 0 && h("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px", marginBottom: "12px" } },
362
+ whitelist.map((p, idx) => h("span", { key: idx, className: "ag-badge ag-badge-allow" }, p))
363
+ ),
364
+
365
+ // Learned Rules Manager (#84)
366
+ h("div", { className: "ag-sec-title" }, "Learned Rules Manager"),
367
+ learnedRules.length === 0
368
+ ? h("div", { style: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px" } }, "No learned rules in active session.")
369
+ : h("table", { className: "ag-table" },
370
+ h("thead", null,
371
+ h("tr", null,
372
+ h("th", null, "Tool"),
373
+ h("th", null, "Command / Target"),
374
+ h("th", null, "Hits"),
375
+ h("th", null, "Action")
376
+ )
377
+ ),
378
+ h("tbody", null,
379
+ learnedRules.map((rule) =>
380
+ h("tr", { key: rule.key },
381
+ h("td", null, rule.tool || "bash"),
382
+ h("td", { style: { fontFamily: "monospace" } }, rule.command || rule.key),
383
+ h("td", null, rule.count || 1),
384
+ h("td", null,
385
+ h("button", {
386
+ className: "ag-btn-revoke",
387
+ onClick: () => revokeRule(rule.key),
388
+ }, "Revoke")
389
+ )
390
+ )
391
+ )
392
+ )
393
+ )
394
+ )
395
+ );
396
+ }
397
+
398
+ // ----------------------------------------------------------- Timeline Tab (#82, #86)
399
+ function ApprovalTimelineTab({ ctx }) {
400
+ const [filter, setFilter] = React.useState("all");
401
+ const [events, setEvents] = React.useState([]);
402
+ const [turnCard, setTurnCard] = React.useState(null);
403
+ const [activeDiff, setActiveDiff] = React.useState(null);
404
+
405
+ const fetchTimeline = React.useCallback(async () => {
406
+ try {
407
+ const res = await fetch(`/dsh-approval-gate/timeline?filter=${filter}`);
408
+ if (res.ok) {
409
+ const data = await res.json();
410
+ setEvents(data.events || []);
411
+ }
412
+ const cRes = await fetch("/dsh-approval-gate/turn-card");
413
+ if (cRes.ok) {
414
+ const cData = await cRes.json();
415
+ setTurnCard(cData.card);
416
+ }
417
+ } catch (_) {}
418
+ }, [filter]);
419
+
420
+ React.useEffect(() => {
421
+ fetchTimeline();
422
+ const poll = setInterval(fetchTimeline, 3000);
423
+ return () => clearInterval(poll);
424
+ }, [fetchTimeline]);
425
+
426
+ return h("div", { className: "ag-view-tab" },
427
+ h("div", { className: "ag-header" },
428
+ h("span", { className: "ag-title" }, "🛡️ Security Gate"),
429
+ h("div", { className: "ag-filters" },
430
+ ["all", "blocked", "asked", "allowed"].map((f) =>
431
+ h("button", {
432
+ key: f,
433
+ className: `ag-btn-filter ${filter === f ? "ag-active" : ""}`,
434
+ onClick: () => setFilter(f),
435
+ }, f.charAt(0).toUpperCase() + f.slice(1))
436
+ )
437
+ )
438
+ ),
439
+ turnCard && h("div", { className: "ag-turn-card" },
440
+ h("div", { className: `ag-turn-status ag-${turnCard.status}` },
441
+ turnCard.status === "clean" ? "✓ Turn Clean" : (turnCard.status === "blocked" ? "⚠ Operations Blocked" : "ℹ Confirmations Asked")
442
+ ),
443
+ h("div", { className: "ag-turn-desc" }, turnCard.summaryText)
444
+ ),
445
+ h("div", { className: "ag-events-list" },
446
+ events.length === 0
447
+ ? h("div", { style: { padding: "20px", textAlign: "center", color: "var(--dsw-alias-label-tertiary)" } }, "No events recorded yet")
448
+ : events.map((evt) =>
449
+ h("div", { key: evt.id, className: "ag-event-item" },
450
+ h("div", { className: "ag-event-head" },
451
+ h("span", { className: "ag-tool-name" }, evt.tool),
452
+ h("span", { className: `ag-badge ag-badge-${evt.verdict}` }, evt.verdict)
453
+ ),
454
+ evt.command && h("div", { className: "ag-cmd-snippet" }, evt.command),
455
+ evt.reason && h("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px" } }, evt.reason),
456
+ evt.verdict === "ask" && evt.countdownSec && h(CountdownBadge, { initialSeconds: evt.countdownSec }),
457
+ h("div", { className: "ag-event-footer" },
458
+ h("span", { className: "ag-time" }, new Date(evt.timestamp).toLocaleTimeString()),
459
+ h("button", { className: "ag-diff-btn", onClick: () => setActiveDiff(evt.diffMeta || { filename: evt.command || "cmd.sh", hunks: [] }) }, "Diff")
460
+ )
461
+ )
462
+ )
463
+ ),
464
+ activeDiff && h(DiffPanelModal, { diffData: activeDiff, onClose: () => setActiveDiff(null) })
465
+ );
466
+ }
467
+
468
+ // ----------------------------------------------------------- Extension Apply
469
+ function apply(ctx) {
470
+ injectStyles();
471
+ try {
472
+ if (ctx.locale && typeof ctx.locale.register === "function") {
473
+ ctx.locale.register(NS, { en, zh });
474
+ }
475
+ } catch (err) {
476
+ console.warn("[dsh-approval-gate] client locale registration:", err && err.message);
477
+ }
478
+
479
+ // 1. Register right session panel slot: conversation.view (#82)
480
+ try {
481
+ if (ctx.slots && typeof ctx.slots.inject === "function") {
482
+ ctx.slots.inject("conversation.view", function () {
483
+ return ctx.slots.register(
484
+ {
485
+ name: "conversation.view",
486
+ id: PKG,
487
+ order: 45,
488
+ label: function () { return "🛡️ Approvals"; },
489
+ },
490
+ function (props) { return h(ApprovalTimelineTab, { ctx, ...props }); }
491
+ );
492
+ });
493
+ }
494
+ } catch (err) {
495
+ console.warn("[dsh-approval-gate] conversation.view slot registration:", err && err.message);
496
+ }
497
+
498
+ // 2. Register settings card slot: settings.plugin.item (#49, #84)
499
+ try {
500
+ if (ctx.slots && typeof ctx.slots.inject === "function") {
501
+ ctx.slots.inject("settings.plugin.item", function () {
502
+ return ctx.slots.register(
503
+ {
504
+ name: "settings.plugin.item",
505
+ key: NS,
506
+ locale: NS,
507
+ inject: function () { return { ctx }; },
508
+ },
509
+ ApprovalGateSettingsCard
510
+ );
511
+ });
512
+ }
513
+ } catch (err) {
514
+ console.warn("[dsh-approval-gate] settings.plugin.item slot registration:", err && err.message);
515
+ }
516
+
517
+ // 3. Register toolbar badge slot: toolbar.badge (#116)
518
+ try {
519
+ if (ctx.slots && typeof ctx.slots.inject === "function") {
520
+ ctx.slots.inject("toolbar.badge", function () {
521
+ return ctx.slots.register(
522
+ {
523
+ name: "toolbar.badge",
524
+ id: PKG,
525
+ order: 25,
526
+ },
527
+ GateModeBadge
528
+ );
529
+ });
530
+ }
531
+ } catch (err) {
532
+ console.warn("[dsh-approval-gate] toolbar.badge slot registration:", err && err.message);
533
+ }
534
+ }
535
+
536
+ module.exports = {
537
+ apply,
538
+ inject: ["slots", "locale"],
539
+ DiffPanelModal,
540
+ CountdownBadge,
541
+ ApprovalTimelineTab,
542
+ ApprovalGateSettingsCard,
543
+ AutoApprovalToast,
544
+ GateModeBadge,
545
+ };
546
+ return module.exports;
547
+ },
548
+ });