dsh-perm-gate 0.2.0 → 0.2.1-beta.2
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/CHANGELOG.ja.md +97 -0
- package/CHANGELOG.ko.md +96 -0
- package/CHANGELOG.md +146 -0
- package/INSTALL.ja.md +2 -2
- package/INSTALL.ko.md +2 -2
- package/INSTALL.md +3 -2
- package/INSTALL.zh.md +2 -2
- package/README.ja.md +29 -6
- package/README.ko.md +37 -8
- package/README.md +73 -11
- package/README.zh.md +44 -9
- package/cordis.patch.yml +19 -12
- package/lib/audit.d.ts +1 -1
- package/lib/classifier.d.ts +2 -0
- package/lib/classifier.js +7 -4
- package/lib/client.js +1786 -53
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +63 -4
- package/lib/config.js +50 -2
- package/lib/deny-defaults.d.ts +12 -0
- package/lib/deny-defaults.js +19 -0
- package/lib/engine.d.ts +13 -1
- package/lib/engine.js +85 -8
- package/lib/evaluate.d.ts +10 -0
- package/lib/evaluate.js +101 -11
- package/lib/events.d.ts +140 -4
- package/lib/events.js +578 -13
- package/lib/host-llm.d.ts +61 -0
- package/lib/host-llm.js +52 -0
- package/lib/index.d.ts +15 -1
- package/lib/index.js +266 -23
- package/lib/learning.d.ts +19 -2
- package/lib/learning.js +47 -5
- package/lib/llm-presets.d.ts +16 -0
- package/lib/llm-presets.js +11 -0
- package/lib/preset.d.ts +30 -0
- package/lib/preset.js +41 -0
- package/lib/receiver-info.d.ts +76 -0
- package/lib/receiver-info.js +70 -0
- package/lib/risk.d.ts +16 -1
- package/lib/risk.js +15 -3
- package/lib/runtime.d.ts +210 -4
- package/lib/runtime.js +589 -33
- package/package.json +6 -5
package/lib/client.js
CHANGED
|
@@ -19,7 +19,7 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
"card.strategies": "后台审批策略(至少一个生效;可组合)",
|
|
20
20
|
"card.strategy.trustAutoAllow": "trustAutoAllow — 作用域内安全操作自动放行,危险/未知转 ask(中间档基线)",
|
|
21
21
|
"card.strategy.alwaysConfirm": "alwaysConfirm — 一律逐次 ask;审批面板「允许控件」含两个扩展按钮:「本会话重复允许该类」(会话语限次 grant)与「允许所有类型」(命令词持久迁入 rulesFile 白名单)",
|
|
22
|
-
"card.strategy.llmAssist": "llmAssist — 自定义 LLM 按风险分级裁决:safe 自动放行;deletion/credential/remote/system/bulk
|
|
22
|
+
"card.strategy.llmAssist": "llmAssist — 自定义 LLM 按风险分级裁决:safe 自动放行;deletion/credential/remote/system/bulk 硬风险自动拒绝;neutral 进入确认学习;失败/超时回退人工",
|
|
23
23
|
"card.readonly": "只读",
|
|
24
24
|
"card.unavailable": "设置命名空间不可用:请确认 dsh-perm-gate 已装配进 profile。",
|
|
25
25
|
"card.llmReceiver": "llmAssist 接收 LLM(OpenAI 兼容,可用自定义 API)",
|
|
@@ -31,12 +31,91 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
"card.llmKeyOverwrite": "已设置保密值,输入新值并失焦即可覆盖。",
|
|
32
32
|
"card.llmKeyClear": "清除",
|
|
33
33
|
"card.allowlist": "白名单(allow,rulesFile 同步)",
|
|
34
|
-
"card.allowlistHint": "
|
|
34
|
+
"card.allowlistHint": "命中 allow 段的命令模式直接放行;逐条添加或删除,实时写入 rulesFile 并重载。",
|
|
35
|
+
"card.allowlistPresetNote": "预置白名单的等价物是 trustAutoAllow 策略(工作区内安全操作自动放行),在上方策略区开启;本列表镜像你的 rulesFile allow 段,初始为空属正常。",
|
|
36
|
+
"card.allowlistEmpty": "无白名单规则",
|
|
37
|
+
"card.allowlistPlaceholder": "新命令模式,如 npm test*",
|
|
38
|
+
"card.allowlistAdd": "添加",
|
|
39
|
+
"card.allowlistRemove": "删除该模式",
|
|
40
|
+
"card.allowlistBulk": "批量编辑",
|
|
41
|
+
"card.allowlistBulkHide": "收起批量编辑",
|
|
35
42
|
"card.allowlistSave": "保存白名单",
|
|
36
43
|
"card.riskLearning": "riskLearning — 裁决学习:neutral 风险操作经人工确认并执行后计数,满阈值且操作指纹一致时自动放行",
|
|
37
44
|
"card.riskLearningHint": "学习状态为插件自有数据($DSH_HOME/perm-gate/learning.json),不写入你的 YAML 规则文件;不同目标永不复用放行。默认关闭。",
|
|
38
45
|
"card.riskThreshold": "自动放行所需人工确认次数(1–10)",
|
|
39
|
-
"notice.label": "权限门禁动态"
|
|
46
|
+
"notice.label": "权限门禁动态",
|
|
47
|
+
"history.label": "审批记录",
|
|
48
|
+
"history.title": "权限审批记录",
|
|
49
|
+
"history.subtitle": "本会话中门禁的每次判定轨迹(最新在上)",
|
|
50
|
+
"history.loading": "加载中…",
|
|
51
|
+
"history.empty": "本会话暂无审批记录",
|
|
52
|
+
"history.error": "加载失败:",
|
|
53
|
+
"history.tag.auto": "自动放行",
|
|
54
|
+
"history.tag.ask": "转人工",
|
|
55
|
+
"history.tag.deny": "拒绝",
|
|
56
|
+
"card.denylistRestore": "恢复预置黑名单",
|
|
57
|
+
"card.denylist": "黑名单(deny 关键词,插件预置)",
|
|
58
|
+
"card.denylistHint": "命中关键词(大小写不敏感子串)的调用直接拒绝,先于白名单/授权/LLM。预置列表继承自 dsh-approval-gate,安装即生效;可增删,清空或删除后可用「恢复预置黑名单」一键还原(空列表等同预置,黑名单常开)。",
|
|
59
|
+
"card.denylistEmpty": "黑名单为空(所有调用都不会被关键词层拒绝)",
|
|
60
|
+
"card.denylistPlaceholder": "新关键词,如 drop database",
|
|
61
|
+
"card.denylistAdd": "添加",
|
|
62
|
+
"card.denylistRemove": "删除该关键词",
|
|
63
|
+
"card.denylistTagPreset": "预置",
|
|
64
|
+
"card.denylistTagCustom": "自定义",
|
|
65
|
+
"card.riskSediment": "riskSediment — 学习沉淀:满阈值的确认样本成为确定性放行规则",
|
|
66
|
+
"card.riskSedimentHint": "开启后,同一「工具|类别」确认满阈值,其样本指纹即沉淀为放行规则——同工具同目标精确命中时直接放行,不再过 LLM(关闭 llmAssist 也继续生效)。",
|
|
67
|
+
"card.sediment": "学习沉淀(确定性放行规则)",
|
|
68
|
+
"card.sedimentHint": "来自人工确认的沉淀规则:样本指纹命中才放行,不同目标永不复用。可终止单个学习或删除单条样本。",
|
|
69
|
+
"card.sedimentEmpty": "暂无沉淀规则(确认满阈值的样本会出现在这里)",
|
|
70
|
+
"card.sedimentCount": "已确认 %n/%t",
|
|
71
|
+
"card.sedimentStop": "终止",
|
|
72
|
+
"card.sedimentStopTitle": "终止该学习(删除计数与样本)",
|
|
73
|
+
"card.sedimentStopConfirm": "终止「%k」的学习?将删除其确认计数与全部样本。",
|
|
74
|
+
"card.sedimentSampleRemove": "删除该沉淀样本",
|
|
75
|
+
"card.llmSource": "接收 LLM 来源",
|
|
76
|
+
"card.llmSourceCustom": "自定义 API(OpenAI 兼容)",
|
|
77
|
+
"card.llmSourceHost": "使用会话当前默认模型(DSH llm 服务)",
|
|
78
|
+
"card.llmSourceHostHint": "默认使用 DSH 会话当前选中的模型组执行审批判定(下方显示当前生效值);Provider / Model 留空即跟随会话,也可从下拉选择或手动输入固定下来——你在 DSH 里配置的自定义组(如 local-35b)会自动出现在下拉中。",
|
|
79
|
+
"card.llmProvider": "Provider(留空 = 跟随会话当前选择)",
|
|
80
|
+
"card.llmProviderPlaceholder": "跟随会话当前选择",
|
|
81
|
+
"card.llmModelHostPlaceholder": "留空 = 跟随会话当前选择",
|
|
82
|
+
"card.llmResolved": "当前生效:%p / %m",
|
|
83
|
+
"card.llmPreset": "预置端点(选择后自动填充,可再编辑)",
|
|
84
|
+
"card.llmPresetChoose": "选择预置…",
|
|
85
|
+
"card.llmPresetPublic": "公共 API 预置",
|
|
86
|
+
"card.llmPresetHost": "DSH 模型组(选择后自动切换为会话模式)",
|
|
87
|
+
"card.healthStale": "宿主路由不可用(404)——node 半区仍是旧版本,请完全重启 dsh(而非仅刷新页面)后重试。",
|
|
88
|
+
"card.healthTest": "健康测试",
|
|
89
|
+
"card.healthRunning": "测试中…",
|
|
90
|
+
"card.healthOk": "正常(%ms ms)",
|
|
91
|
+
"card.healthFail": "失败:",
|
|
92
|
+
"history.tag.learned": "学习沉淀",
|
|
93
|
+
"history.tag.manualApproved": "人工通过",
|
|
94
|
+
"history.tag.manualRejected": "人工拒绝",
|
|
95
|
+
"history.tag.manualCancelled": "人工取消",
|
|
96
|
+
"history.learnedProgress": "学习 %n/%t",
|
|
97
|
+
"history.snapshots": "diff 快照",
|
|
98
|
+
"history.clearSession": "仅清本会话",
|
|
99
|
+
"history.clearAll": "清空全部",
|
|
100
|
+
"history.clearSessionTitle": "仅清除本会话的 diff 快照(不影响其他会话)",
|
|
101
|
+
"history.clearAllTitle": "清空全部会话(含其他会话未查看过的)diff 快照,需二次确认",
|
|
102
|
+
"history.clearSessionConfirm": "确定清除【本会话】的 diff 快照?仅删除本会话审批产生的改动对比数据,不影响审批记录本身。",
|
|
103
|
+
"history.clearAllConfirm": "确定清除【全部会话】的 diff 快照?这可能删除其他会话还没看过的改动对比记录,且不可恢复。",
|
|
104
|
+
"history.diffTitle": "文件改动对比",
|
|
105
|
+
"history.diffClose": "关闭",
|
|
106
|
+
"history.diffLoading": "加载中…",
|
|
107
|
+
"history.diffEmpty": "该文件无内容变化(或文件不可读)",
|
|
108
|
+
"history.diffLoadFail": "加载 diff 失败:",
|
|
109
|
+
"history.diffRevert": "撤销此改动",
|
|
110
|
+
"history.diffReverting": "发送中…",
|
|
111
|
+
"history.diffRevertDone": "已发送撤销指令",
|
|
112
|
+
"history.diffRevertSent": "撤销指令已发送到对话框,AI 将按指令恢复文件",
|
|
113
|
+
"history.diffRevertFail": "发送失败:",
|
|
114
|
+
"history.diffNoSnapshot": "该事件没有此文件的快照",
|
|
115
|
+
"history.diffChipTitle": "查看该文件改动对比",
|
|
116
|
+
"history.diffStats": "+%a / -%r 行变更 · %c 行未变",
|
|
117
|
+
"history.diffGone": "(文件当前已不存在)",
|
|
118
|
+
"history.diffHidden": "%n 行未修改"
|
|
40
119
|
};
|
|
41
120
|
/** English dictionary (keys mirror zh). */
|
|
42
121
|
const en = {
|
|
@@ -47,7 +126,7 @@ window.__ModuleLoader__.load({
|
|
|
47
126
|
"card.strategies": "Backend approval strategies (at least one effective; combinable)",
|
|
48
127
|
"card.strategy.trustAutoAllow": "trustAutoAllow — safe in-scope ops auto-allow; dangerous/unknown ask (baseline middle tier)",
|
|
49
128
|
"card.strategy.alwaysConfirm": "alwaysConfirm — every crossing asks; the approval \"allow controls\" get two extended buttons: \"repeat-allow this type this session\" (bounded session grant) and \"allow every occurrence\" (persist the command word into the rulesFile whitelist)",
|
|
50
|
-
"card.strategy.llmAssist": "llmAssist — a custom LLM grades risk: safe auto-allows; deletion/credential/remote/system/bulk hard risks
|
|
129
|
+
"card.strategy.llmAssist": "llmAssist — a custom LLM grades risk: safe auto-allows; deletion/credential/remote/system/bulk hard risks auto-deny; neutral enters confirm-learning; failure/timeout falls back to the human",
|
|
51
130
|
"card.readonly": "Read-only",
|
|
52
131
|
"card.unavailable": "Settings namespace unavailable: make sure dsh-perm-gate is assembled into this profile.",
|
|
53
132
|
"card.llmReceiver": "llmAssist receiving LLM (OpenAI-compatible; any custom API)",
|
|
@@ -59,14 +138,315 @@ window.__ModuleLoader__.load({
|
|
|
59
138
|
"card.llmKeyOverwrite": "A secret is stored; type a new value and blur to overwrite it.",
|
|
60
139
|
"card.llmKeyClear": "Clear",
|
|
61
140
|
"card.allowlist": "Whitelist (allow, synced to rulesFile)",
|
|
62
|
-
"card.allowlistHint": "
|
|
141
|
+
"card.allowlistHint": "Patterns in the allow section allow directly; add or remove entries one by one, written to the rulesFile and reloaded live.",
|
|
142
|
+
"card.allowlistPresetNote": "The preset-whitelist equivalent is the trustAutoAllow strategy (safe in-scope ops auto-allow), enabled in the strategies section above; this list mirrors your rulesFile allow section and legitimately starts empty.",
|
|
143
|
+
"card.allowlistEmpty": "No whitelist rules",
|
|
144
|
+
"card.allowlistPlaceholder": "New command pattern, e.g. npm test*",
|
|
145
|
+
"card.allowlistAdd": "Add",
|
|
146
|
+
"card.allowlistRemove": "Remove this pattern",
|
|
147
|
+
"card.allowlistBulk": "Bulk edit",
|
|
148
|
+
"card.allowlistBulkHide": "Hide bulk edit",
|
|
63
149
|
"card.allowlistSave": "Save whitelist",
|
|
64
150
|
"card.riskLearning": "riskLearning — verdict learning: neutral-risk asks that the human approves and that execute count up; the same operation auto-allows at threshold with a matching fingerprint",
|
|
65
151
|
"card.riskLearningHint": "Learning state is plugin-owned ($DSH_HOME/perm-gate/learning.json) and never written into your YAML rules; a different target never reuses authority. Off by default.",
|
|
66
152
|
"card.riskThreshold": "Human confirmations required before auto-allow (1–10)",
|
|
67
|
-
"notice.label": "Permission gate activity"
|
|
153
|
+
"notice.label": "Permission gate activity",
|
|
154
|
+
"history.label": "Approvals",
|
|
155
|
+
"history.title": "Permission approval records",
|
|
156
|
+
"history.subtitle": "Every gate decision in this conversation, newest first",
|
|
157
|
+
"history.loading": "Loading…",
|
|
158
|
+
"history.empty": "No approval records in this session yet",
|
|
159
|
+
"history.error": "Failed to load: ",
|
|
160
|
+
"history.tag.auto": "Auto-allowed",
|
|
161
|
+
"history.tag.ask": "Asked",
|
|
162
|
+
"history.tag.deny": "Denied",
|
|
163
|
+
"card.denylistRestore": "Restore preset blacklist",
|
|
164
|
+
"card.denylist": "Blacklist (deny keywords, plugin preset)",
|
|
165
|
+
"card.denylistHint": "A call whose text contains a keyword (case-insensitive substring) is denied outright, before whitelist / grants / LLM. The preset list is inherited from dsh-approval-gate and active from install; edit freely — empty or cleared falls back to the preset (one-click restore available), so the blacklist never silently turns off.",
|
|
166
|
+
"card.denylistEmpty": "Blacklist empty (no call is vetoed by the keyword layer)",
|
|
167
|
+
"card.denylistPlaceholder": "New keyword, e.g. drop database",
|
|
168
|
+
"card.denylistAdd": "Add",
|
|
169
|
+
"card.denylistRemove": "Remove this keyword",
|
|
170
|
+
"card.denylistTagPreset": "Preset",
|
|
171
|
+
"card.denylistTagCustom": "Custom",
|
|
172
|
+
"card.riskSediment": "riskSediment — learning sedimentation: threshold-reached samples become deterministic auto-allows",
|
|
173
|
+
"card.riskSedimentHint": "When on, once a tool|category key reaches the threshold its sample fingerprints are sedimented into allow rules — an exact same-tool same-target hit allows outright without another LLM call (keeps working with llmAssist off).",
|
|
174
|
+
"card.sediment": "Learning sediment (deterministic allow rules)",
|
|
175
|
+
"card.sedimentHint": "Rules sedimented from human confirmations: only an exact fingerprint hit allows, never a different target. Terminate one learning or remove one sample.",
|
|
176
|
+
"card.sedimentEmpty": "No sedimented rules yet (samples reaching the threshold appear here)",
|
|
177
|
+
"card.sedimentCount": "confirmed %n/%t",
|
|
178
|
+
"card.sedimentStop": "Terminate",
|
|
179
|
+
"card.sedimentStopTitle": "Terminate this learning (drops the count and samples)",
|
|
180
|
+
"card.sedimentStopConfirm": "Terminate learning for '%k'? Its count and samples are dropped.",
|
|
181
|
+
"card.sedimentSampleRemove": "Remove this sedimented sample",
|
|
182
|
+
"card.llmSource": "Receiver source",
|
|
183
|
+
"card.llmSourceCustom": "Custom API (OpenAI-compatible)",
|
|
184
|
+
"card.llmSourceHost": "Use the session's current default model (DSH llm service)",
|
|
185
|
+
"card.llmSourceHostHint": "By default approval grading runs on the model group the DSH session currently uses (the effective value is shown below); leave Provider / Model empty to follow the session, or pick from the dropdown or type to pin one — custom groups you configured in DSH (e.g. local-35b) appear automatically.",
|
|
186
|
+
"card.llmProvider": "Provider (empty = follow the session selection)",
|
|
187
|
+
"card.llmProviderPlaceholder": "Follow the session selection",
|
|
188
|
+
"card.llmModelHostPlaceholder": "Empty = follow the session selection",
|
|
189
|
+
"card.llmResolved": "Effective now: %p / %m",
|
|
190
|
+
"card.llmPreset": "Endpoint presets (fills the fields; still editable)",
|
|
191
|
+
"card.llmPresetChoose": "Pick a preset…",
|
|
192
|
+
"card.llmPresetPublic": "Public API presets",
|
|
193
|
+
"card.llmPresetHost": "DSH model groups (selecting one switches to the session receiver)",
|
|
194
|
+
"card.healthStale": "Host route unavailable (404) — the node half is still an old build; fully restart dsh (not just the browser page) and retry.",
|
|
195
|
+
"card.healthTest": "Health test",
|
|
196
|
+
"card.healthRunning": "Testing…",
|
|
197
|
+
"card.healthOk": "OK (%ms ms)",
|
|
198
|
+
"card.healthFail": "Failed: ",
|
|
199
|
+
"history.tag.learned": "Learned",
|
|
200
|
+
"history.tag.manualApproved": "Approved",
|
|
201
|
+
"history.tag.manualRejected": "Rejected",
|
|
202
|
+
"history.tag.manualCancelled": "Cancelled",
|
|
203
|
+
"history.learnedProgress": "learned %n/%t",
|
|
204
|
+
"history.snapshots": "diff snapshots",
|
|
205
|
+
"history.clearSession": "Clear this session",
|
|
206
|
+
"history.clearAll": "Clear all",
|
|
207
|
+
"history.clearSessionTitle": "Clear only this session’s diff snapshots (other sessions untouched)",
|
|
208
|
+
"history.clearAllTitle": "Clear diff snapshots for every session, including ones never reviewed — asks again",
|
|
209
|
+
"history.clearSessionConfirm": "Clear this session’s diff snapshots? Only the change-comparison data produced by this session’s approvals is removed; the approval records stay.",
|
|
210
|
+
"history.clearAllConfirm": "Clear diff snapshots for ALL sessions? This may delete change comparisons other sessions have not reviewed yet, and cannot be undone.",
|
|
211
|
+
"history.diffTitle": "File change comparison",
|
|
212
|
+
"history.diffClose": "Close",
|
|
213
|
+
"history.diffLoading": "Loading…",
|
|
214
|
+
"history.diffEmpty": "No content change (or the file is unreadable)",
|
|
215
|
+
"history.diffLoadFail": "Failed to load diff: ",
|
|
216
|
+
"history.diffRevert": "Revert this change",
|
|
217
|
+
"history.diffReverting": "Sending…",
|
|
218
|
+
"history.diffRevertDone": "Revert instruction sent",
|
|
219
|
+
"history.diffRevertSent": "Revert instruction sent to the conversation; the AI will restore the file accordingly",
|
|
220
|
+
"history.diffRevertFail": "Send failed: ",
|
|
221
|
+
"history.diffNoSnapshot": "This event has no snapshot for that file",
|
|
222
|
+
"history.diffChipTitle": "View this file’s change comparison",
|
|
223
|
+
"history.diffStats": "+%a / -%r lines changed · %c unchanged",
|
|
224
|
+
"history.diffGone": " (file no longer exists)",
|
|
225
|
+
"history.diffHidden": "%n unmodified lines"
|
|
68
226
|
};
|
|
69
227
|
//#endregion
|
|
228
|
+
//#region src/deny-defaults.ts
|
|
229
|
+
/**
|
|
230
|
+
* Preset deny keywords inherited from dsh-approval-gate's `DEFAULT_DENY_KEYWORDS`
|
|
231
|
+
* (the preset blacklist layer of its pipeline). One keyword per entry, matched
|
|
232
|
+
* case-insensitively on word boundaries against the call's command text and its
|
|
233
|
+
* scalar arguments — document bodies are skipped — so a keyword can no longer
|
|
234
|
+
* veto an unrelated identifier or file text that merely contains it.
|
|
235
|
+
*
|
|
236
|
+
* Pure data shared by the node half (deny-keyword pre-layer) and the client
|
|
237
|
+
* bundle (the settings card's preset tags), so it must stay import-free.
|
|
238
|
+
*/
|
|
239
|
+
/** The preset blacklist; the gate applies it whenever the namespace carries no override. */
|
|
240
|
+
const DEFAULT_DENY_KEYWORDS = [
|
|
241
|
+
"rm -rf",
|
|
242
|
+
"rm -fr",
|
|
243
|
+
"rm -r -f",
|
|
244
|
+
"rm --recursive --force",
|
|
245
|
+
"push --force",
|
|
246
|
+
"force-push",
|
|
247
|
+
"force push",
|
|
248
|
+
"drop table",
|
|
249
|
+
"drop database",
|
|
250
|
+
"mkfs",
|
|
251
|
+
"mkfs.ext",
|
|
252
|
+
"format",
|
|
253
|
+
"shutdown",
|
|
254
|
+
"reboot",
|
|
255
|
+
"dd of=",
|
|
256
|
+
"delete from",
|
|
257
|
+
"truncate table",
|
|
258
|
+
"truncate ",
|
|
259
|
+
"terraform destroy",
|
|
260
|
+
"revoke",
|
|
261
|
+
"清空数据库",
|
|
262
|
+
"删除数据库",
|
|
263
|
+
"格式化",
|
|
264
|
+
"sudo rm",
|
|
265
|
+
"chmod 777 /",
|
|
266
|
+
"git reset --hard",
|
|
267
|
+
"git clean -fd",
|
|
268
|
+
"docker rm",
|
|
269
|
+
"docker system prune"
|
|
270
|
+
];
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/llm-presets.ts
|
|
273
|
+
const LLM_PRESETS = [
|
|
274
|
+
{
|
|
275
|
+
id: "deepseek",
|
|
276
|
+
label: "DeepSeek",
|
|
277
|
+
endpoint: "https://api.deepseek.com/v1",
|
|
278
|
+
model: "deepseek-chat"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "xiaomi-mimo",
|
|
282
|
+
label: "小米 MiMo",
|
|
283
|
+
endpoint: "https://api.xiaomimimo.com/v1",
|
|
284
|
+
model: "mimo-v2.5"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "openai",
|
|
288
|
+
label: "OpenAI",
|
|
289
|
+
endpoint: "https://api.openai.com/v1",
|
|
290
|
+
model: "gpt-4o-mini"
|
|
291
|
+
}
|
|
292
|
+
];
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/client/sediment.tsx
|
|
295
|
+
/**
|
|
296
|
+
* Learning-sediment management section — the settings-card face of verdict
|
|
297
|
+
* learning's "沉淀" (the ⑤ card pattern demonstrated by dsh-approval-gate):
|
|
298
|
+
* every threshold-reached key's confirmed samples, listed as the deterministic
|
|
299
|
+
* auto-allow rules they now are, with per-key terminate and per-sample remove.
|
|
300
|
+
*
|
|
301
|
+
* Data source: the host's `GET /api/dsh-perm-gate/learning` route (the same
|
|
302
|
+
* learning.json store the gate reads); actions POST back. Best-effort: a
|
|
303
|
+
* missing route renders the empty state, never an error.
|
|
304
|
+
*/
|
|
305
|
+
const POLL_MS$2 = 5e3;
|
|
306
|
+
const itemStyle = {
|
|
307
|
+
display: "flex",
|
|
308
|
+
alignItems: "center",
|
|
309
|
+
gap: "8px",
|
|
310
|
+
padding: "4px 8px",
|
|
311
|
+
borderRadius: "6px",
|
|
312
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
313
|
+
background: "var(--dsw-alias-bg-surface, transparent)"
|
|
314
|
+
};
|
|
315
|
+
const chipStyle = {
|
|
316
|
+
fontSize: "11px",
|
|
317
|
+
lineHeight: "16px",
|
|
318
|
+
padding: "0 6px",
|
|
319
|
+
borderRadius: "8px",
|
|
320
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
321
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
322
|
+
};
|
|
323
|
+
const delButtonStyle = {
|
|
324
|
+
flex: "0 0 auto",
|
|
325
|
+
width: "20px",
|
|
326
|
+
height: "20px",
|
|
327
|
+
border: "none",
|
|
328
|
+
borderRadius: "999px",
|
|
329
|
+
background: "transparent",
|
|
330
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
331
|
+
cursor: "pointer"
|
|
332
|
+
};
|
|
333
|
+
function SedimentSection({ t }) {
|
|
334
|
+
const [body, setBody] = (0, react.useState)(null);
|
|
335
|
+
(0, react.useEffect)(() => {
|
|
336
|
+
let alive = true;
|
|
337
|
+
const load = () => {
|
|
338
|
+
fetch("/api/dsh-perm-gate/learning", { headers: { "cache-control": "no-cache" } }).then((r) => r.ok ? r.json() : Promise.reject(/* @__PURE__ */ new Error(`HTTP ${r.status}`))).then((data) => {
|
|
339
|
+
if (alive) setBody(data);
|
|
340
|
+
}).catch(() => {
|
|
341
|
+
if (alive) setBody({});
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
load();
|
|
345
|
+
const timer = setInterval(load, POLL_MS$2);
|
|
346
|
+
return () => {
|
|
347
|
+
alive = false;
|
|
348
|
+
clearInterval(timer);
|
|
349
|
+
};
|
|
350
|
+
}, []);
|
|
351
|
+
const post = (payload) => {
|
|
352
|
+
fetch("/api/dsh-perm-gate/learning", {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: { "content-type": "application/json" },
|
|
355
|
+
body: JSON.stringify(payload)
|
|
356
|
+
}).catch(() => {});
|
|
357
|
+
};
|
|
358
|
+
const threshold = body?.threshold ?? 3;
|
|
359
|
+
const confirmed = body?.confirmed ?? {};
|
|
360
|
+
const samples = body?.samples ?? {};
|
|
361
|
+
const keys = Object.entries(confirmed).filter(([, count]) => count >= threshold);
|
|
362
|
+
if (body === null) return null;
|
|
363
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
364
|
+
style: { marginTop: "6px" },
|
|
365
|
+
children: keys.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
366
|
+
style: {
|
|
367
|
+
padding: "4px 0",
|
|
368
|
+
fontSize: "12px",
|
|
369
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
370
|
+
},
|
|
371
|
+
children: t("card.sedimentEmpty")
|
|
372
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
373
|
+
style: {
|
|
374
|
+
display: "flex",
|
|
375
|
+
flexDirection: "column",
|
|
376
|
+
gap: "6px"
|
|
377
|
+
},
|
|
378
|
+
children: keys.map(([key, count]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
379
|
+
style: {
|
|
380
|
+
display: "flex",
|
|
381
|
+
flexDirection: "column",
|
|
382
|
+
gap: "4px"
|
|
383
|
+
},
|
|
384
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
385
|
+
style: itemStyle,
|
|
386
|
+
children: [
|
|
387
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
388
|
+
style: {
|
|
389
|
+
font: "600 12px/18px var(--ds-font-family-code, monospace)",
|
|
390
|
+
color: "var(--dsw-alias-label-primary)"
|
|
391
|
+
},
|
|
392
|
+
children: key
|
|
393
|
+
}),
|
|
394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
395
|
+
style: chipStyle,
|
|
396
|
+
children: t("card.sedimentCount").replace("%n", String(count)).replace("%t", String(threshold))
|
|
397
|
+
}),
|
|
398
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: "1 1 auto" } }),
|
|
399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
400
|
+
type: "button",
|
|
401
|
+
title: t("card.sedimentStopTitle"),
|
|
402
|
+
onClick: () => {
|
|
403
|
+
if (window.confirm(t("card.sedimentStopConfirm").replace("%k", key))) post({ key });
|
|
404
|
+
},
|
|
405
|
+
style: {
|
|
406
|
+
...delButtonStyle,
|
|
407
|
+
width: "auto",
|
|
408
|
+
padding: "0 8px",
|
|
409
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
410
|
+
},
|
|
411
|
+
children: t("card.sedimentStop")
|
|
412
|
+
})
|
|
413
|
+
]
|
|
414
|
+
}), (samples[key] ?? []).map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
415
|
+
style: {
|
|
416
|
+
...itemStyle,
|
|
417
|
+
marginLeft: "16px"
|
|
418
|
+
},
|
|
419
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
|
|
420
|
+
style: {
|
|
421
|
+
flex: "1 1 auto",
|
|
422
|
+
minWidth: 0,
|
|
423
|
+
overflowWrap: "anywhere",
|
|
424
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
425
|
+
color: "var(--dsw-alias-label-secondary, inherit)"
|
|
426
|
+
},
|
|
427
|
+
children: [s.fp, s.ctx !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
428
|
+
style: { color: "var(--dsw-alias-label-tertiary)" },
|
|
429
|
+
children: ` — ${s.ctx}`
|
|
430
|
+
}) : null]
|
|
431
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
432
|
+
type: "button",
|
|
433
|
+
"aria-label": t("card.sedimentSampleRemove"),
|
|
434
|
+
title: t("card.sedimentSampleRemove"),
|
|
435
|
+
onClick: () => {
|
|
436
|
+
post({
|
|
437
|
+
key,
|
|
438
|
+
fp: s.fp
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
style: delButtonStyle,
|
|
442
|
+
children: "✕"
|
|
443
|
+
})]
|
|
444
|
+
}, `${key}:${s.fp}`))]
|
|
445
|
+
}, key))
|
|
446
|
+
})
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
//#endregion
|
|
70
450
|
//#region src/client/card.tsx
|
|
71
451
|
/**
|
|
72
452
|
* Permissive settings card — the `settings.plugin.item` face of dsh-perm-gate.
|
|
@@ -157,6 +537,50 @@ window.__ModuleLoader__.load({
|
|
|
157
537
|
scope.set("allowlist", lines);
|
|
158
538
|
setAllowDirty(false);
|
|
159
539
|
};
|
|
540
|
+
const [addDraft, setAddDraft] = (0, react.useState)("");
|
|
541
|
+
const [bulkOpen, setBulkOpen] = (0, react.useState)(false);
|
|
542
|
+
const [healthBusy, setHealthBusy] = (0, react.useState)(false);
|
|
543
|
+
const [healthResult, setHealthResult] = (0, react.useState)(null);
|
|
544
|
+
const receiverSource = (value.classifierSource ?? "custom") === "host" ? "host" : "custom";
|
|
545
|
+
const [receiverInfo, setReceiverInfo] = (0, react.useState)(null);
|
|
546
|
+
const [receiverNonce, setReceiverNonce] = (0, react.useState)(0);
|
|
547
|
+
(0, react.useEffect)(() => {
|
|
548
|
+
let alive = true;
|
|
549
|
+
fetch("/api/dsh-perm-gate/receiver", { headers: { "cache-control": "no-cache" } }).then((r) => r.ok ? r.json() : Promise.reject(/* @__PURE__ */ new Error(`HTTP ${r.status}`))).then((data) => {
|
|
550
|
+
if (alive) setReceiverInfo(data);
|
|
551
|
+
}).catch(() => {
|
|
552
|
+
if (alive) setReceiverInfo(null);
|
|
553
|
+
});
|
|
554
|
+
return () => {
|
|
555
|
+
alive = false;
|
|
556
|
+
};
|
|
557
|
+
}, [receiverSource, receiverNonce]);
|
|
558
|
+
const receiverProviderDraft = value.classifierProvider ?? "";
|
|
559
|
+
const patterns = value.allowlist ?? [];
|
|
560
|
+
const removePattern = (index) => {
|
|
561
|
+
scope.set("allowlist", patterns.filter((_, i) => i !== index));
|
|
562
|
+
};
|
|
563
|
+
const addPattern = () => {
|
|
564
|
+
const next = addDraft.trim();
|
|
565
|
+
if (next === "" || readonly) return;
|
|
566
|
+
scope.set("allowlist", [...patterns, next]);
|
|
567
|
+
setAddDraft("");
|
|
568
|
+
};
|
|
569
|
+
const denyActive = Array.isArray(value.denyKeywords) && value.denyKeywords.length > 0;
|
|
570
|
+
const denylist = denyActive ? value.denyKeywords : [...DEFAULT_DENY_KEYWORDS];
|
|
571
|
+
const [denyDraft, setDenyDraft] = (0, react.useState)("");
|
|
572
|
+
const removeDeny = (index) => {
|
|
573
|
+
scope.set("denyKeywords", denylist.filter((_, i) => i !== index));
|
|
574
|
+
};
|
|
575
|
+
const addDeny = () => {
|
|
576
|
+
const next = denyDraft.trim();
|
|
577
|
+
if (next === "" || readonly) return;
|
|
578
|
+
scope.set("denyKeywords", [...denylist, next]);
|
|
579
|
+
setDenyDraft("");
|
|
580
|
+
};
|
|
581
|
+
const restoreDeny = () => {
|
|
582
|
+
scope.unset("denyKeywords");
|
|
583
|
+
};
|
|
160
584
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
161
585
|
style: {
|
|
162
586
|
border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))",
|
|
@@ -329,39 +753,153 @@ window.__ModuleLoader__.load({
|
|
|
329
753
|
style: fieldStyle,
|
|
330
754
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
331
755
|
style: fieldLabelStyle,
|
|
332
|
-
children: t("card.
|
|
333
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
334
|
-
|
|
335
|
-
value: value.classifierEndpoint ?? "",
|
|
756
|
+
children: t("card.llmSource")
|
|
757
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
758
|
+
value: value.classifierSource ?? "custom",
|
|
336
759
|
disabled: readonly,
|
|
337
|
-
placeholder: "https://api.openai.com/v1",
|
|
338
760
|
style: controlStyle,
|
|
339
761
|
onChange: (event) => {
|
|
340
|
-
scope.set("
|
|
762
|
+
scope.set("classifierSource", event.currentTarget.value);
|
|
341
763
|
},
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
764
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
765
|
+
value: "custom",
|
|
766
|
+
children: t("card.llmSourceCustom")
|
|
767
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
768
|
+
value: "host",
|
|
769
|
+
children: t("card.llmSourceHost")
|
|
770
|
+
})]
|
|
345
771
|
})]
|
|
346
772
|
}),
|
|
347
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
773
|
+
(value.classifierSource ?? "custom") === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
774
|
+
style: fieldStyle,
|
|
775
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
776
|
+
style: fieldLabelStyle,
|
|
777
|
+
children: t("card.llmPreset")
|
|
778
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
779
|
+
value: "",
|
|
780
|
+
disabled: readonly,
|
|
781
|
+
style: controlStyle,
|
|
782
|
+
onChange: (event) => {
|
|
783
|
+
const v = event.currentTarget.value;
|
|
784
|
+
if (v.startsWith("host:")) {
|
|
785
|
+
scope.set("classifierSource", "host");
|
|
786
|
+
scope.set("classifierProvider", v.slice(5));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const preset = LLM_PRESETS.find((p) => p.id === v);
|
|
790
|
+
if (preset !== void 0) {
|
|
791
|
+
scope.set("classifierEndpoint", preset.endpoint);
|
|
792
|
+
scope.set("classifierModel", preset.model);
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
children: [
|
|
796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
797
|
+
value: "",
|
|
798
|
+
children: t("card.llmPresetChoose")
|
|
799
|
+
}),
|
|
800
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
|
|
801
|
+
label: t("card.llmPresetPublic"),
|
|
802
|
+
children: LLM_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
803
|
+
value: preset.id,
|
|
804
|
+
children: preset.label
|
|
805
|
+
}, preset.id))
|
|
806
|
+
}),
|
|
807
|
+
(receiverInfo?.providers ?? []).length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
|
|
808
|
+
label: t("card.llmPresetHost"),
|
|
809
|
+
children: (receiverInfo?.providers ?? []).map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
810
|
+
value: `host:${p.id}`,
|
|
811
|
+
children: `${t("card.llmSourceHost")} · ${p.id}`
|
|
812
|
+
}, `host:${p.id}`))
|
|
813
|
+
}) : null
|
|
814
|
+
]
|
|
815
|
+
})]
|
|
816
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
348
817
|
style: fieldStyle,
|
|
349
818
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
350
819
|
style: fieldLabelStyle,
|
|
351
|
-
children: t("card.
|
|
820
|
+
children: t("card.llmEndpoint")
|
|
352
821
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
353
822
|
type: "text",
|
|
354
|
-
value: value.
|
|
823
|
+
value: value.classifierEndpoint ?? "",
|
|
355
824
|
disabled: readonly,
|
|
356
|
-
placeholder: "
|
|
825
|
+
placeholder: "https://api.openai.com/v1",
|
|
357
826
|
style: controlStyle,
|
|
358
827
|
onChange: (event) => {
|
|
359
|
-
scope.set("
|
|
828
|
+
scope.set("classifierEndpoint", event.currentTarget.value);
|
|
360
829
|
},
|
|
361
830
|
onBlur: (event) => {
|
|
362
|
-
if (event.currentTarget.value.trim() === "") scope.unset("
|
|
831
|
+
if (event.currentTarget.value.trim() === "") scope.unset("classifierEndpoint");
|
|
363
832
|
}
|
|
364
833
|
})]
|
|
834
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
835
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
836
|
+
style: hintStyle,
|
|
837
|
+
children: t("card.llmSourceHostHint")
|
|
838
|
+
}),
|
|
839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
840
|
+
style: fieldStyle,
|
|
841
|
+
children: [
|
|
842
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
843
|
+
style: fieldLabelStyle,
|
|
844
|
+
children: t("card.llmProvider")
|
|
845
|
+
}),
|
|
846
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
847
|
+
type: "text",
|
|
848
|
+
list: "perm-gate-provider-options",
|
|
849
|
+
value: value.classifierProvider ?? "",
|
|
850
|
+
disabled: readonly,
|
|
851
|
+
placeholder: t("card.llmProviderPlaceholder"),
|
|
852
|
+
style: controlStyle,
|
|
853
|
+
onChange: (event) => {
|
|
854
|
+
scope.set("classifierProvider", event.currentTarget.value);
|
|
855
|
+
},
|
|
856
|
+
onBlur: (event) => {
|
|
857
|
+
if (event.currentTarget.value.trim() === "") scope.unset("classifierProvider");
|
|
858
|
+
}
|
|
859
|
+
}),
|
|
860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
861
|
+
id: "perm-gate-provider-options",
|
|
862
|
+
children: (receiverInfo?.providers ?? []).map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
863
|
+
value: p.id,
|
|
864
|
+
children: [p.name, p.error !== void 0 ? ` (${p.error})` : ""]
|
|
865
|
+
}, p.id))
|
|
866
|
+
})
|
|
867
|
+
]
|
|
868
|
+
}),
|
|
869
|
+
receiverInfo?.selection !== null && receiverInfo?.selection !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
870
|
+
style: hintStyle,
|
|
871
|
+
children: t("card.llmResolved").replace("%p", receiverInfo.selection.provider).replace("%m", receiverInfo.selection.model)
|
|
872
|
+
}) : null
|
|
873
|
+
] }),
|
|
874
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
875
|
+
style: fieldStyle,
|
|
876
|
+
children: [
|
|
877
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
878
|
+
style: fieldLabelStyle,
|
|
879
|
+
children: t("card.llmModel")
|
|
880
|
+
}),
|
|
881
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
882
|
+
type: "text",
|
|
883
|
+
list: receiverSource === "host" ? "perm-gate-model-options" : void 0,
|
|
884
|
+
value: value.classifierModel ?? "",
|
|
885
|
+
disabled: readonly,
|
|
886
|
+
placeholder: receiverSource === "host" ? t("card.llmModelHostPlaceholder") : "deepseek-chat",
|
|
887
|
+
style: controlStyle,
|
|
888
|
+
onChange: (event) => {
|
|
889
|
+
scope.set("classifierModel", event.currentTarget.value);
|
|
890
|
+
},
|
|
891
|
+
onBlur: (event) => {
|
|
892
|
+
if (event.currentTarget.value.trim() === "") scope.unset("classifierModel");
|
|
893
|
+
}
|
|
894
|
+
}),
|
|
895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
896
|
+
id: "perm-gate-model-options",
|
|
897
|
+
children: (receiverInfo?.providers ?? []).filter((p) => receiverProviderDraft === "" || p.id === receiverProviderDraft).flatMap((p) => p.models).map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
898
|
+
value: m.id,
|
|
899
|
+
children: m.name
|
|
900
|
+
}, `${m.id}`))
|
|
901
|
+
})
|
|
902
|
+
]
|
|
365
903
|
}),
|
|
366
904
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
367
905
|
style: fieldStyle,
|
|
@@ -405,6 +943,55 @@ window.__ModuleLoader__.load({
|
|
|
405
943
|
})
|
|
406
944
|
]
|
|
407
945
|
}),
|
|
946
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
947
|
+
style: {
|
|
948
|
+
display: "flex",
|
|
949
|
+
alignItems: "center",
|
|
950
|
+
gap: "8px",
|
|
951
|
+
marginTop: "4px"
|
|
952
|
+
},
|
|
953
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
954
|
+
type: "button",
|
|
955
|
+
disabled: readonly || healthBusy,
|
|
956
|
+
onClick: () => {
|
|
957
|
+
setHealthBusy(true);
|
|
958
|
+
setHealthResult(null);
|
|
959
|
+
fetch("/api/dsh-perm-gate/health", {
|
|
960
|
+
method: "POST",
|
|
961
|
+
headers: { "content-type": "application/json" },
|
|
962
|
+
body: "{}"
|
|
963
|
+
}).then(async (r) => {
|
|
964
|
+
const text = await r.text();
|
|
965
|
+
if (r.status === 404) throw new Error(t("card.healthStale"));
|
|
966
|
+
try {
|
|
967
|
+
return JSON.parse(text);
|
|
968
|
+
} catch {
|
|
969
|
+
throw new Error(text.trim().slice(0, 120) !== "" ? text.trim().slice(0, 120) : `HTTP ${r.status}`);
|
|
970
|
+
}
|
|
971
|
+
}).then((res) => {
|
|
972
|
+
setHealthResult(res.ok === true && typeof res.ms === "number" ? t("card.healthOk").replace("%ms", String(res.ms)) + (typeof res.detail === "string" ? ` · ${res.detail}` : "") : t("card.healthFail") + (res.detail ?? res.error ?? "unknown"));
|
|
973
|
+
setReceiverNonce((n) => n + 1);
|
|
974
|
+
}).catch((e) => {
|
|
975
|
+
setHealthResult(t("card.healthFail") + String(e?.message ?? e));
|
|
976
|
+
}).finally(() => {
|
|
977
|
+
setHealthBusy(false);
|
|
978
|
+
});
|
|
979
|
+
},
|
|
980
|
+
style: {
|
|
981
|
+
...controlStyle,
|
|
982
|
+
cursor: readonly || healthBusy ? "default" : "pointer"
|
|
983
|
+
},
|
|
984
|
+
children: healthBusy ? t("card.healthRunning") : t("card.healthTest")
|
|
985
|
+
}), healthResult !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
986
|
+
style: {
|
|
987
|
+
fontSize: "12px",
|
|
988
|
+
lineHeight: "18px",
|
|
989
|
+
color: "var(--dsw-alias-label-secondary, inherit)",
|
|
990
|
+
overflowWrap: "anywhere"
|
|
991
|
+
},
|
|
992
|
+
children: healthResult
|
|
993
|
+
})]
|
|
994
|
+
}),
|
|
408
995
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
409
996
|
style: rowStyle,
|
|
410
997
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
@@ -446,7 +1033,43 @@ window.__ModuleLoader__.load({
|
|
|
446
1033
|
if (Number.isFinite(n) && n >= 1 && n <= 10) scope.set("riskThreshold", n);
|
|
447
1034
|
}
|
|
448
1035
|
})]
|
|
449
|
-
}) : null
|
|
1036
|
+
}) : null,
|
|
1037
|
+
value.riskLearning === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1038
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1039
|
+
style: rowStyle,
|
|
1040
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1041
|
+
htmlFor: "plugin-config-perm-gate-risk-sediment",
|
|
1042
|
+
style: { fontSize: "12px" },
|
|
1043
|
+
children: t("card.riskSediment")
|
|
1044
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1045
|
+
id: "plugin-config-perm-gate-risk-sediment",
|
|
1046
|
+
type: "checkbox",
|
|
1047
|
+
checked: value.riskSediment ?? true,
|
|
1048
|
+
disabled: readonly || !effective,
|
|
1049
|
+
onChange: (event) => {
|
|
1050
|
+
scope.set("riskSediment", event.currentTarget.checked);
|
|
1051
|
+
}
|
|
1052
|
+
})]
|
|
1053
|
+
}),
|
|
1054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1055
|
+
style: hintStyle,
|
|
1056
|
+
children: t("card.riskSedimentHint")
|
|
1057
|
+
}),
|
|
1058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1059
|
+
style: sectionStyle,
|
|
1060
|
+
children: [
|
|
1061
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1062
|
+
style: labelStyle,
|
|
1063
|
+
children: t("card.sediment")
|
|
1064
|
+
}),
|
|
1065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1066
|
+
style: hintStyle,
|
|
1067
|
+
children: t("card.sedimentHint")
|
|
1068
|
+
}),
|
|
1069
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SedimentSection, { t })
|
|
1070
|
+
]
|
|
1071
|
+
})
|
|
1072
|
+
] }) : null
|
|
450
1073
|
]
|
|
451
1074
|
}) : null
|
|
452
1075
|
]
|
|
@@ -462,7 +1085,123 @@ window.__ModuleLoader__.load({
|
|
|
462
1085
|
style: hintStyle,
|
|
463
1086
|
children: t("card.allowlistHint")
|
|
464
1087
|
}),
|
|
465
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1088
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1089
|
+
style: hintStyle,
|
|
1090
|
+
children: t("card.allowlistPresetNote")
|
|
1091
|
+
}),
|
|
1092
|
+
patterns.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1093
|
+
style: {
|
|
1094
|
+
padding: "6px 0",
|
|
1095
|
+
fontSize: "12px",
|
|
1096
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1097
|
+
},
|
|
1098
|
+
children: t("card.allowlistEmpty")
|
|
1099
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1100
|
+
style: {
|
|
1101
|
+
display: "flex",
|
|
1102
|
+
flexDirection: "column",
|
|
1103
|
+
gap: "4px",
|
|
1104
|
+
margin: "6px 0"
|
|
1105
|
+
},
|
|
1106
|
+
children: patterns.map((pattern, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1107
|
+
style: {
|
|
1108
|
+
display: "flex",
|
|
1109
|
+
alignItems: "center",
|
|
1110
|
+
gap: "8px",
|
|
1111
|
+
padding: "4px 8px",
|
|
1112
|
+
borderRadius: "6px",
|
|
1113
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1114
|
+
background: "var(--dsw-alias-bg-surface, transparent)"
|
|
1115
|
+
},
|
|
1116
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1117
|
+
style: {
|
|
1118
|
+
flex: "1 1 auto",
|
|
1119
|
+
minWidth: 0,
|
|
1120
|
+
overflowWrap: "anywhere",
|
|
1121
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1122
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1123
|
+
},
|
|
1124
|
+
children: pattern
|
|
1125
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1126
|
+
type: "button",
|
|
1127
|
+
"aria-label": t("card.allowlistRemove"),
|
|
1128
|
+
title: t("card.allowlistRemove"),
|
|
1129
|
+
disabled: readonly,
|
|
1130
|
+
onClick: () => {
|
|
1131
|
+
removePattern(index);
|
|
1132
|
+
},
|
|
1133
|
+
style: {
|
|
1134
|
+
flex: "0 0 auto",
|
|
1135
|
+
width: "20px",
|
|
1136
|
+
height: "20px",
|
|
1137
|
+
border: "none",
|
|
1138
|
+
borderRadius: "999px",
|
|
1139
|
+
background: "transparent",
|
|
1140
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
1141
|
+
cursor: readonly ? "default" : "pointer"
|
|
1142
|
+
},
|
|
1143
|
+
children: "✕"
|
|
1144
|
+
})]
|
|
1145
|
+
}, `${index}:${pattern}`))
|
|
1146
|
+
}),
|
|
1147
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1148
|
+
style: {
|
|
1149
|
+
display: "flex",
|
|
1150
|
+
gap: "6px",
|
|
1151
|
+
marginTop: "6px"
|
|
1152
|
+
},
|
|
1153
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1154
|
+
type: "text",
|
|
1155
|
+
value: addDraft,
|
|
1156
|
+
disabled: readonly,
|
|
1157
|
+
placeholder: t("card.allowlistPlaceholder"),
|
|
1158
|
+
spellCheck: false,
|
|
1159
|
+
style: {
|
|
1160
|
+
...controlStyle,
|
|
1161
|
+
flex: "1 1 auto",
|
|
1162
|
+
minWidth: 0
|
|
1163
|
+
},
|
|
1164
|
+
onChange: (event) => {
|
|
1165
|
+
setAddDraft(event.currentTarget.value);
|
|
1166
|
+
},
|
|
1167
|
+
onKeyDown: (event) => {
|
|
1168
|
+
if (event.key === "Enter") addPattern();
|
|
1169
|
+
}
|
|
1170
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1171
|
+
type: "button",
|
|
1172
|
+
disabled: readonly || addDraft.trim() === "",
|
|
1173
|
+
onClick: addPattern,
|
|
1174
|
+
style: {
|
|
1175
|
+
...controlStyle,
|
|
1176
|
+
flex: "0 0 auto",
|
|
1177
|
+
cursor: readonly || addDraft.trim() === "" ? "default" : "pointer",
|
|
1178
|
+
opacity: readonly || addDraft.trim() === "" ? .5 : 1
|
|
1179
|
+
},
|
|
1180
|
+
children: t("card.allowlistAdd")
|
|
1181
|
+
})]
|
|
1182
|
+
}),
|
|
1183
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1184
|
+
style: {
|
|
1185
|
+
display: "flex",
|
|
1186
|
+
justifyContent: "flex-end",
|
|
1187
|
+
marginTop: "6px"
|
|
1188
|
+
},
|
|
1189
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1190
|
+
type: "button",
|
|
1191
|
+
onClick: () => {
|
|
1192
|
+
setBulkOpen((current) => !current);
|
|
1193
|
+
},
|
|
1194
|
+
style: {
|
|
1195
|
+
...controlStyle,
|
|
1196
|
+
cursor: "pointer",
|
|
1197
|
+
border: "none",
|
|
1198
|
+
background: "transparent",
|
|
1199
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1200
|
+
},
|
|
1201
|
+
children: bulkOpen ? t("card.allowlistBulkHide") : t("card.allowlistBulk")
|
|
1202
|
+
})
|
|
1203
|
+
}),
|
|
1204
|
+
bulkOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
466
1205
|
id: "plugin-config-perm-gate-allowlist",
|
|
467
1206
|
value: allowlistShown,
|
|
468
1207
|
disabled: readonly,
|
|
@@ -483,8 +1222,7 @@ window.__ModuleLoader__.load({
|
|
|
483
1222
|
onKeyDown: (event) => {
|
|
484
1223
|
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) commitAllowlist();
|
|
485
1224
|
}
|
|
486
|
-
}),
|
|
487
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1225
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
488
1226
|
style: {
|
|
489
1227
|
display: "flex",
|
|
490
1228
|
justifyContent: "flex-end",
|
|
@@ -501,7 +1239,142 @@ window.__ModuleLoader__.load({
|
|
|
501
1239
|
},
|
|
502
1240
|
children: t("card.allowlistSave")
|
|
503
1241
|
})
|
|
504
|
-
})
|
|
1242
|
+
})] }) : null
|
|
1243
|
+
]
|
|
1244
|
+
}),
|
|
1245
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1246
|
+
style: sectionStyle,
|
|
1247
|
+
children: [
|
|
1248
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1249
|
+
style: labelStyle,
|
|
1250
|
+
children: t("card.denylist")
|
|
1251
|
+
}),
|
|
1252
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1253
|
+
style: hintStyle,
|
|
1254
|
+
children: t("card.denylistHint")
|
|
1255
|
+
}),
|
|
1256
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1257
|
+
style: {
|
|
1258
|
+
display: "flex",
|
|
1259
|
+
flexDirection: "column",
|
|
1260
|
+
gap: "4px",
|
|
1261
|
+
margin: "6px 0"
|
|
1262
|
+
},
|
|
1263
|
+
children: denylist.map((keyword, index) => {
|
|
1264
|
+
const preset = DEFAULT_DENY_KEYWORDS.includes(keyword);
|
|
1265
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1266
|
+
style: {
|
|
1267
|
+
display: "flex",
|
|
1268
|
+
alignItems: "center",
|
|
1269
|
+
gap: "8px",
|
|
1270
|
+
padding: "4px 8px",
|
|
1271
|
+
borderRadius: "6px",
|
|
1272
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1273
|
+
background: "var(--dsw-alias-bg-surface, transparent)"
|
|
1274
|
+
},
|
|
1275
|
+
children: [
|
|
1276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1277
|
+
style: {
|
|
1278
|
+
flex: "1 1 auto",
|
|
1279
|
+
minWidth: 0,
|
|
1280
|
+
overflowWrap: "anywhere",
|
|
1281
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1282
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1283
|
+
},
|
|
1284
|
+
children: keyword
|
|
1285
|
+
}),
|
|
1286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1287
|
+
style: {
|
|
1288
|
+
flex: "0 0 auto",
|
|
1289
|
+
fontSize: "11px",
|
|
1290
|
+
lineHeight: "16px",
|
|
1291
|
+
padding: "0 6px",
|
|
1292
|
+
borderRadius: "8px",
|
|
1293
|
+
color: preset ? "var(--dsw-alias-label-tertiary)" : "var(--dsw-alias-label-secondary, inherit)",
|
|
1294
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
1295
|
+
},
|
|
1296
|
+
children: preset ? t("card.denylistTagPreset") : t("card.denylistTagCustom")
|
|
1297
|
+
}),
|
|
1298
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1299
|
+
type: "button",
|
|
1300
|
+
"aria-label": t("card.denylistRemove"),
|
|
1301
|
+
title: t("card.denylistRemove"),
|
|
1302
|
+
disabled: readonly,
|
|
1303
|
+
onClick: () => {
|
|
1304
|
+
removeDeny(index);
|
|
1305
|
+
},
|
|
1306
|
+
style: {
|
|
1307
|
+
flex: "0 0 auto",
|
|
1308
|
+
width: "20px",
|
|
1309
|
+
height: "20px",
|
|
1310
|
+
border: "none",
|
|
1311
|
+
borderRadius: "999px",
|
|
1312
|
+
background: "transparent",
|
|
1313
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
1314
|
+
cursor: readonly ? "default" : "pointer"
|
|
1315
|
+
},
|
|
1316
|
+
children: "✕"
|
|
1317
|
+
})
|
|
1318
|
+
]
|
|
1319
|
+
}, `${index}:${keyword}`);
|
|
1320
|
+
})
|
|
1321
|
+
}),
|
|
1322
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1323
|
+
style: {
|
|
1324
|
+
display: "flex",
|
|
1325
|
+
gap: "6px",
|
|
1326
|
+
marginTop: "6px"
|
|
1327
|
+
},
|
|
1328
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1329
|
+
type: "text",
|
|
1330
|
+
value: denyDraft,
|
|
1331
|
+
disabled: readonly,
|
|
1332
|
+
placeholder: t("card.denylistPlaceholder"),
|
|
1333
|
+
spellCheck: false,
|
|
1334
|
+
style: {
|
|
1335
|
+
...controlStyle,
|
|
1336
|
+
flex: "1 1 auto",
|
|
1337
|
+
minWidth: 0
|
|
1338
|
+
},
|
|
1339
|
+
onChange: (event) => {
|
|
1340
|
+
setDenyDraft(event.currentTarget.value);
|
|
1341
|
+
},
|
|
1342
|
+
onKeyDown: (event) => {
|
|
1343
|
+
if (event.key === "Enter") addDeny();
|
|
1344
|
+
}
|
|
1345
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1346
|
+
type: "button",
|
|
1347
|
+
disabled: readonly || denyDraft.trim() === "",
|
|
1348
|
+
onClick: addDeny,
|
|
1349
|
+
style: {
|
|
1350
|
+
...controlStyle,
|
|
1351
|
+
flex: "0 0 auto",
|
|
1352
|
+
cursor: readonly || denyDraft.trim() === "" ? "default" : "pointer",
|
|
1353
|
+
opacity: readonly || denyDraft.trim() === "" ? .5 : 1
|
|
1354
|
+
},
|
|
1355
|
+
children: t("card.denylistAdd")
|
|
1356
|
+
})]
|
|
1357
|
+
}),
|
|
1358
|
+
denyActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1359
|
+
style: {
|
|
1360
|
+
display: "flex",
|
|
1361
|
+
justifyContent: "flex-end",
|
|
1362
|
+
marginTop: "6px"
|
|
1363
|
+
},
|
|
1364
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1365
|
+
type: "button",
|
|
1366
|
+
disabled: readonly,
|
|
1367
|
+
onClick: restoreDeny,
|
|
1368
|
+
style: {
|
|
1369
|
+
...controlStyle,
|
|
1370
|
+
cursor: readonly ? "default" : "pointer",
|
|
1371
|
+
border: "none",
|
|
1372
|
+
background: "transparent",
|
|
1373
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1374
|
+
},
|
|
1375
|
+
children: t("card.denylistRestore")
|
|
1376
|
+
})
|
|
1377
|
+
}) : null
|
|
505
1378
|
]
|
|
506
1379
|
}),
|
|
507
1380
|
!snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -626,22 +1499,8 @@ window.__ModuleLoader__.load({
|
|
|
626
1499
|
};
|
|
627
1500
|
}
|
|
628
1501
|
//#endregion
|
|
629
|
-
//#region src/client/
|
|
630
|
-
/**
|
|
631
|
-
* Gate decision notice strip — the `conversation.input.dock` face of
|
|
632
|
-
* dsh-perm-gate (a simplified version of the review-feed pattern demonstrated
|
|
633
|
-
* by dsh-approval-gate).
|
|
634
|
-
*
|
|
635
|
-
* Polls the host's `GET /api/dsh-perm-gate/events?sessionId=&since=` feed every
|
|
636
|
-
* 2 s and shows the latest decision above the conversation input: auto-allows
|
|
637
|
-
* (green) and denies (red) auto-dismiss after a few seconds; an ask (amber)
|
|
638
|
-
* stays until the next event because it needs the human's attention.
|
|
639
|
-
*
|
|
640
|
-
* Everything is best-effort: no session id, a missing route, or any fetch
|
|
641
|
-
* failure simply means "render nothing". No @deepseek-ai value imports.
|
|
642
|
-
*/
|
|
643
|
-
const POLL_MS = 2e3;
|
|
644
|
-
const AUTO_HIDE_MS = 4e3;
|
|
1502
|
+
//#region src/client/feed.ts
|
|
1503
|
+
/** Fetch decision events for one session; `since > 0` returns only newer ones. */
|
|
645
1504
|
async function fetchEvents(sessionId, since) {
|
|
646
1505
|
const query = `/api/dsh-perm-gate/events?sessionId=${encodeURIComponent(sessionId)}${since > 0 ? `&since=${since}` : ""}`;
|
|
647
1506
|
const res = await fetch(query, { headers: { "cache-control": "no-cache" } });
|
|
@@ -649,19 +1508,22 @@ window.__ModuleLoader__.load({
|
|
|
649
1508
|
const body = await res.json();
|
|
650
1509
|
return Array.isArray(body.events) ? body.events : [];
|
|
651
1510
|
}
|
|
652
|
-
/** Resolve the current session id from
|
|
1511
|
+
/** Resolve the current session id from slot props (top level, nested, or hook). */
|
|
653
1512
|
function resolveSessionId(props) {
|
|
654
1513
|
const direct = props?.sessionId;
|
|
655
1514
|
if (typeof direct === "string" && direct !== "") return direct;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1515
|
+
const nested = props?.slotsProps;
|
|
1516
|
+
if (typeof nested?.sessionId === "string" && nested.sessionId !== "") return nested.sessionId;
|
|
1517
|
+
for (const hook of [props?.useSessions, nested?.useSessions]) {
|
|
1518
|
+
if (typeof hook !== "function") continue;
|
|
1519
|
+
try {
|
|
1520
|
+
const state = hook((s) => s);
|
|
659
1521
|
if (typeof state?.current === "string" && state.current !== "") return state.current;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
1522
|
+
} catch {}
|
|
1523
|
+
}
|
|
662
1524
|
return null;
|
|
663
1525
|
}
|
|
664
|
-
/** Presentation per event kind: accent color,
|
|
1526
|
+
/** Presentation per event kind: accent color, background wash, and tag. */
|
|
665
1527
|
function presentation(kind) {
|
|
666
1528
|
switch (kind) {
|
|
667
1529
|
case "deny": return {
|
|
@@ -682,6 +1544,24 @@ window.__ModuleLoader__.load({
|
|
|
682
1544
|
tag: "LEARNED",
|
|
683
1545
|
sticky: false
|
|
684
1546
|
};
|
|
1547
|
+
case "manual-approved": return {
|
|
1548
|
+
color: "var(--dsw-alias-state-warn-label, #b9770e)",
|
|
1549
|
+
bg: "var(--dsw-alias-state-warn-tertiary, rgba(185,119,14,0.08))",
|
|
1550
|
+
tag: "APPROVED",
|
|
1551
|
+
sticky: false
|
|
1552
|
+
};
|
|
1553
|
+
case "manual-rejected": return {
|
|
1554
|
+
color: "var(--dsw-alias-state-error-primary, #c0392b)",
|
|
1555
|
+
bg: "var(--dsw-alias-interactive-bg-hover-danger, rgba(192,57,43,0.08))",
|
|
1556
|
+
tag: "REJECTED",
|
|
1557
|
+
sticky: false
|
|
1558
|
+
};
|
|
1559
|
+
case "manual-cancelled": return {
|
|
1560
|
+
color: "var(--dsw-alias-label-secondary, #666)",
|
|
1561
|
+
bg: "var(--dsw-alias-bg-module-platform, rgba(127,127,127,0.08))",
|
|
1562
|
+
tag: "CANCELLED",
|
|
1563
|
+
sticky: false
|
|
1564
|
+
};
|
|
685
1565
|
default: return {
|
|
686
1566
|
color: "var(--dsw-alias-state-success-primary, #1e8449)",
|
|
687
1567
|
bg: "var(--dsw-alias-state-success-tertiary, rgba(30,132,73,0.08))",
|
|
@@ -690,8 +1570,141 @@ window.__ModuleLoader__.load({
|
|
|
690
1570
|
};
|
|
691
1571
|
}
|
|
692
1572
|
}
|
|
693
|
-
|
|
694
|
-
|
|
1573
|
+
/**
|
|
1574
|
+
* Decision-path labels shown beside an event's tool name. Kept alongside
|
|
1575
|
+
* `presentation` (not in the dictionary) because they name host-internal
|
|
1576
|
+
* decision sources; an unknown value falls through to its raw string.
|
|
1577
|
+
*/
|
|
1578
|
+
const VERDICT_LABELS = {
|
|
1579
|
+
rule: "规则命中",
|
|
1580
|
+
grant: "会话授权",
|
|
1581
|
+
"hard-deny": "硬拒绝",
|
|
1582
|
+
"deny-keyword": "黑名单关键词",
|
|
1583
|
+
default: "默认策略",
|
|
1584
|
+
ask: "默认策略",
|
|
1585
|
+
permissive: "Permissive 放行",
|
|
1586
|
+
classifier: "LLM 裁决",
|
|
1587
|
+
"llm-safe": "LLM 判定安全",
|
|
1588
|
+
"llm-learned": "LLM 学习放行",
|
|
1589
|
+
"learned-sediment": "沉淀规则放行",
|
|
1590
|
+
"learned-confirm": "学习确认",
|
|
1591
|
+
"human-approved": "人工通过",
|
|
1592
|
+
"human-rejected": "人工拒绝",
|
|
1593
|
+
"human-cancelled": "人工取消",
|
|
1594
|
+
"no-approval-channel": "无审批通道"
|
|
1595
|
+
};
|
|
1596
|
+
/** Compact wall-clock rendering of an ISO timestamp. */
|
|
1597
|
+
function clockTime(iso) {
|
|
1598
|
+
const date = new Date(iso);
|
|
1599
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
1600
|
+
return date.toLocaleTimeString([], {
|
|
1601
|
+
hour: "2-digit",
|
|
1602
|
+
minute: "2-digit",
|
|
1603
|
+
second: "2-digit"
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
/** Human-readable byte size for the snapshot bar. */
|
|
1607
|
+
function fmtBytes(n) {
|
|
1608
|
+
if (!Number.isFinite(n) || n <= 0) return "0 B";
|
|
1609
|
+
if (n < 1024) return `${n} B`;
|
|
1610
|
+
if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`;
|
|
1611
|
+
return `${(n / 1048576).toFixed(2)} MB`;
|
|
1612
|
+
}
|
|
1613
|
+
/** Load the snapshot inventory (optionally session-scoped); null on failure. */
|
|
1614
|
+
async function fetchSnapshotStats(sessionId) {
|
|
1615
|
+
const query = sessionId === null ? "" : `?sessionId=${encodeURIComponent(sessionId)}`;
|
|
1616
|
+
const res = await fetch(`/api/dsh-perm-gate/snapshots-stats${query}`, { headers: { "cache-control": "no-cache" } });
|
|
1617
|
+
if (!res.ok) return null;
|
|
1618
|
+
const body = await res.json();
|
|
1619
|
+
if (body.ok !== true) return null;
|
|
1620
|
+
const files = {};
|
|
1621
|
+
if (typeof body.files === "object" && body.files !== null) {
|
|
1622
|
+
for (const [key, value] of Object.entries(body.files)) if (Array.isArray(value)) files[key] = value.map(String);
|
|
1623
|
+
}
|
|
1624
|
+
return {
|
|
1625
|
+
count: typeof body.count === "number" ? body.count : 0,
|
|
1626
|
+
bytes: typeof body.bytes === "number" ? body.bytes : 0,
|
|
1627
|
+
ids: Array.isArray(body.ids) ? body.ids.map(String) : [],
|
|
1628
|
+
files
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
/** Drop snapshots: pass a session id for a session-scoped clear, null for all. */
|
|
1632
|
+
async function clearSnapshots(sessionId) {
|
|
1633
|
+
try {
|
|
1634
|
+
return (await fetch("/api/dsh-perm-gate/snapshots-clear", {
|
|
1635
|
+
method: "POST",
|
|
1636
|
+
headers: { "content-type": "application/json" },
|
|
1637
|
+
body: JSON.stringify(sessionId === null ? {} : { sessionId })
|
|
1638
|
+
})).ok;
|
|
1639
|
+
} catch {
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
/** Load the before/after diff for one event's file; throws on failure. */
|
|
1644
|
+
async function fetchDiff(eventId, path) {
|
|
1645
|
+
const res = await fetch(`/api/dsh-perm-gate/diff?eventId=${eventId}&path=${encodeURIComponent(path)}`, { headers: { "cache-control": "no-cache" } });
|
|
1646
|
+
const body = await res.json().catch(() => null);
|
|
1647
|
+
if (body === null || body.ok !== true) throw new Error(typeof body?.error === "string" ? body.error : `HTTP ${res.status}`);
|
|
1648
|
+
return {
|
|
1649
|
+
hunks: Array.isArray(body.hunks) ? body.hunks : [],
|
|
1650
|
+
stats: typeof body.stats === "object" && body.stats !== null ? body.stats : {
|
|
1651
|
+
added: 0,
|
|
1652
|
+
removed: 0,
|
|
1653
|
+
contextLines: 0
|
|
1654
|
+
},
|
|
1655
|
+
afterExists: body.afterExists !== false
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
/** Deliver a revert instruction for one event into its conversation. */
|
|
1659
|
+
async function sendRevert(sessionId, eventId) {
|
|
1660
|
+
try {
|
|
1661
|
+
const res = await fetch("/api/dsh-perm-gate/revert", {
|
|
1662
|
+
method: "POST",
|
|
1663
|
+
headers: { "content-type": "application/json" },
|
|
1664
|
+
body: JSON.stringify({
|
|
1665
|
+
sessionId,
|
|
1666
|
+
eventId
|
|
1667
|
+
})
|
|
1668
|
+
});
|
|
1669
|
+
const body = await res.json().catch(() => null);
|
|
1670
|
+
if (body !== null && body.ok === true) return { ok: true };
|
|
1671
|
+
return {
|
|
1672
|
+
ok: false,
|
|
1673
|
+
error: typeof body?.error === "string" ? body.error : `HTTP ${res.status}`
|
|
1674
|
+
};
|
|
1675
|
+
} catch (e) {
|
|
1676
|
+
return {
|
|
1677
|
+
ok: false,
|
|
1678
|
+
error: String(e?.message ?? e)
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
//#endregion
|
|
1683
|
+
//#region src/client/notice.tsx
|
|
1684
|
+
/**
|
|
1685
|
+
* Gate decision notice strip — the `conversation.input.dock` face of
|
|
1686
|
+
* dsh-perm-gate (a simplified version of the review-feed pattern demonstrated
|
|
1687
|
+
* by dsh-approval-gate).
|
|
1688
|
+
*
|
|
1689
|
+
* Polls the host's `GET /api/dsh-perm-gate/events?sessionId=&since=` feed every
|
|
1690
|
+
* 2 s and shows the latest decision above the conversation input: auto-allows
|
|
1691
|
+
* (green) and denies (red) auto-dismiss after a few seconds; an ask (amber)
|
|
1692
|
+
* stays until the next event because it needs the human's attention.
|
|
1693
|
+
*
|
|
1694
|
+
* Everything is best-effort: no session id, a missing route, or any fetch
|
|
1695
|
+
* failure simply means "render nothing". No @deepseek-ai value imports.
|
|
1696
|
+
*/
|
|
1697
|
+
const POLL_MS$1 = 2e3;
|
|
1698
|
+
const AUTO_HIDE_MS = 4e3;
|
|
1699
|
+
/** Localized tag for a manual terminal state; other kinds keep their short tag. */
|
|
1700
|
+
function manualTagKey(kind) {
|
|
1701
|
+
if (kind === "manual-approved") return "history.tag.manualApproved";
|
|
1702
|
+
if (kind === "manual-rejected") return "history.tag.manualRejected";
|
|
1703
|
+
if (kind === "manual-cancelled") return "history.tag.manualCancelled";
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1706
|
+
function NoticeStrip({ t, ...props }) {
|
|
1707
|
+
const sessionId = resolveSessionId(props);
|
|
695
1708
|
const [notice, setNotice] = (0, react.useState)(null);
|
|
696
1709
|
const sinceRef = (0, react.useRef)(0);
|
|
697
1710
|
const shownIdRef = (0, react.useRef)(0);
|
|
@@ -718,7 +1731,7 @@ window.__ModuleLoader__.load({
|
|
|
718
1731
|
setNotice(null);
|
|
719
1732
|
}, AUTO_HIDE_MS);
|
|
720
1733
|
}).catch(() => {});
|
|
721
|
-
}, POLL_MS);
|
|
1734
|
+
}, POLL_MS$1);
|
|
722
1735
|
};
|
|
723
1736
|
fetchEvents(sessionId, 0).then((events) => {
|
|
724
1737
|
if (!alive) return;
|
|
@@ -736,6 +1749,7 @@ window.__ModuleLoader__.load({
|
|
|
736
1749
|
}, [sessionId]);
|
|
737
1750
|
if (notice === null) return null;
|
|
738
1751
|
const style = presentation(notice.kind);
|
|
1752
|
+
const manualKey = manualTagKey(notice.kind);
|
|
739
1753
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
740
1754
|
role: "status",
|
|
741
1755
|
style: {
|
|
@@ -761,7 +1775,7 @@ window.__ModuleLoader__.load({
|
|
|
761
1775
|
color: style.color,
|
|
762
1776
|
fontWeight: 600
|
|
763
1777
|
},
|
|
764
|
-
children: style.tag
|
|
1778
|
+
children: manualKey === null ? style.tag : t(manualKey)
|
|
765
1779
|
}),
|
|
766
1780
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
767
1781
|
style: {
|
|
@@ -807,6 +1821,715 @@ window.__ModuleLoader__.load({
|
|
|
807
1821
|
});
|
|
808
1822
|
}
|
|
809
1823
|
//#endregion
|
|
1824
|
+
//#region src/client/history.tsx
|
|
1825
|
+
/**
|
|
1826
|
+
* Approval-history view — the `conversation.view` face of dsh-perm-gate,
|
|
1827
|
+
* following the session-scoped review-page pattern demonstrated by
|
|
1828
|
+
* dsh-approval-gate: a timeline of every gate decision in the current
|
|
1829
|
+
* conversation, newest first, refreshed by polling.
|
|
1830
|
+
*
|
|
1831
|
+
* Beyond the decision timeline this view owns the review data plane: a snapshot
|
|
1832
|
+
* inventory bar (with session/all clearing), per-event file chips, and a diff
|
|
1833
|
+
* overlay that shows the pre-change vs current content of one file and can send
|
|
1834
|
+
* a revert instruction back into the conversation.
|
|
1835
|
+
*
|
|
1836
|
+
* Data source: the host's `GET /api/dsh-perm-gate/events?sessionId=` feed (the
|
|
1837
|
+
* same JSONL the notice strip consumes) plus the diff / revert / snapshot
|
|
1838
|
+
* routes. Everything is best-effort: no session id or any fetch failure renders
|
|
1839
|
+
* the empty / error state. No @deepseek-ai value imports.
|
|
1840
|
+
*/
|
|
1841
|
+
const POLL_MS = 5e3;
|
|
1842
|
+
/** Tag text per event kind, keyed into the plugin dictionary. */
|
|
1843
|
+
const TAG_KEYS = {
|
|
1844
|
+
auto: "history.tag.auto",
|
|
1845
|
+
ask: "history.tag.ask",
|
|
1846
|
+
deny: "history.tag.deny",
|
|
1847
|
+
learned: "history.tag.learned",
|
|
1848
|
+
"manual-approved": "history.tag.manualApproved",
|
|
1849
|
+
"manual-rejected": "history.tag.manualRejected",
|
|
1850
|
+
"manual-cancelled": "history.tag.manualCancelled"
|
|
1851
|
+
};
|
|
1852
|
+
/** The file name alone (chips show the basename, like the reference page). */
|
|
1853
|
+
function basename(p) {
|
|
1854
|
+
const seg = p.split(/[\\/]/);
|
|
1855
|
+
return seg[seg.length - 1] ?? p;
|
|
1856
|
+
}
|
|
1857
|
+
function glyphOf(kind) {
|
|
1858
|
+
const style = presentation(kind);
|
|
1859
|
+
switch (kind) {
|
|
1860
|
+
case "deny":
|
|
1861
|
+
case "manual-rejected": return {
|
|
1862
|
+
char: "✕",
|
|
1863
|
+
color: style.color
|
|
1864
|
+
};
|
|
1865
|
+
case "ask": return {
|
|
1866
|
+
char: "◔",
|
|
1867
|
+
color: style.color
|
|
1868
|
+
};
|
|
1869
|
+
case "manual-cancelled": return {
|
|
1870
|
+
char: "—",
|
|
1871
|
+
color: style.color
|
|
1872
|
+
};
|
|
1873
|
+
default: return {
|
|
1874
|
+
char: "✓",
|
|
1875
|
+
color: style.color
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const barStyle = {
|
|
1880
|
+
display: "flex",
|
|
1881
|
+
alignItems: "center",
|
|
1882
|
+
gap: "8px",
|
|
1883
|
+
flexWrap: "wrap",
|
|
1884
|
+
padding: "6px 10px",
|
|
1885
|
+
borderRadius: "8px",
|
|
1886
|
+
background: "var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.05))",
|
|
1887
|
+
fontSize: "12px",
|
|
1888
|
+
lineHeight: "18px",
|
|
1889
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1890
|
+
};
|
|
1891
|
+
const barButtonStyle = {
|
|
1892
|
+
boxSizing: "border-box",
|
|
1893
|
+
height: "26px",
|
|
1894
|
+
color: "var(--dsw-alias-label-primary)",
|
|
1895
|
+
cursor: "pointer",
|
|
1896
|
+
font: "inherit",
|
|
1897
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1898
|
+
background: "transparent",
|
|
1899
|
+
borderRadius: "13px",
|
|
1900
|
+
padding: "0 12px",
|
|
1901
|
+
fontSize: "12px",
|
|
1902
|
+
lineHeight: "24px"
|
|
1903
|
+
};
|
|
1904
|
+
/** The snapshot inventory bar: usage plus session/all clearing. */
|
|
1905
|
+
function SnapshotBar(props) {
|
|
1906
|
+
const { t, stats, sessionId, onCleared } = props;
|
|
1907
|
+
const busy = stats === null || stats.count === 0;
|
|
1908
|
+
const clear = (mode) => {
|
|
1909
|
+
if (!window.confirm(mode === "all" ? t("history.clearAllConfirm") : t("history.clearSessionConfirm"))) return;
|
|
1910
|
+
clearSnapshots(mode === "session" ? sessionId : null).then(() => {
|
|
1911
|
+
onCleared();
|
|
1912
|
+
});
|
|
1913
|
+
};
|
|
1914
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1915
|
+
style: barStyle,
|
|
1916
|
+
children: [
|
|
1917
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [`${t("history.snapshots")} `, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
|
|
1918
|
+
style: {
|
|
1919
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1920
|
+
fontWeight: 500
|
|
1921
|
+
},
|
|
1922
|
+
children: stats === null ? "…" : `${fmtBytes(stats.bytes)} · ${stats.count}`
|
|
1923
|
+
})] }),
|
|
1924
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: "1 1 auto" } }),
|
|
1925
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1926
|
+
type: "button",
|
|
1927
|
+
title: t("history.clearSessionTitle"),
|
|
1928
|
+
disabled: busy,
|
|
1929
|
+
onClick: () => {
|
|
1930
|
+
clear("session");
|
|
1931
|
+
},
|
|
1932
|
+
style: {
|
|
1933
|
+
...barButtonStyle,
|
|
1934
|
+
opacity: busy ? .4 : 1
|
|
1935
|
+
},
|
|
1936
|
+
children: t("history.clearSession")
|
|
1937
|
+
}),
|
|
1938
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1939
|
+
type: "button",
|
|
1940
|
+
title: t("history.clearAllTitle"),
|
|
1941
|
+
disabled: busy,
|
|
1942
|
+
onClick: () => {
|
|
1943
|
+
clear("all");
|
|
1944
|
+
},
|
|
1945
|
+
style: {
|
|
1946
|
+
...barButtonStyle,
|
|
1947
|
+
opacity: busy ? .4 : 1,
|
|
1948
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
1949
|
+
},
|
|
1950
|
+
children: t("history.clearAll")
|
|
1951
|
+
})
|
|
1952
|
+
]
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
/** One diff line row (marker + line numbers + text). */
|
|
1956
|
+
function DiffRow({ line }) {
|
|
1957
|
+
const isAdd = line.type === "add";
|
|
1958
|
+
const isDel = line.type === "del";
|
|
1959
|
+
const background = isAdd ? "var(--dsw-alias-state-success-tertiary)" : isDel ? "var(--dsw-alias-interactive-bg-hover-danger)" : "transparent";
|
|
1960
|
+
const color = isDel ? "var(--dsw-alias-state-error-primary)" : isAdd ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary)";
|
|
1961
|
+
const aNo = line.aNo === void 0 ? "" : String(line.aNo);
|
|
1962
|
+
const bNo = line.bNo === void 0 ? "" : String(line.bNo);
|
|
1963
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1964
|
+
style: {
|
|
1965
|
+
boxSizing: "border-box",
|
|
1966
|
+
display: "flex",
|
|
1967
|
+
gap: "8px",
|
|
1968
|
+
padding: "1px 8px",
|
|
1969
|
+
whiteSpace: "pre-wrap",
|
|
1970
|
+
wordBreak: "break-all",
|
|
1971
|
+
minWidth: 0,
|
|
1972
|
+
background,
|
|
1973
|
+
color
|
|
1974
|
+
},
|
|
1975
|
+
children: [
|
|
1976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1977
|
+
"aria-hidden": true,
|
|
1978
|
+
style: {
|
|
1979
|
+
flex: "0 0 auto",
|
|
1980
|
+
width: "16px",
|
|
1981
|
+
userSelect: "none"
|
|
1982
|
+
},
|
|
1983
|
+
children: isAdd ? "+" : isDel ? "-" : " "
|
|
1984
|
+
}),
|
|
1985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1986
|
+
style: {
|
|
1987
|
+
flex: "0 0 auto",
|
|
1988
|
+
width: "60px",
|
|
1989
|
+
color: "var(--dsw-alias-label-caption)",
|
|
1990
|
+
textAlign: "right",
|
|
1991
|
+
userSelect: "none",
|
|
1992
|
+
fontVariantNumeric: "tabular-nums"
|
|
1993
|
+
},
|
|
1994
|
+
children: [
|
|
1995
|
+
aNo,
|
|
1996
|
+
aNo !== "" && bNo !== "" ? " " : "",
|
|
1997
|
+
bNo
|
|
1998
|
+
]
|
|
1999
|
+
}),
|
|
2000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2001
|
+
style: {
|
|
2002
|
+
flex: "1 1 auto",
|
|
2003
|
+
minWidth: 0
|
|
2004
|
+
},
|
|
2005
|
+
children: line.text
|
|
2006
|
+
})
|
|
2007
|
+
]
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
/** The file-change overlay: before/after diff plus the revert action. */
|
|
2011
|
+
function DiffPanel(props) {
|
|
2012
|
+
const { t, sessionId, eventId, path, onClose } = props;
|
|
2013
|
+
const [data, setData] = (0, react.useState)(null);
|
|
2014
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2015
|
+
const [revertMsg, setRevertMsg] = (0, react.useState)(null);
|
|
2016
|
+
const [revertOk, setRevertOk] = (0, react.useState)(false);
|
|
2017
|
+
const [reverting, setReverting] = (0, react.useState)(false);
|
|
2018
|
+
const [revertDone, setRevertDone] = (0, react.useState)(false);
|
|
2019
|
+
(0, react.useEffect)(() => {
|
|
2020
|
+
let alive = true;
|
|
2021
|
+
setData(null);
|
|
2022
|
+
setError(null);
|
|
2023
|
+
setRevertDone(false);
|
|
2024
|
+
setRevertMsg(null);
|
|
2025
|
+
fetchDiff(eventId, path).then((res) => {
|
|
2026
|
+
if (alive) setData(res);
|
|
2027
|
+
}).catch((e) => {
|
|
2028
|
+
if (alive) setError(`${t("history.diffLoadFail")}${String(e?.message ?? e)}`);
|
|
2029
|
+
});
|
|
2030
|
+
return () => {
|
|
2031
|
+
alive = false;
|
|
2032
|
+
};
|
|
2033
|
+
}, [
|
|
2034
|
+
eventId,
|
|
2035
|
+
path,
|
|
2036
|
+
t
|
|
2037
|
+
]);
|
|
2038
|
+
const doRevert = () => {
|
|
2039
|
+
if (reverting || revertDone || sessionId === null) return;
|
|
2040
|
+
setReverting(true);
|
|
2041
|
+
setRevertMsg(null);
|
|
2042
|
+
sendRevert(sessionId, eventId).then((res) => {
|
|
2043
|
+
if (res.ok) {
|
|
2044
|
+
setRevertMsg(t("history.diffRevertSent"));
|
|
2045
|
+
setRevertOk(true);
|
|
2046
|
+
setRevertDone(true);
|
|
2047
|
+
} else {
|
|
2048
|
+
setRevertMsg(`${t("history.diffRevertFail")}${res.error ?? ""}`);
|
|
2049
|
+
setRevertOk(false);
|
|
2050
|
+
}
|
|
2051
|
+
setReverting(false);
|
|
2052
|
+
});
|
|
2053
|
+
};
|
|
2054
|
+
const stats = data?.stats;
|
|
2055
|
+
const body = error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2056
|
+
style: diffEmptyStyle,
|
|
2057
|
+
children: error
|
|
2058
|
+
}) : data === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2059
|
+
style: diffEmptyStyle,
|
|
2060
|
+
children: t("history.diffLoading")
|
|
2061
|
+
}) : data.hunks.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2062
|
+
style: diffEmptyStyle,
|
|
2063
|
+
children: t("history.diffEmpty")
|
|
2064
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2065
|
+
style: diffBodyStyle,
|
|
2066
|
+
children: data.hunks.map((hunk, hi) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [hunk.hiddenBefore > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2067
|
+
style: hunkSepStyle,
|
|
2068
|
+
children: t("history.diffHidden").replace("%n", String(hunk.hiddenBefore))
|
|
2069
|
+
}) : null, hunk.lines.map((line, li) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffRow, { line }, `l-${hi}-${li}`))] }, `hunk-${hi}`))
|
|
2070
|
+
});
|
|
2071
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2072
|
+
style: overlayStyle,
|
|
2073
|
+
onClick: (e) => {
|
|
2074
|
+
if (e.target === e.currentTarget) onClose();
|
|
2075
|
+
},
|
|
2076
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2077
|
+
style: panelStyle,
|
|
2078
|
+
children: [
|
|
2079
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2080
|
+
style: panelHeadStyle,
|
|
2081
|
+
children: [
|
|
2082
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2083
|
+
style: {
|
|
2084
|
+
display: "flex",
|
|
2085
|
+
alignItems: "center",
|
|
2086
|
+
gap: "8px"
|
|
2087
|
+
},
|
|
2088
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2089
|
+
style: {
|
|
2090
|
+
flex: "1 1 auto",
|
|
2091
|
+
fontSize: "14px",
|
|
2092
|
+
fontWeight: 500,
|
|
2093
|
+
color: "var(--dsw-alias-label-primary)"
|
|
2094
|
+
},
|
|
2095
|
+
children: t("history.diffTitle")
|
|
2096
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2097
|
+
type: "button",
|
|
2098
|
+
"aria-label": t("history.diffClose"),
|
|
2099
|
+
title: t("history.diffClose"),
|
|
2100
|
+
onClick: onClose,
|
|
2101
|
+
style: closeStyle,
|
|
2102
|
+
children: "✕"
|
|
2103
|
+
})]
|
|
2104
|
+
}),
|
|
2105
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2106
|
+
style: {
|
|
2107
|
+
fontSize: "12px",
|
|
2108
|
+
lineHeight: "18px",
|
|
2109
|
+
fontFamily: "var(--ds-font-family-code, monospace)",
|
|
2110
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2111
|
+
wordBreak: "break-all"
|
|
2112
|
+
},
|
|
2113
|
+
children: path
|
|
2114
|
+
}),
|
|
2115
|
+
stats === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2116
|
+
style: {
|
|
2117
|
+
fontSize: "12px",
|
|
2118
|
+
lineHeight: "18px",
|
|
2119
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
2120
|
+
},
|
|
2121
|
+
children: [t("history.diffStats").replace("%a", String(stats.added)).replace("%r", String(stats.removed)).replace("%c", String(stats.contextLines)), data !== null && !data.afterExists ? t("history.diffGone") : ""]
|
|
2122
|
+
})
|
|
2123
|
+
]
|
|
2124
|
+
}),
|
|
2125
|
+
body,
|
|
2126
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2127
|
+
style: panelFootStyle,
|
|
2128
|
+
children: [
|
|
2129
|
+
revertMsg === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2130
|
+
style: {
|
|
2131
|
+
marginRight: "auto",
|
|
2132
|
+
fontSize: "12px",
|
|
2133
|
+
color: revertOk ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
|
|
2134
|
+
},
|
|
2135
|
+
children: revertMsg
|
|
2136
|
+
}),
|
|
2137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2138
|
+
type: "button",
|
|
2139
|
+
onClick: doRevert,
|
|
2140
|
+
disabled: reverting || revertDone || sessionId === null,
|
|
2141
|
+
style: {
|
|
2142
|
+
...barButtonStyle,
|
|
2143
|
+
border: "none",
|
|
2144
|
+
background: "var(--dsw-alias-button-primary-fill, var(--dsw-alias-label-primary))",
|
|
2145
|
+
color: "var(--dsw-alias-label-primary-foreground, #fff)",
|
|
2146
|
+
opacity: reverting || revertDone ? .5 : 1
|
|
2147
|
+
},
|
|
2148
|
+
children: reverting ? t("history.diffReverting") : revertDone ? t("history.diffRevertDone") : t("history.diffRevert")
|
|
2149
|
+
}),
|
|
2150
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2151
|
+
type: "button",
|
|
2152
|
+
onClick: onClose,
|
|
2153
|
+
style: barButtonStyle,
|
|
2154
|
+
children: t("history.diffClose")
|
|
2155
|
+
})
|
|
2156
|
+
]
|
|
2157
|
+
})
|
|
2158
|
+
]
|
|
2159
|
+
})
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
const overlayStyle = {
|
|
2163
|
+
position: "fixed",
|
|
2164
|
+
inset: 0,
|
|
2165
|
+
zIndex: 900,
|
|
2166
|
+
display: "flex",
|
|
2167
|
+
alignItems: "center",
|
|
2168
|
+
justifyContent: "center",
|
|
2169
|
+
background: "var(--dsw-alias-bg-mask-1, rgba(0,0,0,0.35))"
|
|
2170
|
+
};
|
|
2171
|
+
const panelStyle = {
|
|
2172
|
+
boxSizing: "border-box",
|
|
2173
|
+
width: "min(760px, calc(100vw - 48px))",
|
|
2174
|
+
maxHeight: "min(720px, calc(100vh - 48px))",
|
|
2175
|
+
display: "flex",
|
|
2176
|
+
flexDirection: "column",
|
|
2177
|
+
background: "var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1))",
|
|
2178
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
2179
|
+
borderRadius: "16px",
|
|
2180
|
+
overflow: "hidden"
|
|
2181
|
+
};
|
|
2182
|
+
const panelHeadStyle = {
|
|
2183
|
+
boxSizing: "border-box",
|
|
2184
|
+
flex: "0 0 auto",
|
|
2185
|
+
display: "flex",
|
|
2186
|
+
flexDirection: "column",
|
|
2187
|
+
gap: "4px",
|
|
2188
|
+
padding: "12px 14px",
|
|
2189
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2)"
|
|
2190
|
+
};
|
|
2191
|
+
const panelFootStyle = {
|
|
2192
|
+
boxSizing: "border-box",
|
|
2193
|
+
flex: "0 0 auto",
|
|
2194
|
+
display: "flex",
|
|
2195
|
+
alignItems: "center",
|
|
2196
|
+
gap: "8px",
|
|
2197
|
+
justifyContent: "flex-end",
|
|
2198
|
+
padding: "10px 14px",
|
|
2199
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)"
|
|
2200
|
+
};
|
|
2201
|
+
const diffBodyStyle = {
|
|
2202
|
+
flex: "1 1 auto",
|
|
2203
|
+
minHeight: 0,
|
|
2204
|
+
overflowY: "auto",
|
|
2205
|
+
padding: "8px 10px",
|
|
2206
|
+
display: "flex",
|
|
2207
|
+
flexDirection: "column",
|
|
2208
|
+
fontFamily: "var(--ds-font-family-code, monospace)",
|
|
2209
|
+
fontSize: "12px",
|
|
2210
|
+
lineHeight: "19px"
|
|
2211
|
+
};
|
|
2212
|
+
const diffEmptyStyle = {
|
|
2213
|
+
flex: "1 1 auto",
|
|
2214
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2215
|
+
fontSize: "13px",
|
|
2216
|
+
lineHeight: "20px",
|
|
2217
|
+
padding: "16px",
|
|
2218
|
+
textAlign: "center"
|
|
2219
|
+
};
|
|
2220
|
+
const hunkSepStyle = {
|
|
2221
|
+
boxSizing: "border-box",
|
|
2222
|
+
display: "flex",
|
|
2223
|
+
alignItems: "center",
|
|
2224
|
+
justifyContent: "center",
|
|
2225
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2226
|
+
fontSize: "11px",
|
|
2227
|
+
lineHeight: "16px",
|
|
2228
|
+
padding: "2px 8px",
|
|
2229
|
+
margin: "2px 0",
|
|
2230
|
+
borderTop: "1px solid var(--dsw-alias-border-l1)",
|
|
2231
|
+
borderBottom: "1px solid var(--dsw-alias-border-l1)",
|
|
2232
|
+
userSelect: "none"
|
|
2233
|
+
};
|
|
2234
|
+
const closeStyle = {
|
|
2235
|
+
flex: "0 0 auto",
|
|
2236
|
+
width: "24px",
|
|
2237
|
+
height: "24px",
|
|
2238
|
+
border: "none",
|
|
2239
|
+
borderRadius: "999px",
|
|
2240
|
+
background: "transparent",
|
|
2241
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2242
|
+
cursor: "pointer"
|
|
2243
|
+
};
|
|
2244
|
+
function HistoryView({ t, ...props }) {
|
|
2245
|
+
const sessionId = resolveSessionId(props);
|
|
2246
|
+
const [events, setEvents] = (0, react.useState)(null);
|
|
2247
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2248
|
+
const [stats, setStats] = (0, react.useState)(null);
|
|
2249
|
+
const [diffOpen, setDiffOpen] = (0, react.useState)(null);
|
|
2250
|
+
(0, react.useEffect)(() => {
|
|
2251
|
+
setEvents(null);
|
|
2252
|
+
setError(null);
|
|
2253
|
+
setStats(null);
|
|
2254
|
+
let alive = true;
|
|
2255
|
+
const load = () => {
|
|
2256
|
+
fetchSnapshotStats(sessionId).then((next) => {
|
|
2257
|
+
if (alive) setStats(next);
|
|
2258
|
+
});
|
|
2259
|
+
if (sessionId === null) {
|
|
2260
|
+
setEvents([]);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
fetchEvents(sessionId, 0).then((loaded) => {
|
|
2264
|
+
if (!alive) return;
|
|
2265
|
+
setEvents([...loaded].sort((a, b) => b.id - a.id));
|
|
2266
|
+
setError(null);
|
|
2267
|
+
}).catch((e) => {
|
|
2268
|
+
if (!alive) return;
|
|
2269
|
+
setError(String(e?.message ?? e));
|
|
2270
|
+
});
|
|
2271
|
+
};
|
|
2272
|
+
load();
|
|
2273
|
+
const timer = setInterval(load, POLL_MS);
|
|
2274
|
+
return () => {
|
|
2275
|
+
alive = false;
|
|
2276
|
+
clearInterval(timer);
|
|
2277
|
+
};
|
|
2278
|
+
}, [sessionId]);
|
|
2279
|
+
/** Whether one event has a snapshot covering this file (chip is clickable). */
|
|
2280
|
+
const hasSnapshot = (ev, file) => {
|
|
2281
|
+
const paths = stats?.files[String(ev.id)];
|
|
2282
|
+
if (paths === void 0 || paths.length === 0) return false;
|
|
2283
|
+
const base = basename(file);
|
|
2284
|
+
return paths.some((p) => p === file || basename(p) === base);
|
|
2285
|
+
};
|
|
2286
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2287
|
+
style: {
|
|
2288
|
+
boxSizing: "border-box",
|
|
2289
|
+
maxWidth: "720px",
|
|
2290
|
+
margin: "0 auto",
|
|
2291
|
+
padding: "8px 4px"
|
|
2292
|
+
},
|
|
2293
|
+
children: [
|
|
2294
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2295
|
+
style: { marginBottom: "10px" },
|
|
2296
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2297
|
+
style: {
|
|
2298
|
+
fontSize: "14px",
|
|
2299
|
+
fontWeight: 600,
|
|
2300
|
+
color: "var(--dsw-alias-label-primary)"
|
|
2301
|
+
},
|
|
2302
|
+
children: t("history.title")
|
|
2303
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2304
|
+
style: {
|
|
2305
|
+
marginTop: "2px",
|
|
2306
|
+
fontSize: "12px",
|
|
2307
|
+
lineHeight: "18px",
|
|
2308
|
+
color: "var(--dsw-alias-label-primary) !important"
|
|
2309
|
+
},
|
|
2310
|
+
children: t("history.subtitle")
|
|
2311
|
+
})]
|
|
2312
|
+
}),
|
|
2313
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapshotBar, {
|
|
2314
|
+
t,
|
|
2315
|
+
stats,
|
|
2316
|
+
sessionId,
|
|
2317
|
+
onCleared: () => {
|
|
2318
|
+
fetchSnapshotStats(sessionId).then(setStats);
|
|
2319
|
+
}
|
|
2320
|
+
}),
|
|
2321
|
+
events === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2322
|
+
style: {
|
|
2323
|
+
padding: "16px 0",
|
|
2324
|
+
fontSize: "13px",
|
|
2325
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2326
|
+
},
|
|
2327
|
+
children: error === null ? t("history.loading") : `${t("history.error")}${error}`
|
|
2328
|
+
}) : events.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2329
|
+
style: {
|
|
2330
|
+
padding: "16px 0",
|
|
2331
|
+
fontSize: "13px",
|
|
2332
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2333
|
+
},
|
|
2334
|
+
children: error === null ? t("history.empty") : `${t("history.error")}${error}`
|
|
2335
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: events.map((ev) => {
|
|
2336
|
+
const style = presentation(ev.kind);
|
|
2337
|
+
const mark = glyphOf(ev.kind);
|
|
2338
|
+
const verdict = ev.verdict === void 0 ? "" : VERDICT_LABELS[ev.verdict] ?? ev.verdict;
|
|
2339
|
+
const files = ev.files ?? [];
|
|
2340
|
+
const chips = [];
|
|
2341
|
+
const seenBase = /* @__PURE__ */ new Set();
|
|
2342
|
+
for (const f of files) {
|
|
2343
|
+
const base = basename(f);
|
|
2344
|
+
if (base === "" || seenBase.has(base)) continue;
|
|
2345
|
+
seenBase.add(base);
|
|
2346
|
+
chips.push(f);
|
|
2347
|
+
}
|
|
2348
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2349
|
+
style: {
|
|
2350
|
+
display: "flex",
|
|
2351
|
+
gap: "10px",
|
|
2352
|
+
padding: "6px 10px",
|
|
2353
|
+
borderRadius: "8px",
|
|
2354
|
+
background: "var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.05))"
|
|
2355
|
+
},
|
|
2356
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2357
|
+
style: {
|
|
2358
|
+
display: "flex",
|
|
2359
|
+
flexDirection: "column",
|
|
2360
|
+
alignItems: "center",
|
|
2361
|
+
flex: "0 0 auto",
|
|
2362
|
+
width: "20px"
|
|
2363
|
+
},
|
|
2364
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2365
|
+
"aria-hidden": true,
|
|
2366
|
+
style: {
|
|
2367
|
+
width: "18px",
|
|
2368
|
+
height: "18px",
|
|
2369
|
+
marginTop: "3px",
|
|
2370
|
+
display: "flex",
|
|
2371
|
+
alignItems: "center",
|
|
2372
|
+
justifyContent: "center",
|
|
2373
|
+
fontSize: "11px",
|
|
2374
|
+
lineHeight: "18px",
|
|
2375
|
+
borderRadius: "999px",
|
|
2376
|
+
border: `1px solid ${mark.color}`,
|
|
2377
|
+
color: mark.color
|
|
2378
|
+
},
|
|
2379
|
+
children: mark.char
|
|
2380
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2381
|
+
"aria-hidden": true,
|
|
2382
|
+
style: {
|
|
2383
|
+
flex: "1 1 auto",
|
|
2384
|
+
width: "1px",
|
|
2385
|
+
minHeight: "10px",
|
|
2386
|
+
background: "var(--dsw-alias-border-l2, rgba(127,127,127,0.3))"
|
|
2387
|
+
}
|
|
2388
|
+
})]
|
|
2389
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2390
|
+
style: {
|
|
2391
|
+
flex: "1 1 auto",
|
|
2392
|
+
minWidth: 0,
|
|
2393
|
+
paddingBottom: "12px"
|
|
2394
|
+
},
|
|
2395
|
+
children: [
|
|
2396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2397
|
+
style: {
|
|
2398
|
+
display: "flex",
|
|
2399
|
+
alignItems: "center",
|
|
2400
|
+
gap: "8px",
|
|
2401
|
+
flexWrap: "wrap"
|
|
2402
|
+
},
|
|
2403
|
+
children: [
|
|
2404
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2405
|
+
style: {
|
|
2406
|
+
font: "600 12px/18px var(--ds-font-family-code, monospace)",
|
|
2407
|
+
color: "var(--dsw-alias-label-primary)"
|
|
2408
|
+
},
|
|
2409
|
+
children: ev.tool
|
|
2410
|
+
}),
|
|
2411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2412
|
+
style: {
|
|
2413
|
+
fontSize: "11px",
|
|
2414
|
+
lineHeight: "18px",
|
|
2415
|
+
padding: "0 8px",
|
|
2416
|
+
borderRadius: "9px",
|
|
2417
|
+
color: style.color,
|
|
2418
|
+
background: style.bg,
|
|
2419
|
+
fontWeight: 600
|
|
2420
|
+
},
|
|
2421
|
+
children: t(TAG_KEYS[ev.kind])
|
|
2422
|
+
}),
|
|
2423
|
+
verdict === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2424
|
+
style: {
|
|
2425
|
+
fontSize: "11px",
|
|
2426
|
+
lineHeight: "18px",
|
|
2427
|
+
padding: "0 6px",
|
|
2428
|
+
borderRadius: "9px",
|
|
2429
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2430
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
2431
|
+
},
|
|
2432
|
+
children: verdict
|
|
2433
|
+
}),
|
|
2434
|
+
ev.risk === void 0 || ev.risk === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2435
|
+
style: {
|
|
2436
|
+
fontSize: "11px",
|
|
2437
|
+
lineHeight: "18px",
|
|
2438
|
+
padding: "0 6px",
|
|
2439
|
+
borderRadius: "9px",
|
|
2440
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
2441
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
2442
|
+
},
|
|
2443
|
+
children: ev.risk
|
|
2444
|
+
}),
|
|
2445
|
+
ev.kind === "manual-approved" && ev.learningCount !== void 0 && ev.threshold !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2446
|
+
style: {
|
|
2447
|
+
fontSize: "11px",
|
|
2448
|
+
lineHeight: "18px",
|
|
2449
|
+
padding: "0 6px",
|
|
2450
|
+
borderRadius: "9px",
|
|
2451
|
+
color: "var(--dsw-alias-state-warn-label)",
|
|
2452
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
2453
|
+
},
|
|
2454
|
+
children: t("history.learnedProgress").replace("%n", String(ev.learningCount)).replace("%t", String(ev.threshold))
|
|
2455
|
+
}) : null,
|
|
2456
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2457
|
+
style: {
|
|
2458
|
+
marginLeft: "auto",
|
|
2459
|
+
flex: "0 0 auto",
|
|
2460
|
+
fontSize: "11px",
|
|
2461
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
2462
|
+
},
|
|
2463
|
+
children: clockTime(ev.ts)
|
|
2464
|
+
})
|
|
2465
|
+
]
|
|
2466
|
+
}),
|
|
2467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2468
|
+
style: {
|
|
2469
|
+
marginTop: "2px",
|
|
2470
|
+
fontSize: "12px",
|
|
2471
|
+
lineHeight: "18px",
|
|
2472
|
+
color: "var(--dsw-alias-label-secondary, inherit)",
|
|
2473
|
+
overflowWrap: "anywhere"
|
|
2474
|
+
},
|
|
2475
|
+
children: ev.justification ?? ev.reason
|
|
2476
|
+
}),
|
|
2477
|
+
chips.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2478
|
+
style: {
|
|
2479
|
+
display: "flex",
|
|
2480
|
+
flexWrap: "wrap",
|
|
2481
|
+
gap: "4px",
|
|
2482
|
+
marginTop: "4px"
|
|
2483
|
+
},
|
|
2484
|
+
children: chips.map((file) => {
|
|
2485
|
+
const clickable = hasSnapshot(ev, file);
|
|
2486
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2487
|
+
title: clickable ? t("history.diffChipTitle") : file,
|
|
2488
|
+
role: clickable ? "button" : void 0,
|
|
2489
|
+
onClick: clickable ? () => {
|
|
2490
|
+
setDiffOpen({
|
|
2491
|
+
eventId: ev.id,
|
|
2492
|
+
path: file
|
|
2493
|
+
});
|
|
2494
|
+
} : void 0,
|
|
2495
|
+
style: {
|
|
2496
|
+
boxSizing: "border-box",
|
|
2497
|
+
maxWidth: "260px",
|
|
2498
|
+
height: "20px",
|
|
2499
|
+
display: "inline-flex",
|
|
2500
|
+
alignItems: "center",
|
|
2501
|
+
padding: "0 7px",
|
|
2502
|
+
borderRadius: "5px",
|
|
2503
|
+
font: "11px/18px var(--ds-font-family-code, monospace)",
|
|
2504
|
+
border: `1px solid ${clickable ? "var(--dsw-alias-state-business-primary)" : "var(--dsw-alias-border-l1)"}`,
|
|
2505
|
+
color: clickable ? "var(--dsw-alias-state-business-primary)" : "var(--dsw-alias-label-secondary)",
|
|
2506
|
+
background: "var(--dsw-alias-bg-layer-1, transparent)",
|
|
2507
|
+
cursor: clickable ? "pointer" : "default",
|
|
2508
|
+
overflow: "hidden",
|
|
2509
|
+
textOverflow: "ellipsis",
|
|
2510
|
+
whiteSpace: "nowrap"
|
|
2511
|
+
},
|
|
2512
|
+
children: basename(file)
|
|
2513
|
+
}, file);
|
|
2514
|
+
})
|
|
2515
|
+
})
|
|
2516
|
+
]
|
|
2517
|
+
})]
|
|
2518
|
+
}, ev.id);
|
|
2519
|
+
}) }),
|
|
2520
|
+
diffOpen === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffPanel, {
|
|
2521
|
+
t,
|
|
2522
|
+
sessionId,
|
|
2523
|
+
eventId: diffOpen.eventId,
|
|
2524
|
+
path: diffOpen.path,
|
|
2525
|
+
onClose: () => {
|
|
2526
|
+
setDiffOpen(null);
|
|
2527
|
+
}
|
|
2528
|
+
})
|
|
2529
|
+
]
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
//#endregion
|
|
810
2533
|
//#region src/client/index.ts
|
|
811
2534
|
/** The settings namespace the host half registers (kept in lockstep with src/index.ts). */
|
|
812
2535
|
const PERMISSIVE_NS = "dsh-perm-gate";
|
|
@@ -835,6 +2558,16 @@ window.__ModuleLoader__.load({
|
|
|
835
2558
|
locale: NS
|
|
836
2559
|
}, NoticeStrip);
|
|
837
2560
|
});
|
|
2561
|
+
ctx.slots.inject("conversation.view", function* () {
|
|
2562
|
+
yield ctx.slots.register({
|
|
2563
|
+
name: "conversation.view",
|
|
2564
|
+
id: "dsh-perm-gate.history",
|
|
2565
|
+
order: 20,
|
|
2566
|
+
label: () => t("history.label"),
|
|
2567
|
+
locale: NS,
|
|
2568
|
+
inject: (sessionId) => ({ sessionId })
|
|
2569
|
+
}, HistoryView);
|
|
2570
|
+
});
|
|
838
2571
|
const t = ctx.locale.bind(NS);
|
|
839
2572
|
ctx.slots.inject("settings.plugins.tab", function* () {
|
|
840
2573
|
yield ctx.slots.register({
|