dsh-perm-gate 0.2.0
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 +62 -0
- package/CHANGELOG.ko.md +60 -0
- package/CHANGELOG.md +62 -0
- package/INSTALL.ja.md +152 -0
- package/INSTALL.ko.md +151 -0
- package/INSTALL.md +154 -0
- package/INSTALL.zh.md +147 -0
- package/LICENSE +21 -0
- package/README.ja.md +197 -0
- package/README.ko.md +195 -0
- package/README.md +209 -0
- package/README.zh.md +144 -0
- package/assets/.gitkeep +0 -0
- package/assets/README.md +5 -0
- package/cordis.patch.yml +35 -0
- package/examples/permissions.example.yaml +20 -0
- package/lib/allowlist.d.ts +19 -0
- package/lib/allowlist.js +97 -0
- package/lib/audit.d.ts +45 -0
- package/lib/audit.js +34 -0
- package/lib/classifier.d.ts +74 -0
- package/lib/classifier.js +129 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +67 -0
- package/lib/client.js +859 -0
- package/lib/client.js.map +1 -0
- package/lib/compiler.d.ts +30 -0
- package/lib/compiler.js +126 -0
- package/lib/config.d.ts +69 -0
- package/lib/config.js +64 -0
- package/lib/engine.d.ts +17 -0
- package/lib/engine.js +85 -0
- package/lib/evaluate.d.ts +20 -0
- package/lib/evaluate.js +94 -0
- package/lib/events.d.ts +56 -0
- package/lib/events.js +129 -0
- package/lib/grant.d.ts +60 -0
- package/lib/grant.js +121 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +140 -0
- package/lib/learning.d.ts +58 -0
- package/lib/learning.js +181 -0
- package/lib/path.d.ts +47 -0
- package/lib/path.js +94 -0
- package/lib/risk.d.ts +58 -0
- package/lib/risk.js +100 -0
- package/lib/rule.d.ts +64 -0
- package/lib/rule.js +220 -0
- package/lib/runtime.d.ts +146 -0
- package/lib/runtime.js +350 -0
- package/lib/shell.d.ts +38 -0
- package/lib/shell.js +176 -0
- package/package.json +120 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-perm-gate",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/locales.ts
|
|
10
|
+
/** `dsh-perm-gate` client dictionaries (zh / en / ja / ko). */
|
|
11
|
+
/** Dictionary namespace owned by this plugin. */
|
|
12
|
+
const NS = "dsh-perm-gate";
|
|
13
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
14
|
+
const zh = {
|
|
15
|
+
"card.title": "Permissive 审批档",
|
|
16
|
+
"card.description": "独立审批模式档位:与只读 / 完全权限 / 白名单平行。前端只暴露一个开关;后台三个审批策略可组合、由此处设置决定,仍对 P0 硬拒绝保持 fail-closed。",
|
|
17
|
+
"card.permissive": "启用 Permissive 档位",
|
|
18
|
+
"card.permissiveHint": "关闭时门禁行为与此前完全一致。",
|
|
19
|
+
"card.strategies": "后台审批策略(至少一个生效;可组合)",
|
|
20
|
+
"card.strategy.trustAutoAllow": "trustAutoAllow — 作用域内安全操作自动放行,危险/未知转 ask(中间档基线)",
|
|
21
|
+
"card.strategy.alwaysConfirm": "alwaysConfirm — 一律逐次 ask;审批面板「允许控件」含两个扩展按钮:「本会话重复允许该类」(会话语限次 grant)与「允许所有类型」(命令词持久迁入 rulesFile 白名单)",
|
|
22
|
+
"card.strategy.llmAssist": "llmAssist — 自定义 LLM 按风险分级裁决:safe 自动放行;deletion/credential/remote/system/bulk 硬风险永远转人工;neutral 进入确认学习;失败/超时回退人工",
|
|
23
|
+
"card.readonly": "只读",
|
|
24
|
+
"card.unavailable": "设置命名空间不可用:请确认 dsh-perm-gate 已装配进 profile。",
|
|
25
|
+
"card.llmReceiver": "llmAssist 接收 LLM(OpenAI 兼容,可用自定义 API)",
|
|
26
|
+
"card.llmEndpoint": "Endpoint(BaseURL,如 https://api.openai.com/v1)",
|
|
27
|
+
"card.llmModel": "Model(模型 id,如 deepseek-chat)",
|
|
28
|
+
"card.llmKey": "API Key(保密)",
|
|
29
|
+
"card.llmKeyHidden": "未设置 · 首次输入后以 * 显示",
|
|
30
|
+
"card.llmKeyMasked": "已设置 · 输入新值覆盖",
|
|
31
|
+
"card.llmKeyOverwrite": "已设置保密值,输入新值并失焦即可覆盖。",
|
|
32
|
+
"card.llmKeyClear": "清除",
|
|
33
|
+
"card.allowlist": "白名单(allow,rulesFile 同步)",
|
|
34
|
+
"card.allowlistHint": "一行一个命令模式,换行即一个元素;保存后写入 rulesFile 的 allow 段并重载(Ctrl/Cmd+Enter 保存)。",
|
|
35
|
+
"card.allowlistSave": "保存白名单",
|
|
36
|
+
"card.riskLearning": "riskLearning — 裁决学习:neutral 风险操作经人工确认并执行后计数,满阈值且操作指纹一致时自动放行",
|
|
37
|
+
"card.riskLearningHint": "学习状态为插件自有数据($DSH_HOME/perm-gate/learning.json),不写入你的 YAML 规则文件;不同目标永不复用放行。默认关闭。",
|
|
38
|
+
"card.riskThreshold": "自动放行所需人工确认次数(1–10)",
|
|
39
|
+
"notice.label": "权限门禁动态"
|
|
40
|
+
};
|
|
41
|
+
/** English dictionary (keys mirror zh). */
|
|
42
|
+
const en = {
|
|
43
|
+
"card.title": "Permissive approval tier",
|
|
44
|
+
"card.description": "Independent approval mode tier, parallel to read-only / full-access / whitelist. The front-end exposes a single switch; the three backend approval strategies are combinable via this panel and still fail-closed against P0 hard-deny.",
|
|
45
|
+
"card.permissive": "Enable Permissive tier",
|
|
46
|
+
"card.permissiveHint": "When off, the gate behaves exactly as before.",
|
|
47
|
+
"card.strategies": "Backend approval strategies (at least one effective; combinable)",
|
|
48
|
+
"card.strategy.trustAutoAllow": "trustAutoAllow — safe in-scope ops auto-allow; dangerous/unknown ask (baseline middle tier)",
|
|
49
|
+
"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 always ask; neutral enters confirm-learning; failure/timeout falls back to the human",
|
|
51
|
+
"card.readonly": "Read-only",
|
|
52
|
+
"card.unavailable": "Settings namespace unavailable: make sure dsh-perm-gate is assembled into this profile.",
|
|
53
|
+
"card.llmReceiver": "llmAssist receiving LLM (OpenAI-compatible; any custom API)",
|
|
54
|
+
"card.llmEndpoint": "Endpoint (BaseURL, e.g. https://api.openai.com/v1)",
|
|
55
|
+
"card.llmModel": "Model (model id, e.g. deepseek-chat)",
|
|
56
|
+
"card.llmKey": "API Key (secret)",
|
|
57
|
+
"card.llmKeyHidden": "Unset · shown as * after first entry",
|
|
58
|
+
"card.llmKeyMasked": "Set · type a new value to overwrite",
|
|
59
|
+
"card.llmKeyOverwrite": "A secret is stored; type a new value and blur to overwrite it.",
|
|
60
|
+
"card.llmKeyClear": "Clear",
|
|
61
|
+
"card.allowlist": "Whitelist (allow, synced to rulesFile)",
|
|
62
|
+
"card.allowlistHint": "One command pattern per line (a newline is one element). Saving writes the rulesFile allow section and reloads (Ctrl/Cmd+Enter to save).",
|
|
63
|
+
"card.allowlistSave": "Save whitelist",
|
|
64
|
+
"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
|
+
"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
|
+
"card.riskThreshold": "Human confirmations required before auto-allow (1–10)",
|
|
67
|
+
"notice.label": "Permission gate activity"
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/client/card.tsx
|
|
71
|
+
/**
|
|
72
|
+
* Permissive settings card — the `settings.plugin.item` face of dsh-perm-gate.
|
|
73
|
+
*
|
|
74
|
+
* The card binds the `dsh-perm-gate` settings namespace through the
|
|
75
|
+
* `settingsScope` cordis service and renders its fields: the single front switch
|
|
76
|
+
* (`permissive`) plus the three combinable backend strategies
|
|
77
|
+
* (`trustAutoAllow` / `alwaysConfirm` / `llmAssist`). Every change commits
|
|
78
|
+
* immediately through the scope (no staged form); the host reads the namespace
|
|
79
|
+
* live, so a committed change applies to the next tool call without a restart.
|
|
80
|
+
*
|
|
81
|
+
* Kept dependency-free beyond react: the scope is subscribed with
|
|
82
|
+
* `useSyncExternalStore`, and the controls are plain HTML.
|
|
83
|
+
*/
|
|
84
|
+
const rowStyle = {
|
|
85
|
+
display: "flex",
|
|
86
|
+
alignItems: "center",
|
|
87
|
+
justifyContent: "space-between",
|
|
88
|
+
gap: "12px",
|
|
89
|
+
padding: "6px 0",
|
|
90
|
+
fontSize: "13px",
|
|
91
|
+
lineHeight: "20px"
|
|
92
|
+
};
|
|
93
|
+
const labelStyle = {
|
|
94
|
+
margin: 0,
|
|
95
|
+
color: "var(--dsw-alias-label-primary)"
|
|
96
|
+
};
|
|
97
|
+
const hintStyle = {
|
|
98
|
+
margin: "4px 0 0",
|
|
99
|
+
fontSize: "12px",
|
|
100
|
+
lineHeight: "18px",
|
|
101
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
102
|
+
};
|
|
103
|
+
const sectionStyle = {
|
|
104
|
+
marginTop: "12px",
|
|
105
|
+
paddingTop: "10px",
|
|
106
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)"
|
|
107
|
+
};
|
|
108
|
+
const controlStyle = {
|
|
109
|
+
background: "var(--dsw-alias-bg-surface, #fff)",
|
|
110
|
+
color: "var(--dsw-alias-label-primary)",
|
|
111
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
112
|
+
borderRadius: "4px",
|
|
113
|
+
padding: "3px 8px",
|
|
114
|
+
fontSize: "13px"
|
|
115
|
+
};
|
|
116
|
+
const fieldStyle = {
|
|
117
|
+
display: "flex",
|
|
118
|
+
flexDirection: "column",
|
|
119
|
+
gap: "4px"
|
|
120
|
+
};
|
|
121
|
+
const fieldLabelStyle = {
|
|
122
|
+
margin: 0,
|
|
123
|
+
fontSize: "12px",
|
|
124
|
+
lineHeight: "18px",
|
|
125
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* The card body, wrapped in a disclosure shell like every peer settings card:
|
|
129
|
+
* a header (name + description + chevron) toggling the body, collapsed by
|
|
130
|
+
* default so the plugin tab stays a tidy list of drawers.
|
|
131
|
+
*/
|
|
132
|
+
function PermissiveCard({ t, scope }) {
|
|
133
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
134
|
+
const [apiKeyDraft, setApiKeyDraft] = (0, react.useState)("");
|
|
135
|
+
const [allowText, setAllowText] = (0, react.useState)("");
|
|
136
|
+
const [allowDirty, setAllowDirty] = (0, react.useState)(false);
|
|
137
|
+
const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
138
|
+
const unavailable = snapshot.status === "unavailable";
|
|
139
|
+
const readonly = unavailable || !snapshot.writable;
|
|
140
|
+
const value = snapshot.value ?? {};
|
|
141
|
+
const strategies = value.permissiveStrategies ?? {};
|
|
142
|
+
const hasApiKey = typeof value.classifierApiKey === "string" && value.classifierApiKey !== "";
|
|
143
|
+
const effective = value.permissive === true;
|
|
144
|
+
const commitApiKey = () => {
|
|
145
|
+
const next = apiKeyDraft.trim();
|
|
146
|
+
if (next === "") return;
|
|
147
|
+
scope.set("classifierApiKey", next);
|
|
148
|
+
setApiKeyDraft("");
|
|
149
|
+
};
|
|
150
|
+
const clearApiKey = () => {
|
|
151
|
+
scope.unset("classifierApiKey");
|
|
152
|
+
setApiKeyDraft("");
|
|
153
|
+
};
|
|
154
|
+
const allowlistShown = allowDirty ? allowText : (value.allowlist ?? []).join("\n");
|
|
155
|
+
const commitAllowlist = () => {
|
|
156
|
+
const lines = allowlistShown.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
157
|
+
scope.set("allowlist", lines);
|
|
158
|
+
setAllowDirty(false);
|
|
159
|
+
};
|
|
160
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
161
|
+
style: {
|
|
162
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))",
|
|
163
|
+
background: "var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.05))",
|
|
164
|
+
borderRadius: "12px",
|
|
165
|
+
transition: "border-color 0.16s, background 0.16s"
|
|
166
|
+
},
|
|
167
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
168
|
+
type: "button",
|
|
169
|
+
"aria-expanded": open,
|
|
170
|
+
style: {
|
|
171
|
+
appearance: "none",
|
|
172
|
+
width: "100%",
|
|
173
|
+
font: "inherit",
|
|
174
|
+
color: "inherit",
|
|
175
|
+
textAlign: "left",
|
|
176
|
+
cursor: "pointer",
|
|
177
|
+
background: "none",
|
|
178
|
+
border: 0,
|
|
179
|
+
borderRadius: "12px",
|
|
180
|
+
display: "flex",
|
|
181
|
+
alignItems: "center",
|
|
182
|
+
gap: "12px",
|
|
183
|
+
padding: "14px 16px"
|
|
184
|
+
},
|
|
185
|
+
onClick: () => {
|
|
186
|
+
setOpen((current) => !current);
|
|
187
|
+
},
|
|
188
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
189
|
+
style: {
|
|
190
|
+
flex: "1 1 0%",
|
|
191
|
+
minWidth: 0
|
|
192
|
+
},
|
|
193
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
194
|
+
style: {
|
|
195
|
+
fontSize: "14px",
|
|
196
|
+
fontWeight: 600,
|
|
197
|
+
color: "var(--dsw-alias-label-primary)"
|
|
198
|
+
},
|
|
199
|
+
children: t("card.title")
|
|
200
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
201
|
+
style: {
|
|
202
|
+
color: "var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))",
|
|
203
|
+
fontSize: "13px",
|
|
204
|
+
lineHeight: 1.5
|
|
205
|
+
},
|
|
206
|
+
children: t("card.description")
|
|
207
|
+
})]
|
|
208
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
209
|
+
width: "16",
|
|
210
|
+
height: "16",
|
|
211
|
+
viewBox: "0 0 16 16",
|
|
212
|
+
"aria-hidden": true,
|
|
213
|
+
style: {
|
|
214
|
+
color: "var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))",
|
|
215
|
+
flex: "0 0 auto",
|
|
216
|
+
transition: "transform 0.16s",
|
|
217
|
+
transform: open ? "rotate(180deg)" : "none"
|
|
218
|
+
},
|
|
219
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
220
|
+
d: "M4 6l4 4 4-4",
|
|
221
|
+
fill: "none",
|
|
222
|
+
stroke: "currentColor",
|
|
223
|
+
strokeWidth: "1.5",
|
|
224
|
+
strokeLinecap: "round",
|
|
225
|
+
strokeLinejoin: "round"
|
|
226
|
+
})
|
|
227
|
+
})]
|
|
228
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
229
|
+
style: { padding: "12px 16px" },
|
|
230
|
+
children: unavailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
231
|
+
style: {
|
|
232
|
+
fontSize: "13px",
|
|
233
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
234
|
+
},
|
|
235
|
+
children: t("card.unavailable")
|
|
236
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
237
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
238
|
+
style: rowStyle,
|
|
239
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
240
|
+
htmlFor: "plugin-config-perm-gate-permissive",
|
|
241
|
+
style: labelStyle,
|
|
242
|
+
children: t("card.permissive")
|
|
243
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
244
|
+
id: "plugin-config-perm-gate-permissive",
|
|
245
|
+
type: "checkbox",
|
|
246
|
+
checked: effective,
|
|
247
|
+
disabled: readonly,
|
|
248
|
+
onChange: (event) => {
|
|
249
|
+
scope.set("permissive", event.currentTarget.checked);
|
|
250
|
+
}
|
|
251
|
+
})]
|
|
252
|
+
}),
|
|
253
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
254
|
+
style: hintStyle,
|
|
255
|
+
children: t("card.permissiveHint")
|
|
256
|
+
}),
|
|
257
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
258
|
+
style: sectionStyle,
|
|
259
|
+
children: [
|
|
260
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
261
|
+
style: labelStyle,
|
|
262
|
+
children: t("card.strategies")
|
|
263
|
+
}),
|
|
264
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
265
|
+
style: rowStyle,
|
|
266
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
267
|
+
htmlFor: "plugin-config-perm-gate-trust",
|
|
268
|
+
style: { fontSize: "12px" },
|
|
269
|
+
children: t("card.strategy.trustAutoAllow")
|
|
270
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
271
|
+
id: "plugin-config-perm-gate-trust",
|
|
272
|
+
type: "checkbox",
|
|
273
|
+
checked: strategies.trustAutoAllow ?? true,
|
|
274
|
+
disabled: readonly || !effective,
|
|
275
|
+
onChange: (event) => {
|
|
276
|
+
scope.set("permissiveStrategies", {
|
|
277
|
+
...strategies,
|
|
278
|
+
trustAutoAllow: event.currentTarget.checked
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
})]
|
|
282
|
+
}),
|
|
283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
284
|
+
style: rowStyle,
|
|
285
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
286
|
+
htmlFor: "plugin-config-perm-gate-confirm",
|
|
287
|
+
style: { fontSize: "12px" },
|
|
288
|
+
children: t("card.strategy.alwaysConfirm")
|
|
289
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
290
|
+
id: "plugin-config-perm-gate-confirm",
|
|
291
|
+
type: "checkbox",
|
|
292
|
+
checked: strategies.alwaysConfirm ?? false,
|
|
293
|
+
disabled: readonly || !effective,
|
|
294
|
+
onChange: (event) => {
|
|
295
|
+
scope.set("permissiveStrategies", {
|
|
296
|
+
...strategies,
|
|
297
|
+
alwaysConfirm: event.currentTarget.checked
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
})]
|
|
301
|
+
}),
|
|
302
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
303
|
+
style: rowStyle,
|
|
304
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
305
|
+
htmlFor: "plugin-config-perm-gate-llm",
|
|
306
|
+
style: { fontSize: "12px" },
|
|
307
|
+
children: t("card.strategy.llmAssist")
|
|
308
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
309
|
+
id: "plugin-config-perm-gate-llm",
|
|
310
|
+
type: "checkbox",
|
|
311
|
+
checked: strategies.llmAssist ?? false,
|
|
312
|
+
disabled: readonly || !effective,
|
|
313
|
+
onChange: (event) => {
|
|
314
|
+
scope.set("permissiveStrategies", {
|
|
315
|
+
...strategies,
|
|
316
|
+
llmAssist: event.currentTarget.checked
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
})]
|
|
320
|
+
}),
|
|
321
|
+
strategies.llmAssist === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
322
|
+
style: sectionStyle,
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
325
|
+
style: labelStyle,
|
|
326
|
+
children: t("card.llmReceiver")
|
|
327
|
+
}),
|
|
328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
329
|
+
style: fieldStyle,
|
|
330
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
331
|
+
style: fieldLabelStyle,
|
|
332
|
+
children: t("card.llmEndpoint")
|
|
333
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
334
|
+
type: "text",
|
|
335
|
+
value: value.classifierEndpoint ?? "",
|
|
336
|
+
disabled: readonly,
|
|
337
|
+
placeholder: "https://api.openai.com/v1",
|
|
338
|
+
style: controlStyle,
|
|
339
|
+
onChange: (event) => {
|
|
340
|
+
scope.set("classifierEndpoint", event.currentTarget.value);
|
|
341
|
+
},
|
|
342
|
+
onBlur: (event) => {
|
|
343
|
+
if (event.currentTarget.value.trim() === "") scope.unset("classifierEndpoint");
|
|
344
|
+
}
|
|
345
|
+
})]
|
|
346
|
+
}),
|
|
347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
348
|
+
style: fieldStyle,
|
|
349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
350
|
+
style: fieldLabelStyle,
|
|
351
|
+
children: t("card.llmModel")
|
|
352
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
353
|
+
type: "text",
|
|
354
|
+
value: value.classifierModel ?? "",
|
|
355
|
+
disabled: readonly,
|
|
356
|
+
placeholder: "deepseek-chat",
|
|
357
|
+
style: controlStyle,
|
|
358
|
+
onChange: (event) => {
|
|
359
|
+
scope.set("classifierModel", event.currentTarget.value);
|
|
360
|
+
},
|
|
361
|
+
onBlur: (event) => {
|
|
362
|
+
if (event.currentTarget.value.trim() === "") scope.unset("classifierModel");
|
|
363
|
+
}
|
|
364
|
+
})]
|
|
365
|
+
}),
|
|
366
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
367
|
+
style: fieldStyle,
|
|
368
|
+
children: [
|
|
369
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
370
|
+
style: fieldLabelStyle,
|
|
371
|
+
children: t("card.llmKey")
|
|
372
|
+
}),
|
|
373
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
374
|
+
style: {
|
|
375
|
+
display: "flex",
|
|
376
|
+
gap: "6px"
|
|
377
|
+
},
|
|
378
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
379
|
+
type: "password",
|
|
380
|
+
autoComplete: "off",
|
|
381
|
+
spellCheck: false,
|
|
382
|
+
value: apiKeyDraft,
|
|
383
|
+
disabled: readonly,
|
|
384
|
+
placeholder: hasApiKey ? t("card.llmKeyMasked") : t("card.llmKeyHidden"),
|
|
385
|
+
style: {
|
|
386
|
+
...controlStyle,
|
|
387
|
+
flex: "1 1 0%"
|
|
388
|
+
},
|
|
389
|
+
onChange: (event) => setApiKeyDraft(event.currentTarget.value),
|
|
390
|
+
onBlur: commitApiKey,
|
|
391
|
+
onKeyDown: (event) => {
|
|
392
|
+
if (event.key === "Enter") event.currentTarget.blur();
|
|
393
|
+
}
|
|
394
|
+
}), hasApiKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
395
|
+
type: "button",
|
|
396
|
+
disabled: readonly,
|
|
397
|
+
onClick: clearApiKey,
|
|
398
|
+
style: controlStyle,
|
|
399
|
+
children: t("card.llmKeyClear")
|
|
400
|
+
}) : null]
|
|
401
|
+
}),
|
|
402
|
+
hasApiKey && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
403
|
+
style: hintStyle,
|
|
404
|
+
children: t("card.llmKeyOverwrite")
|
|
405
|
+
})
|
|
406
|
+
]
|
|
407
|
+
}),
|
|
408
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
409
|
+
style: rowStyle,
|
|
410
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
411
|
+
htmlFor: "plugin-config-perm-gate-risk-learning",
|
|
412
|
+
style: { fontSize: "12px" },
|
|
413
|
+
children: t("card.riskLearning")
|
|
414
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
415
|
+
id: "plugin-config-perm-gate-risk-learning",
|
|
416
|
+
type: "checkbox",
|
|
417
|
+
checked: value.riskLearning ?? false,
|
|
418
|
+
disabled: readonly || !effective,
|
|
419
|
+
onChange: (event) => {
|
|
420
|
+
scope.set("riskLearning", event.currentTarget.checked);
|
|
421
|
+
}
|
|
422
|
+
})]
|
|
423
|
+
}),
|
|
424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
425
|
+
style: hintStyle,
|
|
426
|
+
children: t("card.riskLearningHint")
|
|
427
|
+
}),
|
|
428
|
+
value.riskLearning === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
429
|
+
style: fieldStyle,
|
|
430
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
431
|
+
style: fieldLabelStyle,
|
|
432
|
+
children: t("card.riskThreshold")
|
|
433
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
434
|
+
id: "plugin-config-perm-gate-risk-threshold",
|
|
435
|
+
type: "number",
|
|
436
|
+
min: 1,
|
|
437
|
+
max: 10,
|
|
438
|
+
value: value.riskThreshold ?? 3,
|
|
439
|
+
disabled: readonly || !effective,
|
|
440
|
+
style: {
|
|
441
|
+
...controlStyle,
|
|
442
|
+
width: "96px"
|
|
443
|
+
},
|
|
444
|
+
onChange: (event) => {
|
|
445
|
+
const n = Number.parseInt(event.currentTarget.value, 10);
|
|
446
|
+
if (Number.isFinite(n) && n >= 1 && n <= 10) scope.set("riskThreshold", n);
|
|
447
|
+
}
|
|
448
|
+
})]
|
|
449
|
+
}) : null
|
|
450
|
+
]
|
|
451
|
+
}) : null
|
|
452
|
+
]
|
|
453
|
+
}),
|
|
454
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
455
|
+
style: sectionStyle,
|
|
456
|
+
children: [
|
|
457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
458
|
+
style: labelStyle,
|
|
459
|
+
children: t("card.allowlist")
|
|
460
|
+
}),
|
|
461
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
462
|
+
style: hintStyle,
|
|
463
|
+
children: t("card.allowlistHint")
|
|
464
|
+
}),
|
|
465
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
466
|
+
id: "plugin-config-perm-gate-allowlist",
|
|
467
|
+
value: allowlistShown,
|
|
468
|
+
disabled: readonly,
|
|
469
|
+
rows: 6,
|
|
470
|
+
spellCheck: false,
|
|
471
|
+
style: {
|
|
472
|
+
...controlStyle,
|
|
473
|
+
width: "100%",
|
|
474
|
+
boxSizing: "border-box",
|
|
475
|
+
resize: "vertical",
|
|
476
|
+
fontFamily: "var(--ds-font-family-code, monospace)",
|
|
477
|
+
whiteSpace: "pre"
|
|
478
|
+
},
|
|
479
|
+
onChange: (event) => {
|
|
480
|
+
setAllowDirty(true);
|
|
481
|
+
setAllowText(event.currentTarget.value);
|
|
482
|
+
},
|
|
483
|
+
onKeyDown: (event) => {
|
|
484
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) commitAllowlist();
|
|
485
|
+
}
|
|
486
|
+
}),
|
|
487
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
488
|
+
style: {
|
|
489
|
+
display: "flex",
|
|
490
|
+
justifyContent: "flex-end",
|
|
491
|
+
marginTop: "6px"
|
|
492
|
+
},
|
|
493
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
494
|
+
type: "button",
|
|
495
|
+
disabled: readonly || !allowDirty,
|
|
496
|
+
onClick: commitAllowlist,
|
|
497
|
+
style: {
|
|
498
|
+
...controlStyle,
|
|
499
|
+
cursor: readonly || !allowDirty ? "default" : "pointer",
|
|
500
|
+
opacity: readonly || !allowDirty ? .5 : 1
|
|
501
|
+
},
|
|
502
|
+
children: t("card.allowlistSave")
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
]
|
|
506
|
+
}),
|
|
507
|
+
!snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
508
|
+
style: {
|
|
509
|
+
margin: "8px 0 0",
|
|
510
|
+
fontSize: "12px",
|
|
511
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
512
|
+
},
|
|
513
|
+
children: t("card.readonly")
|
|
514
|
+
})
|
|
515
|
+
] })
|
|
516
|
+
}) : null]
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/client/permission-icon.ts
|
|
521
|
+
/**
|
|
522
|
+
* Permission-picker icon decoration for the Permissive tier.
|
|
523
|
+
*
|
|
524
|
+
* dsh-auto-mode injects a shield glyph onto its "Auto" permission menu item
|
|
525
|
+
* via a `data-*` attribute + a CSS mask. This module does the same for the
|
|
526
|
+
* "Permissive" preset so the two approval tiers look consistent in the
|
|
527
|
+
* dropdown. It only annotates the item; the preset itself comes from
|
|
528
|
+
* `cordis.patch.yml` (the `permission.config.presets` table).
|
|
529
|
+
*
|
|
530
|
+
* Pure DOM + CSS, no @deepseek-ai value imports (client bundle purity).
|
|
531
|
+
*/
|
|
532
|
+
const PLUGIN_ID = "dsh-perm-gate";
|
|
533
|
+
const ICON_ATTRIBUTE = "data-dsh-perm-gate-icon";
|
|
534
|
+
/** Labels of the Permissive preset item, current + future localized variants. */
|
|
535
|
+
const PERMISSIVE_LABELS = /* @__PURE__ */ new Set([
|
|
536
|
+
"Permissive",
|
|
537
|
+
"Permissive 审批档",
|
|
538
|
+
"Permissive 审批档位"
|
|
539
|
+
]);
|
|
540
|
+
/** Same shield glyph as dsh-auto-mode so the two approval tiers match visually. */
|
|
541
|
+
const ICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\"><path d=\"M8.21.9l6.58 2.47v3.64c0 4.99-3.74 7.2-6.58 8.29C5.36 14.21 1.62 12 1.62 7.01V3.37L8.21.9Z\" fill=\"none\" stroke=\"black\" stroke-width=\"1.32\" stroke-linejoin=\"round\"/><path d=\"M8.75 3.65 5.95 8.2h2.08l-.78 4.15 2.82-4.9H8.12l.63-3.8Z\" fill=\"black\"/></svg>";
|
|
542
|
+
function iconStyles() {
|
|
543
|
+
const mask = `url("data:image/svg+xml,${encodeURIComponent(ICON_SVG)}")`;
|
|
544
|
+
return `
|
|
545
|
+
[${ICON_ATTRIBUTE}]::before {
|
|
546
|
+
content: "";
|
|
547
|
+
display: inline-block;
|
|
548
|
+
flex: 0 0 auto;
|
|
549
|
+
width: 16px;
|
|
550
|
+
height: 16px;
|
|
551
|
+
background-color: var(--dsw-alias-label-tertiary, currentColor);
|
|
552
|
+
-webkit-mask-image: ${mask};
|
|
553
|
+
mask-image: ${mask};
|
|
554
|
+
-webkit-mask-position: center;
|
|
555
|
+
mask-position: center;
|
|
556
|
+
-webkit-mask-repeat: no-repeat;
|
|
557
|
+
mask-repeat: no-repeat;
|
|
558
|
+
-webkit-mask-size: contain;
|
|
559
|
+
mask-size: contain;
|
|
560
|
+
}
|
|
561
|
+
[${ICON_ATTRIBUTE}='trigger']::before {
|
|
562
|
+
width: 14px;
|
|
563
|
+
height: 14px;
|
|
564
|
+
margin-right: 6px;
|
|
565
|
+
}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
/** The permission dropdown menu (lists the permission presets as menuitems). */
|
|
569
|
+
function isPermissionMenu(menu) {
|
|
570
|
+
return menu.matches("[role=\"menu\"]") && Array.from(menu.querySelectorAll("button[role=\"menuitem\"]")).length >= 4;
|
|
571
|
+
}
|
|
572
|
+
/** Whether one menuitem is the Permissive preset. */
|
|
573
|
+
function isPermissiveMenuItem(element) {
|
|
574
|
+
if (!element.matches("button[role=\"menuitem\"]")) return false;
|
|
575
|
+
const label = (element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
576
|
+
if (!PERMISSIVE_LABELS.has(label)) return false;
|
|
577
|
+
return element.closest("[role=\"menu\"]") !== null;
|
|
578
|
+
}
|
|
579
|
+
/** The collapsed permission-picker trigger (opens the presets menu). */
|
|
580
|
+
function isPermissionTrigger(element) {
|
|
581
|
+
return element.matches("button[aria-haspopup=\"menu\"]") && PERMISSIVE_LABELS.has((element.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Annotate the Permissive permission menuitems with the icon attribute, plus
|
|
585
|
+
* the collapsed picker trigger when its label names the Permissive preset.
|
|
586
|
+
* Unknown DOM shapes simply never match, so decoration stays conservative.
|
|
587
|
+
*/
|
|
588
|
+
function decorate(document) {
|
|
589
|
+
for (const marked of document.querySelectorAll(`[${ICON_ATTRIBUTE}]`)) if (!isPermissiveMenuItem(marked) && !isPermissionTrigger(marked)) marked.removeAttribute(ICON_ATTRIBUTE);
|
|
590
|
+
for (const menu of document.querySelectorAll("[role=\"menu\"]")) {
|
|
591
|
+
if (!isPermissionMenu(menu)) continue;
|
|
592
|
+
for (const item of menu.querySelectorAll("button[role=\"menuitem\"]")) if (isPermissiveMenuItem(item)) item.setAttribute(ICON_ATTRIBUTE, "menu");
|
|
593
|
+
}
|
|
594
|
+
for (const trigger of document.querySelectorAll("button[aria-haspopup=\"menu\"]")) if (isPermissionTrigger(trigger)) trigger.setAttribute(ICON_ATTRIBUTE, "trigger");
|
|
595
|
+
}
|
|
596
|
+
/** Install the Permissive tile icon into the permission picker; returns the disposer. */
|
|
597
|
+
function installPermissivePermissionIcon(document) {
|
|
598
|
+
for (const existing of document.querySelectorAll("style[data-plugin]")) if (existing.getAttribute("data-plugin") === PLUGIN_ID) existing.remove();
|
|
599
|
+
const style = document.createElement("style");
|
|
600
|
+
style.dataset.plugin = PLUGIN_ID;
|
|
601
|
+
style.dataset.pluginCss = `${PLUGIN_ID}/permission-icon`;
|
|
602
|
+
style.textContent = iconStyles();
|
|
603
|
+
document.head.appendChild(style);
|
|
604
|
+
let active = true;
|
|
605
|
+
let queued = false;
|
|
606
|
+
const scan = () => {
|
|
607
|
+
if (!active || queued) return;
|
|
608
|
+
queued = true;
|
|
609
|
+
queueMicrotask(() => {
|
|
610
|
+
queued = false;
|
|
611
|
+
if (active) decorate(document);
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
|
+
decorate(document);
|
|
615
|
+
const observer = new MutationObserver(scan);
|
|
616
|
+
observer.observe(document.documentElement, {
|
|
617
|
+
childList: true,
|
|
618
|
+
characterData: true,
|
|
619
|
+
subtree: true
|
|
620
|
+
});
|
|
621
|
+
return () => {
|
|
622
|
+
active = false;
|
|
623
|
+
observer.disconnect();
|
|
624
|
+
for (const marked of document.querySelectorAll(`[${ICON_ATTRIBUTE}]`)) marked.removeAttribute(ICON_ATTRIBUTE);
|
|
625
|
+
style.remove();
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/client/notice.tsx
|
|
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;
|
|
645
|
+
async function fetchEvents(sessionId, since) {
|
|
646
|
+
const query = `/api/dsh-perm-gate/events?sessionId=${encodeURIComponent(sessionId)}${since > 0 ? `&since=${since}` : ""}`;
|
|
647
|
+
const res = await fetch(query, { headers: { "cache-control": "no-cache" } });
|
|
648
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
649
|
+
const body = await res.json();
|
|
650
|
+
return Array.isArray(body.events) ? body.events : [];
|
|
651
|
+
}
|
|
652
|
+
/** Resolve the current session id from the slot props; null when unavailable. */
|
|
653
|
+
function resolveSessionId(props) {
|
|
654
|
+
const direct = props?.sessionId;
|
|
655
|
+
if (typeof direct === "string" && direct !== "") return direct;
|
|
656
|
+
try {
|
|
657
|
+
if (typeof props?.useSessions === "function") {
|
|
658
|
+
const state = props.useSessions((s) => s);
|
|
659
|
+
if (typeof state?.current === "string" && state.current !== "") return state.current;
|
|
660
|
+
}
|
|
661
|
+
} catch {}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
/** Presentation per event kind: accent color, tag, and whether it auto-dismisses. */
|
|
665
|
+
function presentation(kind) {
|
|
666
|
+
switch (kind) {
|
|
667
|
+
case "deny": return {
|
|
668
|
+
color: "var(--dsw-alias-state-error-primary, #c0392b)",
|
|
669
|
+
bg: "var(--dsw-alias-interactive-bg-hover-danger, rgba(192,57,43,0.08))",
|
|
670
|
+
tag: "DENY",
|
|
671
|
+
sticky: false
|
|
672
|
+
};
|
|
673
|
+
case "ask": return {
|
|
674
|
+
color: "var(--dsw-alias-state-warn-label, #b9770e)",
|
|
675
|
+
bg: "var(--dsw-alias-state-warn-tertiary, rgba(185,119,14,0.08))",
|
|
676
|
+
tag: "ASK",
|
|
677
|
+
sticky: true
|
|
678
|
+
};
|
|
679
|
+
case "learned": return {
|
|
680
|
+
color: "var(--dsw-alias-state-success-primary, #1e8449)",
|
|
681
|
+
bg: "var(--dsw-alias-state-success-tertiary, rgba(30,132,73,0.08))",
|
|
682
|
+
tag: "LEARNED",
|
|
683
|
+
sticky: false
|
|
684
|
+
};
|
|
685
|
+
default: return {
|
|
686
|
+
color: "var(--dsw-alias-state-success-primary, #1e8449)",
|
|
687
|
+
bg: "var(--dsw-alias-state-success-tertiary, rgba(30,132,73,0.08))",
|
|
688
|
+
tag: "ALLOW",
|
|
689
|
+
sticky: false
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function NoticeStrip({ slotsProps }) {
|
|
694
|
+
const sessionId = resolveSessionId(slotsProps);
|
|
695
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
696
|
+
const sinceRef = (0, react.useRef)(0);
|
|
697
|
+
const shownIdRef = (0, react.useRef)(0);
|
|
698
|
+
const hideTimerRef = (0, react.useRef)(null);
|
|
699
|
+
(0, react.useEffect)(() => {
|
|
700
|
+
sinceRef.current = 0;
|
|
701
|
+
shownIdRef.current = 0;
|
|
702
|
+
setNotice(null);
|
|
703
|
+
if (sessionId === null) return;
|
|
704
|
+
let alive = true;
|
|
705
|
+
let timer = null;
|
|
706
|
+
const startPolling = () => {
|
|
707
|
+
if (!alive) return;
|
|
708
|
+
timer = setInterval(() => {
|
|
709
|
+
fetchEvents(sessionId, sinceRef.current).then((events) => {
|
|
710
|
+
if (!alive || events.length === 0) return;
|
|
711
|
+
const last = events[events.length - 1];
|
|
712
|
+
if (last === void 0 || last.id <= shownIdRef.current) return;
|
|
713
|
+
shownIdRef.current = last.id;
|
|
714
|
+
sinceRef.current = last.id;
|
|
715
|
+
setNotice(last);
|
|
716
|
+
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
717
|
+
if (!presentation(last.kind).sticky) hideTimerRef.current = setTimeout(() => {
|
|
718
|
+
setNotice(null);
|
|
719
|
+
}, AUTO_HIDE_MS);
|
|
720
|
+
}).catch(() => {});
|
|
721
|
+
}, POLL_MS);
|
|
722
|
+
};
|
|
723
|
+
fetchEvents(sessionId, 0).then((events) => {
|
|
724
|
+
if (!alive) return;
|
|
725
|
+
if (events.length > 0) {
|
|
726
|
+
sinceRef.current = events[events.length - 1]?.id ?? 0;
|
|
727
|
+
shownIdRef.current = sinceRef.current;
|
|
728
|
+
}
|
|
729
|
+
startPolling();
|
|
730
|
+
}).catch(startPolling);
|
|
731
|
+
return () => {
|
|
732
|
+
alive = false;
|
|
733
|
+
if (timer !== null) clearInterval(timer);
|
|
734
|
+
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
735
|
+
};
|
|
736
|
+
}, [sessionId]);
|
|
737
|
+
if (notice === null) return null;
|
|
738
|
+
const style = presentation(notice.kind);
|
|
739
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
740
|
+
role: "status",
|
|
741
|
+
style: {
|
|
742
|
+
boxSizing: "border-box",
|
|
743
|
+
display: "flex",
|
|
744
|
+
alignItems: "center",
|
|
745
|
+
gap: "8px",
|
|
746
|
+
margin: "0 auto 6px",
|
|
747
|
+
maxWidth: "720px",
|
|
748
|
+
border: `1px solid ${style.color}`,
|
|
749
|
+
background: style.bg,
|
|
750
|
+
borderRadius: "10px",
|
|
751
|
+
padding: "5px 10px"
|
|
752
|
+
},
|
|
753
|
+
children: [
|
|
754
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
755
|
+
style: {
|
|
756
|
+
flex: "0 0 auto",
|
|
757
|
+
fontSize: "11px",
|
|
758
|
+
lineHeight: "18px",
|
|
759
|
+
padding: "0 8px",
|
|
760
|
+
borderRadius: "9px",
|
|
761
|
+
color: style.color,
|
|
762
|
+
fontWeight: 600
|
|
763
|
+
},
|
|
764
|
+
children: style.tag
|
|
765
|
+
}),
|
|
766
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
767
|
+
style: {
|
|
768
|
+
flex: "1 1 auto",
|
|
769
|
+
minWidth: 0,
|
|
770
|
+
overflow: "hidden",
|
|
771
|
+
textOverflow: "ellipsis",
|
|
772
|
+
whiteSpace: "nowrap",
|
|
773
|
+
fontSize: "12px",
|
|
774
|
+
color: "var(--dsw-alias-label-secondary, inherit)"
|
|
775
|
+
},
|
|
776
|
+
children: [
|
|
777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
778
|
+
style: {
|
|
779
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
780
|
+
color: "var(--dsw-alias-label-primary, inherit)"
|
|
781
|
+
},
|
|
782
|
+
children: notice.tool
|
|
783
|
+
}),
|
|
784
|
+
" — ",
|
|
785
|
+
notice.reason
|
|
786
|
+
]
|
|
787
|
+
}),
|
|
788
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
789
|
+
type: "button",
|
|
790
|
+
"aria-label": "dismiss",
|
|
791
|
+
onClick: () => {
|
|
792
|
+
setNotice(null);
|
|
793
|
+
},
|
|
794
|
+
style: {
|
|
795
|
+
flex: "0 0 auto",
|
|
796
|
+
width: "22px",
|
|
797
|
+
height: "22px",
|
|
798
|
+
border: "none",
|
|
799
|
+
borderRadius: "999px",
|
|
800
|
+
background: "transparent",
|
|
801
|
+
color: "var(--dsw-alias-label-tertiary, inherit)",
|
|
802
|
+
cursor: "pointer"
|
|
803
|
+
},
|
|
804
|
+
children: "×"
|
|
805
|
+
})
|
|
806
|
+
]
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/client/index.ts
|
|
811
|
+
/** The settings namespace the host half registers (kept in lockstep with src/index.ts). */
|
|
812
|
+
const PERMISSIVE_NS = "dsh-perm-gate";
|
|
813
|
+
/** Services required by the browser half. */
|
|
814
|
+
const inject = [
|
|
815
|
+
"slots",
|
|
816
|
+
"locale",
|
|
817
|
+
"settingsScope"
|
|
818
|
+
];
|
|
819
|
+
/**
|
|
820
|
+
* Client plugin body: dictionaries plus the settings page registration.
|
|
821
|
+
* @param ctx - client root context.
|
|
822
|
+
*/
|
|
823
|
+
function apply(ctx) {
|
|
824
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
825
|
+
zh,
|
|
826
|
+
en
|
|
827
|
+
}), "dsh-perm-gate: dictionaries");
|
|
828
|
+
ctx.effect(() => installPermissivePermissionIcon(globalThis.document), "dsh-perm-gate: permission icon");
|
|
829
|
+
ctx.slots.inject("conversation.input.dock", function* () {
|
|
830
|
+
yield ctx.slots.register({
|
|
831
|
+
name: "conversation.input.dock",
|
|
832
|
+
id: "dsh-perm-gate.notice",
|
|
833
|
+
order: 30,
|
|
834
|
+
label: () => t("notice.label"),
|
|
835
|
+
locale: NS
|
|
836
|
+
}, NoticeStrip);
|
|
837
|
+
});
|
|
838
|
+
const t = ctx.locale.bind(NS);
|
|
839
|
+
ctx.slots.inject("settings.plugins.tab", function* () {
|
|
840
|
+
yield ctx.slots.register({
|
|
841
|
+
name: "settings.plugins.tab",
|
|
842
|
+
id: PERMISSIVE_NS,
|
|
843
|
+
order: 50,
|
|
844
|
+
label: () => t("card.title"),
|
|
845
|
+
locale: NS,
|
|
846
|
+
inject: () => {
|
|
847
|
+
return { scope: ctx.settingsScope.bind({ namespace: PERMISSIVE_NS }) };
|
|
848
|
+
}
|
|
849
|
+
}, PermissiveCard);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
//#endregion
|
|
853
|
+
exports.apply = apply;
|
|
854
|
+
exports.inject = inject;
|
|
855
|
+
return module.exports;
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
//# sourceMappingURL=client.js.map
|