@dsh-plus/secret-env 0.1.1 → 0.1.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/lib/client.js CHANGED
@@ -6,6 +6,230 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region src/client/panel-bus.ts
11
+ const listeners = /* @__PURE__ */ new Set();
12
+ /** 请求打开指定会话的密钥面板(由 /secret 命令的 onSelect 调用)。 */
13
+ function requestOpenSessionPanel(sessionId) {
14
+ for (const listener of listeners) listener(sessionId);
15
+ }
16
+ /** 订阅打开信号;返回退订器(随组件卸载清理)。 */
17
+ function onOpenSessionPanel(listener) {
18
+ listeners.add(listener);
19
+ return () => {
20
+ listeners.delete(listener);
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/client/command.ts
25
+ /**
26
+ * `/var` 斜杠命令(dsh-client-ui-commands 的 commandUi 服务):
27
+ * contribution 仅支持官方 popupSelect 壳(kind 唯一),故命令提供单个
28
+ * 「打开面板」动作,选中后经打开总线唤起本会话的变量面板——
29
+ * 完全自定义的 React 面板只能在自己占有的 overlay 槽渲染。
30
+ * 子代理会话不提供本命令(对齐官方 /model 的可用性过滤)。
31
+ * @module secret-env/client/command
32
+ */
33
+ /** 注册 /var 命令(commandUi 服务缺席时 inject 回调不触发,自然跳过)。 */
34
+ function registerVarCommand(ctx, sessions, t) {
35
+ ctx.inject(["commandUi"], (scope) => {
36
+ scope.effect(() => scope.commandUi.register({
37
+ name: "var",
38
+ description: t("command.description"),
39
+ available: (session) => sessions.subagentAddress?.(session.sessionId) === void 0,
40
+ ui: {
41
+ kind: "popupSelect",
42
+ options: () => Promise.resolve([{
43
+ id: "open",
44
+ label: t("command.openPanel")
45
+ }]),
46
+ onSelect: (_option, session) => {
47
+ requestOpenSessionPanel(session.sessionId);
48
+ }
49
+ }
50
+ }), "secret-env: /var contribution");
51
+ });
52
+ }
53
+ //#endregion
54
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs
55
+ /**
56
+ * @license lucide-react v1.38.0 - ISC
57
+ *
58
+ * This source code is licensed under the ISC license.
59
+ * See the LICENSE file in the root directory of this source tree.
60
+ */
61
+ const mergeClasses = (...classes) => classes.filter((className, index, array) => {
62
+ return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
63
+ }).join(" ").trim();
64
+ //#endregion
65
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/toKebabCase.mjs
66
+ /**
67
+ * @license lucide-react v1.38.0 - ISC
68
+ *
69
+ * This source code is licensed under the ISC license.
70
+ * See the LICENSE file in the root directory of this source tree.
71
+ */
72
+ const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
73
+ //#endregion
74
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/toCamelCase.mjs
75
+ /**
76
+ * @license lucide-react v1.38.0 - ISC
77
+ *
78
+ * This source code is licensed under the ISC license.
79
+ * See the LICENSE file in the root directory of this source tree.
80
+ */
81
+ const toCamelCase = (string) => string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase());
82
+ //#endregion
83
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/toPascalCase.mjs
84
+ /**
85
+ * @license lucide-react v1.38.0 - ISC
86
+ *
87
+ * This source code is licensed under the ISC license.
88
+ * See the LICENSE file in the root directory of this source tree.
89
+ */
90
+ const toPascalCase = (string) => {
91
+ const camelCase = toCamelCase(string);
92
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
93
+ };
94
+ //#endregion
95
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/defaultAttributes.mjs
96
+ /**
97
+ * @license lucide-react v1.38.0 - ISC
98
+ *
99
+ * This source code is licensed under the ISC license.
100
+ * See the LICENSE file in the root directory of this source tree.
101
+ */
102
+ var defaultAttributes = {
103
+ xmlns: "http://www.w3.org/2000/svg",
104
+ width: 24,
105
+ height: 24,
106
+ viewBox: "0 0 24 24",
107
+ fill: "none",
108
+ stroke: "currentColor",
109
+ strokeWidth: 2,
110
+ strokeLinecap: "round",
111
+ strokeLinejoin: "round"
112
+ };
113
+ //#endregion
114
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/hasA11yProp.mjs
115
+ /**
116
+ * @license lucide-react v1.38.0 - ISC
117
+ *
118
+ * This source code is licensed under the ISC license.
119
+ * See the LICENSE file in the root directory of this source tree.
120
+ */
121
+ const hasA11yProp = (props) => {
122
+ for (const prop in props) if (prop.startsWith("aria-") || prop === "role" || prop === "title") return true;
123
+ return false;
124
+ };
125
+ //#endregion
126
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/context.mjs
127
+ /**
128
+ * @license lucide-react v1.38.0 - ISC
129
+ *
130
+ * This source code is licensed under the ISC license.
131
+ * See the LICENSE file in the root directory of this source tree.
132
+ */
133
+ const LucideContext = (0, react.createContext)({});
134
+ const useLucideContext = () => (0, react.useContext)(LucideContext);
135
+ //#endregion
136
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/Icon.mjs
137
+ /**
138
+ * @license lucide-react v1.38.0 - ISC
139
+ *
140
+ * This source code is licensed under the ISC license.
141
+ * See the LICENSE file in the root directory of this source tree.
142
+ */
143
+ const Icon = (0, react.forwardRef)(({ color, size, strokeWidth, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
144
+ const { size: contextSize = 24, strokeWidth: contextStrokeWidth = 2, absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, color: contextColor = "currentColor", className: contextClass = "" } = useLucideContext() ?? {};
145
+ const calculatedStrokeWidth = absoluteStrokeWidth ?? contextAbsoluteStrokeWidth ? Number(strokeWidth ?? contextStrokeWidth) * 24 / Number(size ?? contextSize) : strokeWidth ?? contextStrokeWidth;
146
+ return (0, react.createElement)("svg", {
147
+ ref,
148
+ ...defaultAttributes,
149
+ width: size ?? contextSize ?? defaultAttributes.width,
150
+ height: size ?? contextSize ?? defaultAttributes.height,
151
+ stroke: color ?? contextColor,
152
+ strokeWidth: calculatedStrokeWidth,
153
+ className: mergeClasses("lucide", contextClass, className),
154
+ ...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
155
+ ...rest
156
+ }, [...iconNode.map(([tag, attrs]) => (0, react.createElement)(tag, attrs)), ...Array.isArray(children) ? children : [children]]);
157
+ });
158
+ //#endregion
159
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/createLucideIcon.mjs
160
+ /**
161
+ * @license lucide-react v1.38.0 - ISC
162
+ *
163
+ * This source code is licensed under the ISC license.
164
+ * See the LICENSE file in the root directory of this source tree.
165
+ */
166
+ const createLucideIcon = (iconName, iconNode) => {
167
+ const Component = (0, react.forwardRef)(({ className, ...props }, ref) => (0, react.createElement)(Icon, {
168
+ ref,
169
+ iconNode,
170
+ className: mergeClasses(`lucide-${toKebabCase(toPascalCase(iconName))}`, `lucide-${iconName}`, className),
171
+ ...props
172
+ }));
173
+ Component.displayName = toPascalCase(iconName);
174
+ return Component;
175
+ };
176
+ /**
177
+ * @license lucide-react v1.38.0 - ISC
178
+ *
179
+ * This source code is licensed under the ISC license.
180
+ * See the LICENSE file in the root directory of this source tree.
181
+ */
182
+ const Eye = createLucideIcon("eye", [["path", {
183
+ d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",
184
+ key: "1nclc0"
185
+ }], ["circle", {
186
+ cx: "12",
187
+ cy: "12",
188
+ r: "3",
189
+ key: "1v7zrd"
190
+ }]]);
191
+ /**
192
+ * @license lucide-react v1.38.0 - ISC
193
+ *
194
+ * This source code is licensed under the ISC license.
195
+ * See the LICENSE file in the root directory of this source tree.
196
+ */
197
+ const EyeOff = createLucideIcon("eye-off", [
198
+ ["path", {
199
+ d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",
200
+ key: "ct8e1f"
201
+ }],
202
+ ["path", {
203
+ d: "M14.084 14.158a3 3 0 0 1-4.242-4.242",
204
+ key: "151rxh"
205
+ }],
206
+ ["path", {
207
+ d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143",
208
+ key: "13bj9a"
209
+ }],
210
+ ["path", {
211
+ d: "m2 2 20 20",
212
+ key: "1ooewy"
213
+ }]
214
+ ]);
215
+ //#endregion
216
+ //#region ../shared/src/client/icons.tsx
217
+ function lucideProps(props) {
218
+ return {
219
+ size: props.size ?? 14,
220
+ className: props.className,
221
+ "aria-hidden": true
222
+ };
223
+ }
224
+ /** 可见(注入生效)语义——官方基元无 Eye 系,lucide 补齐。 */
225
+ function IconEye(props) {
226
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eye, { ...lucideProps(props) });
227
+ }
228
+ /** 已屏蔽(本会话/全局不注入)语义。 */
229
+ function IconEyeOff(props) {
230
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EyeOff, { ...lucideProps(props) });
231
+ }
232
+ //#endregion
9
233
  //#region ../shared/src/client/fetch.ts
10
234
  /**
11
235
  * 同源自定义端点的最小 fetch 封装(各插件 client/api.ts 的公共收编版)。
@@ -175,6 +399,137 @@ ${extra}
175
399
  return tag;
176
400
  }
177
401
  //#endregion
402
+ //#region src/client/i18n.ts
403
+ /**
404
+ * 浏览器半文案(zh/en)。公共键来自 shared common,本文件只维护业务键。
405
+ * @module secret-env/client/i18n
406
+ */
407
+ const NS = "dsh-plus-secret-env";
408
+ const ownZh = {
409
+ nav: "环境变量",
410
+ title: "环境变量",
411
+ description: "以 $DSH_VAR_* 变量名向 agent 暴露值(密钥、链接等):值在每次 shell 调用时由宿主注入,不进入对话记录与提示词,不影响上下文缓存。",
412
+ globalList: "全局变量",
413
+ configured: "已配置",
414
+ unconfigured: "未配置",
415
+ readonly: "只读来源",
416
+ addGlobal: "添加全局变量",
417
+ nameLabel: "变量名",
418
+ nameHint: "大写字母/数字/下划线,如 GITHUB_TOKEN;模型实际使用 $DSH_VAR_ 前缀拼接后的完整名。",
419
+ valueLabel: "值",
420
+ valueHint: "写入后不可回读;保存于宿主凭据库,不进入对话。",
421
+ descLabel: "描述(可选)",
422
+ descHint: "仅人读备注,不进提示词。",
423
+ save: "保存",
424
+ saving: "保存中…",
425
+ delete: "删除",
426
+ copy: "复制变量名",
427
+ copied: "已复制",
428
+ emptyGlobal: "暂无全局变量。添加后 agent 即可通过变量名引用。",
429
+ loadFailed: "加载失败,请稍后重试。",
430
+ retry: "重试",
431
+ inheritedList: "继承变量(宿主环境)",
432
+ inheritedNote: "来自宿主进程环境,默认纳入注入",
433
+ inheritedEmpty: "宿主环境中没有可继承的 DSH_VAR_* 变量。",
434
+ maskGlobal: "全局屏蔽",
435
+ unmaskGlobal: "恢复注入",
436
+ maskedBadge: "已屏蔽",
437
+ maskedGlobalBadge: "已全局屏蔽",
438
+ sessionManage: "会话变量",
439
+ sessionPick: "选择会话",
440
+ sessionPickEmpty: "暂无会话",
441
+ sessionSecrets: "会话变量",
442
+ sessionList: "会话变量",
443
+ sessionEmpty: "本会话暂无变量。",
444
+ sessionHint: "仅当前会话的 shell 命令可用;会话结束或一次性使用即失效。",
445
+ addSession: "添加会话变量",
446
+ onceLabel: "一次性(首次使用后销毁)",
447
+ globalInSession: "全局变量(可在本会话屏蔽)",
448
+ inheritedInSession: "继承变量(可在本会话屏蔽)",
449
+ maskSession: "在本会话屏蔽",
450
+ unmaskSession: "在本会话恢复",
451
+ close: "关闭",
452
+ refresh: "刷新",
453
+ scopeGlobal: "全局",
454
+ scopeSession: "会话",
455
+ scopeOnce: "一次性",
456
+ scopeInherited: "继承",
457
+ "menu.title": "变量",
458
+ "menu.aria": "变量候选",
459
+ "command.description": "管理本会话的环境变量(密钥、链接等)",
460
+ "command.openPanel": "打开会话变量面板",
461
+ "error.invalid-name": "变量名不合法:大写字母开头,仅字母/数字/下划线,≤64 字符。",
462
+ "error.empty-value": "值不能为空。",
463
+ "error.shadowed": "进程环境中已存在同名变量且优先级更高,写入无效;请先取消该环境变量。",
464
+ "error.conflict": "变量名与其他插件注册的受管变量冲突。",
465
+ "error.no-session": "缺少会话标识,请刷新后重试。",
466
+ "error.method": "请求方法不允许。",
467
+ "error.internal": "服务内部错误,请查看宿主日志。"
468
+ };
469
+ const ownEn = {
470
+ nav: "Env Variables",
471
+ title: "Env Variables",
472
+ description: "Expose values (keys, links, etc.) to the agent as $DSH_VAR_* variables: values are injected by the host into each shell call, never enter the transcript or prompt, and do not affect context caching.",
473
+ globalList: "Global variables",
474
+ configured: "Configured",
475
+ unconfigured: "Not configured",
476
+ readonly: "Read-only source",
477
+ addGlobal: "Add global variable",
478
+ nameLabel: "Variable name",
479
+ nameHint: "Uppercase letters/digits/underscore, e.g. GITHUB_TOKEN; the model uses the full name with the $DSH_VAR_ prefix.",
480
+ valueLabel: "Value",
481
+ valueHint: "Write-only after saving; stored in the host credential store, never in chat.",
482
+ descLabel: "Description (optional)",
483
+ descHint: "Human-readable note only; never enters prompts.",
484
+ save: "Save",
485
+ saving: "Saving…",
486
+ delete: "Delete",
487
+ copy: "Copy variable name",
488
+ copied: "Copied",
489
+ emptyGlobal: "No global variables yet. Once added, the agent can reference them by name.",
490
+ loadFailed: "Failed to load. Try again later.",
491
+ retry: "Retry",
492
+ inheritedList: "Inherited variables (host env)",
493
+ inheritedNote: "From the host process environment; injected by default",
494
+ inheritedEmpty: "No inheritable DSH_VAR_* variables in the host environment.",
495
+ maskGlobal: "Mask globally",
496
+ unmaskGlobal: "Resume injection",
497
+ maskedBadge: "Masked",
498
+ maskedGlobalBadge: "Masked globally",
499
+ sessionManage: "Session variables",
500
+ sessionPick: "Select session",
501
+ sessionPickEmpty: "No sessions",
502
+ sessionSecrets: "Session variables",
503
+ sessionList: "Session variables",
504
+ sessionEmpty: "No session variables yet.",
505
+ sessionHint: "Available to shell commands of this session only; expires when the session ends or after one use.",
506
+ addSession: "Add session variable",
507
+ onceLabel: "One-time (destroyed after first use)",
508
+ globalInSession: "Global variables (can be masked in this session)",
509
+ inheritedInSession: "Inherited variables (can be masked in this session)",
510
+ maskSession: "Mask in this session",
511
+ unmaskSession: "Resume in this session",
512
+ close: "Close",
513
+ refresh: "Refresh",
514
+ scopeGlobal: "global",
515
+ scopeSession: "session",
516
+ scopeOnce: "once",
517
+ scopeInherited: "inherited",
518
+ "menu.title": "Variables",
519
+ "menu.aria": "Variable suggestions",
520
+ "command.description": "Manage this session's env variables (keys, links, etc.)",
521
+ "command.openPanel": "Open the session variable panel",
522
+ "error.invalid-name": "Invalid name: start with an uppercase letter; letters/digits/underscore only, ≤64 chars.",
523
+ "error.empty-value": "Value must not be empty.",
524
+ "error.shadowed": "A same-named process environment variable shadows this write; unset it first.",
525
+ "error.conflict": "The variable conflicts with a managed variable registered by another plugin.",
526
+ "error.no-session": "Missing session id; refresh and retry.",
527
+ "error.method": "Method not allowed.",
528
+ "error.internal": "Internal error; check the host log."
529
+ };
530
+ const zh = mergeDict(commonZh, ownZh);
531
+ const en = mergeDict(commonEn, ownEn);
532
+ //#endregion
178
533
  //#region src/client/api.ts
179
534
  /**
180
535
  * 数据端点通道:GET list / POST set/unset(同源 fetch,值不上行到任何对话通道)。
@@ -223,477 +578,93 @@ ${extra}
223
578
  name
224
579
  });
225
580
  }
581
+ /** 屏蔽/恢复:sessionId 缺席为全局屏蔽(设置页),否则为会话级屏蔽。 */
582
+ function setMask(name, masked, sessionId) {
583
+ return write("mask/set", {
584
+ name,
585
+ masked,
586
+ sessionId
587
+ });
588
+ }
226
589
  //#endregion
227
- //#region src/client/common.ts
590
+ //#region src/client/menu-core.ts
591
+ const WORD_CHAR = /[\p{L}\p{N}_]/u;
592
+ const TOKEN_CHAR = /^[A-Za-z0-9_]$/;
228
593
  /**
229
- * 浏览器半共享小件:错误码文案映射与复制助手(设置页与会话控件共用)。
230
- * @module secret-env/client/common
594
+ * 检测光标处的 `$` 触发令牌。词法:
595
+ * - 从光标向左扫描,token 字符(字母/数字/下划线)继续,空白直接判负;
596
+ * - 遇到 `$` 时校验词边界:起草开头 / 前字符为空白 / 前字符为非词字符(标点)
597
+ * 才开闸——`foo$BAR` 这类词中 $ 不触发(与官方 URL 内 / 不触发同理);
598
+ * - 其余字符(如 `{`、`(`)终止扫描。
231
599
  */
232
- /** 端点错误 本地化文案(未知码回落 internal)。 */
233
- function errorText(t, error) {
234
- const code = error instanceof ApiError ? error.code : "internal";
235
- const text = t(`error.${code}`);
236
- return text === `error.${code}` ? t("error.internal") : text;
600
+ function detectSecretTrigger(draft, caret) {
601
+ if (caret <= 0 || caret > draft.length) return null;
602
+ for (let i = caret - 1; i >= 0; i--) {
603
+ const ch = draft.charAt(i);
604
+ if (/\s/u.test(ch)) return null;
605
+ if (ch !== "$") {
606
+ if (!TOKEN_CHAR.test(ch)) return null;
607
+ continue;
608
+ }
609
+ if (i > 0) {
610
+ const prev = draft.charAt(i - 1);
611
+ if (!/\s/u.test(prev) && WORD_CHAR.test(prev)) return null;
612
+ }
613
+ return {
614
+ query: draft.slice(i + 1, caret),
615
+ start: i,
616
+ end: caret
617
+ };
618
+ }
619
+ return null;
237
620
  }
238
- /** 复制文本到剪贴板(clipboard API 缺席时退 execCommand)。 */
239
- async function copyText(text) {
240
- try {
241
- await navigator.clipboard.writeText(text);
242
- return true;
243
- } catch {
244
- const area = document.createElement("textarea");
245
- area.value = text;
246
- area.style.position = "fixed";
247
- area.style.opacity = "0";
248
- document.body.appendChild(area);
249
- area.select();
250
- const ok = document.execCommand("copy");
251
- area.remove();
252
- return ok;
621
+ /** 两个命中的等价判定(抑制重复 setState)。 */
622
+ function sameHit(a, b) {
623
+ if (a === null || b === null) return a === b;
624
+ return a.query === b.query && a.start === b.start && a.end === b.end;
625
+ }
626
+ /** query 过滤候选(大小写不敏感;变量名前缀命中排前,其余子串命中随后)。 */
627
+ function filterCandidates(entries, query) {
628
+ const q = query.toUpperCase();
629
+ const prefixed = [];
630
+ const partial = [];
631
+ for (const entry of entries) {
632
+ if (q === "") {
633
+ prefixed.push(entry);
634
+ continue;
635
+ }
636
+ if (entry.envName.toUpperCase().includes(q) || entry.name.toUpperCase().includes(q)) {
637
+ if (entry.name.toUpperCase().startsWith(q) || entry.envName.toUpperCase().includes(`_${q}`)) prefixed.push(entry);
638
+ else partial.push(entry);
639
+ }
253
640
  }
641
+ return [...prefixed, ...partial];
642
+ }
643
+ /**
644
+ * 渲染文本偏移 → 草稿投影偏移。contenteditable 中芯片(引用装饰器)的
645
+ * 渲染文本与其草稿投影长度不等,光标渲染偏移需逐芯片修正。
646
+ */
647
+ function applyChipCorrection(renderedOffset, chipsBeforeCaret) {
648
+ let offset = renderedOffset;
649
+ for (const chip of chipsBeforeCaret) offset -= chip.rendered - chip.draft;
650
+ return Math.max(0, offset);
254
651
  }
255
652
  //#endregion
256
- //#region src/client/composer.tsx
653
+ //#region src/client/menu.tsx
257
654
  /**
258
- * 会话注入控件(conversation.input.right 官方插槽,session 作用域):
259
- * composer 工具行的钥匙胶囊,弹出本会话的会话级密钥面板(列表 + 增删)。
260
- * 会话 id 由插槽 inject 工厂直接供给(框架解析的严格 session 作用域)。
261
- * 窄屏(≤767px)popover 转底部抽屉;桌面点外部或 Esc 关闭。
262
- * @module secret-env/client/composer
655
+ * `$` 触发补全菜单(conversation.input.overlay 官方插槽,session 作用域):
656
+ * composer 中输入 `$` 弹出密钥名候选(对齐官方 / 命令与 @ 引用的交互)。
657
+ *
658
+ * 官方 input-trigger 管线的检测核硬编码 '/' 与 '@'(TriggerChar 联合类型),
659
+ * '$' 进不了官方检测,故检测与菜单由本组件自理,插入复用官方会话作用域
660
+ * bail 通道 'slash/input-insert-text'(span + draftRev CAS,编辑器内应用)。
661
+ * @module secret-env/client/menu
263
662
  */
264
- const EMPTY_FORM$1 = {
265
- name: "",
266
- value: "",
267
- description: "",
268
- once: false
269
- };
270
- function SessionRow(props) {
271
- const { t, entry } = props;
272
- const [copied, setCopied] = (0, react.useState)(false);
273
- const timer = (0, react.useRef)(null);
274
- (0, react.useEffect)(() => () => {
275
- if (timer.current !== null) clearTimeout(timer.current);
276
- }, []);
277
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
278
- className: "dse-row",
279
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
280
- className: "dse-rowMain",
281
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
282
- className: "dse-env",
283
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
284
- className: "dse-envName",
285
- children: ["$", entry.envName]
286
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
287
- type: "button",
288
- className: "dse-iconBtn",
289
- title: t("copy"),
290
- "aria-label": t("copy"),
291
- onClick: () => {
292
- copyText(`$${entry.envName}`).then((ok) => {
293
- if (!ok) return;
294
- setCopied(true);
295
- if (timer.current !== null) clearTimeout(timer.current);
296
- timer.current = setTimeout(() => setCopied(false), 1500);
297
- });
298
- },
299
- children: copied ? "✓" : "⧉"
300
- })]
301
- }), entry.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
302
- className: "dse-note",
303
- children: entry.description
304
- }) : null]
305
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
306
- className: "dse-badges",
307
- children: [entry.once ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
308
- className: "dse-badge",
309
- children: t("scopeOnce")
310
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
311
- type: "button",
312
- className: "dse-iconBtn dse-delBtn",
313
- title: t("delete"),
314
- "aria-label": t("delete"),
315
- onClick: () => {
316
- unsetSession(props.sessionId, entry.name).then(() => props.onDeleted()).catch((error) => props.onError(errorText(t, error)));
317
- },
318
- children: "✕"
319
- })]
320
- })]
321
- });
322
- }
323
- function ComposerSecret(props) {
324
- const { sessionId, t } = props;
325
- const [open, setOpen] = (0, react.useState)(false);
326
- const [items, setItems] = (0, react.useState)(null);
327
- const [form, setForm] = (0, react.useState)(EMPTY_FORM$1);
328
- const [saving, setSaving] = (0, react.useState)(false);
329
- const [status, setStatus] = (0, react.useState)(null);
330
- const wrapRef = (0, react.useRef)(null);
331
- const load = () => {
332
- fetchSecrets(sessionId).then((list) => setItems(list.session)).catch(() => setStatus(errorText(t, /* @__PURE__ */ new Error("internal"))));
333
- };
334
- (0, react.useEffect)(() => {
335
- if (!open) return;
336
- load();
337
- }, [open, sessionId]);
338
- (0, react.useEffect)(() => {
339
- if (!open) return;
340
- const onDown = (event) => {
341
- if (wrapRef.current !== null && !wrapRef.current.contains(event.target)) setOpen(false);
342
- };
343
- const onKey = (event) => {
344
- if (event.key === "Escape") setOpen(false);
345
- };
346
- document.addEventListener("mousedown", onDown);
347
- document.addEventListener("keydown", onKey);
348
- return () => {
349
- document.removeEventListener("mousedown", onDown);
350
- document.removeEventListener("keydown", onKey);
351
- };
352
- }, [open]);
353
- const onSave = () => {
354
- setSaving(true);
355
- setStatus(null);
356
- setSession(sessionId, form.name, form.value, form.description, form.once).then(() => {
357
- setForm(EMPTY_FORM$1);
358
- load();
359
- }).catch((error) => setStatus(errorText(t, error))).finally(() => setSaving(false));
360
- };
361
- const count = items?.length ?? 0;
362
- const canSave = form.name.trim() !== "" && form.value !== "" && !saving;
363
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
364
- className: "dse-wrap",
365
- ref: wrapRef,
366
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
367
- type: "button",
368
- className: "dse-chip",
369
- "aria-expanded": open,
370
- "aria-label": t("sessionSecrets"),
371
- title: t("sessionSecrets"),
372
- onClick: () => setOpen(!open),
373
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
374
- width: "14",
375
- height: "14",
376
- viewBox: "0 0 24 24",
377
- fill: "none",
378
- "aria-hidden": "true",
379
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
380
- d: "M14.5 2a7.5 7.5 0 0 0-7.36 9.04L2 20.19V22h1.81l1.5-1.5v-1.81h1.81v-1.81h1.81l1.22-1.22A7.5 7.5 0 1 0 14.5 2Zm2 5.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Z",
381
- fill: "currentColor"
382
- })
383
- }), count > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
384
- className: "dse-count",
385
- children: count
386
- }) : null]
387
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
388
- className: "dse-pop",
389
- role: "dialog",
390
- "aria-label": t("sessionSecrets"),
391
- children: [
392
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
393
- className: "dse-popHead",
394
- children: [
395
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
396
- className: "dse-popTitle",
397
- children: t("sessionSecrets")
398
- }),
399
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
400
- type: "button",
401
- className: "dse-iconBtn",
402
- title: t("refresh"),
403
- "aria-label": t("refresh"),
404
- onClick: load,
405
- children: "⟳"
406
- }),
407
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
408
- type: "button",
409
- className: "dse-iconBtn",
410
- title: t("close"),
411
- "aria-label": t("close"),
412
- onClick: () => setOpen(false),
413
- children: "✕"
414
- })
415
- ]
416
- }),
417
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
418
- className: "dse-popHint",
419
- children: t("sessionHint")
420
- }),
421
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
422
- className: "dse-popList",
423
- children: items === null || items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
424
- className: "dse-empty",
425
- children: t("sessionEmpty")
426
- }) : items.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionRow, {
427
- t,
428
- sessionId,
429
- entry,
430
- onDeleted: load,
431
- onError: (text) => setStatus(text === "" ? null : text)
432
- }, entry.name))
433
- }),
434
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
435
- className: "dse-popForm",
436
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
437
- className: "dse-field",
438
- children: [
439
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
440
- className: "dse-head",
441
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
442
- className: "dse-label",
443
- htmlFor: "dse-s-name",
444
- children: t("addSession")
445
- })
446
- }),
447
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
448
- id: "dse-s-name",
449
- className: "dse-input",
450
- placeholder: "API_TOKEN",
451
- value: form.name,
452
- onChange: (event) => setForm({
453
- ...form,
454
- name: event.target.value
455
- })
456
- }),
457
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
458
- className: "dse-input",
459
- type: "password",
460
- autoComplete: "off",
461
- placeholder: t("valueLabel"),
462
- value: form.value,
463
- onChange: (event) => setForm({
464
- ...form,
465
- value: event.target.value
466
- })
467
- }),
468
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
469
- className: "dse-input",
470
- placeholder: t("descLabel"),
471
- value: form.description,
472
- onChange: (event) => setForm({
473
- ...form,
474
- description: event.target.value
475
- })
476
- }),
477
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
478
- className: "dse-checkRow",
479
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
480
- id: "dse-s-once",
481
- type: "checkbox",
482
- role: "switch",
483
- "aria-checked": form.once,
484
- checked: form.once,
485
- onChange: (event) => setForm({
486
- ...form,
487
- once: event.target.checked
488
- })
489
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
490
- htmlFor: "dse-s-once",
491
- children: t("onceLabel")
492
- })]
493
- })
494
- ]
495
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
496
- className: "dse-foot",
497
- children: [status !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
498
- className: "dse-status dse-statusError",
499
- children: status
500
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
501
- type: "button",
502
- className: "dse-btn dse-btnPrimary",
503
- disabled: !canSave,
504
- onClick: onSave,
505
- children: saving ? t("saving") : t("save")
506
- })]
507
- })]
508
- })
509
- ]
510
- }) : null]
511
- });
512
- }
513
- //#endregion
514
- //#region src/client/i18n.ts
515
- /**
516
- * 浏览器半文案(zh/en)。公共键来自 shared common,本文件只维护业务键。
517
- * @module secret-env/client/i18n
518
- */
519
- const NS = "dsh-plus-secret-env";
520
- const ownZh = {
521
- nav: "密钥变量",
522
- title: "密钥变量",
523
- description: "以 $DSH_SECRET_* 变量名向 agent 暴露密钥:值在每次 shell 调用时由宿主注入,不进入对话记录与提示词,不影响上下文缓存。",
524
- globalList: "全局密钥",
525
- colName: "变量名(模型可见)",
526
- colDescription: "描述",
527
- colState: "状态",
528
- colActions: "操作",
529
- configured: "已配置",
530
- unconfigured: "未配置",
531
- readonly: "只读来源",
532
- addGlobal: "添加全局密钥",
533
- nameLabel: "变量名后缀",
534
- nameHint: "大写字母/数字/下划线,如 GITHUB_TOKEN;模型实际使用 $DSH_SECRET_ 前缀拼接后的完整名。",
535
- valueLabel: "值",
536
- valueHint: "写入后不可回读;保存于宿主凭据库,不进入对话。",
537
- descLabel: "描述(可选)",
538
- descHint: "仅人读备注,不进提示词。",
539
- save: "保存",
540
- saving: "保存中…",
541
- delete: "删除",
542
- deleteConfirm: "确认删除 {name}?该变量将立即对后续命令失效。",
543
- copy: "复制变量名",
544
- copied: "已复制",
545
- emptyGlobal: "暂无全局密钥。添加后 agent 即可通过变量名引用。",
546
- loadFailed: "加载失败,请稍后重试。",
547
- retry: "重试",
548
- sessionSecrets: "会话密钥",
549
- sessionEmpty: "本会话暂无有默认值的密钥。",
550
- sessionHint: "仅当前会话的 shell 命令可用;会话结束或一次性使用即失效。",
551
- addSession: "添加会话密钥",
552
- onceLabel: "一次性(首次使用后销毁)",
553
- close: "关闭",
554
- refresh: "刷新",
555
- scopeGlobal: "全局",
556
- scopeSession: "会话",
557
- scopeOnce: "一次性",
558
- "menu.title": "密钥变量",
559
- "menu.aria": "密钥变量候选",
560
- "error.invalid-name": "变量名不合法:大写字母开头,仅字母/数字/下划线,≤64 字符。",
561
- "error.empty-value": "值不能为空。",
562
- "error.shadowed": "进程环境中已存在同名变量且优先级更高,写入无效;请先取消该环境变量。",
563
- "error.conflict": "变量名与其他插件注册的受管变量冲突。",
564
- "error.no-session": "缺少会话标识,请刷新后重试。",
565
- "error.method": "请求方法不允许。",
566
- "error.internal": "服务内部错误,请查看宿主日志。"
567
- };
568
- const ownEn = {
569
- nav: "Secret Variables",
570
- title: "Secret Variables",
571
- description: "Expose secrets to the agent as $DSH_SECRET_* variables: values are injected by the host into each shell call, never enter the transcript or prompt, and do not affect context caching.",
572
- globalList: "Global secrets",
573
- colName: "Variable (model-visible)",
574
- colDescription: "Description",
575
- colState: "State",
576
- colActions: "Actions",
577
- configured: "Configured",
578
- unconfigured: "Not configured",
579
- readonly: "Read-only source",
580
- addGlobal: "Add global secret",
581
- nameLabel: "Name suffix",
582
- nameHint: "Uppercase letters/digits/underscore, e.g. GITHUB_TOKEN; the model uses the full name with the $DSH_SECRET_ prefix.",
583
- valueLabel: "Value",
584
- valueHint: "Write-only after saving; stored in the host credential store, never in chat.",
585
- descLabel: "Description (optional)",
586
- descHint: "Human-readable note only; never enters prompts.",
587
- save: "Save",
588
- saving: "Saving…",
589
- delete: "Delete",
590
- deleteConfirm: "Delete {name}? The variable stops working for subsequent commands immediately.",
591
- copy: "Copy variable name",
592
- copied: "Copied",
593
- emptyGlobal: "No global secrets yet. Once added, the agent can reference them by name.",
594
- loadFailed: "Failed to load. Try again later.",
595
- retry: "Retry",
596
- sessionSecrets: "Session secrets",
597
- sessionEmpty: "No session secrets yet.",
598
- sessionHint: "Available to shell commands of this session only; expires when the session ends or after one use.",
599
- addSession: "Add session secret",
600
- onceLabel: "One-time (destroyed after first use)",
601
- close: "Close",
602
- refresh: "Refresh",
603
- scopeGlobal: "global",
604
- scopeSession: "session",
605
- scopeOnce: "once",
606
- "menu.title": "Secret variables",
607
- "menu.aria": "Secret variable suggestions",
608
- "error.invalid-name": "Invalid name: start with an uppercase letter; letters/digits/underscore only, ≤64 chars.",
609
- "error.empty-value": "Value must not be empty.",
610
- "error.shadowed": "A same-named process environment variable shadows this write; unset it first.",
611
- "error.conflict": "The variable conflicts with a managed variable registered by another plugin.",
612
- "error.no-session": "Missing session id; refresh and retry.",
613
- "error.method": "Method not allowed.",
614
- "error.internal": "Internal error; check the host log."
615
- };
616
- const zh = mergeDict(commonZh, ownZh);
617
- const en = mergeDict(commonEn, ownEn);
618
- //#endregion
619
- //#region src/client/menu-core.ts
620
- const WORD_CHAR = /[\p{L}\p{N}_]/u;
621
- const TOKEN_CHAR = /^[A-Za-z0-9_]$/;
622
- /**
623
- * 检测光标处的 `$` 触发令牌。词法:
624
- * - 从光标向左扫描,token 字符(字母/数字/下划线)继续,空白直接判负;
625
- * - 遇到 `$` 时校验词边界:起草开头 / 前字符为空白 / 前字符为非词字符(标点)
626
- * 才开闸——`foo$BAR` 这类词中 $ 不触发(与官方 URL 内 / 不触发同理);
627
- * - 其余字符(如 `{`、`(`)终止扫描。
628
- */
629
- function detectSecretTrigger(draft, caret) {
630
- if (caret <= 0 || caret > draft.length) return null;
631
- for (let i = caret - 1; i >= 0; i--) {
632
- const ch = draft.charAt(i);
633
- if (/\s/u.test(ch)) return null;
634
- if (ch !== "$") {
635
- if (!TOKEN_CHAR.test(ch)) return null;
636
- continue;
637
- }
638
- if (i > 0) {
639
- const prev = draft.charAt(i - 1);
640
- if (!/\s/u.test(prev) && WORD_CHAR.test(prev)) return null;
641
- }
642
- return {
643
- query: draft.slice(i + 1, caret),
644
- start: i,
645
- end: caret
646
- };
647
- }
648
- return null;
649
- }
650
- /** 两个命中的等价判定(抑制重复 setState)。 */
651
- function sameHit(a, b) {
652
- if (a === null || b === null) return a === b;
653
- return a.query === b.query && a.start === b.start && a.end === b.end;
654
- }
655
- /** 按 query 过滤候选(大小写不敏感;变量名前缀命中排前,其余子串命中随后)。 */
656
- function filterCandidates(entries, query) {
657
- const q = query.toUpperCase();
658
- const prefixed = [];
659
- const partial = [];
660
- for (const entry of entries) {
661
- if (q === "") {
662
- prefixed.push(entry);
663
- continue;
664
- }
665
- if (entry.envName.toUpperCase().includes(q) || entry.name.toUpperCase().includes(q)) {
666
- if (entry.name.toUpperCase().startsWith(q) || entry.envName.toUpperCase().includes(`_${q}`)) prefixed.push(entry);
667
- else partial.push(entry);
668
- }
669
- }
670
- return [...prefixed, ...partial];
671
- }
672
- /**
673
- * 渲染文本偏移 → 草稿投影偏移。contenteditable 中芯片(引用装饰器)的
674
- * 渲染文本与其草稿投影长度不等,光标渲染偏移需逐芯片修正。
675
- */
676
- function applyChipCorrection(renderedOffset, chipsBeforeCaret) {
677
- let offset = renderedOffset;
678
- for (const chip of chipsBeforeCaret) offset -= chip.rendered - chip.draft;
679
- return Math.max(0, offset);
680
- }
681
- //#endregion
682
- //#region src/client/menu.tsx
683
- /**
684
- * `$` 触发补全菜单(conversation.input.overlay 官方插槽,session 作用域):
685
- * 在 composer 中输入 `$` 弹出密钥名候选(对齐官方 / 命令与 @ 引用的交互)。
686
- *
687
- * 官方 input-trigger 管线的检测核硬编码 '/' 与 '@'(TriggerChar 联合类型),
688
- * '$' 进不了官方检测,故检测与菜单由本组件自理,插入复用官方会话作用域
689
- * bail 通道 'slash/input-insert-text'(span + draftRev CAS,编辑器内应用)。
690
- * @module secret-env/client/menu
691
- */
692
- const FALLBACK_INPUT = {
693
- draft: "",
694
- draftRev: 0,
695
- phase: "plain",
696
- occurrences: []
663
+ const FALLBACK_INPUT = {
664
+ draft: "",
665
+ draftRev: 0,
666
+ phase: "plain",
667
+ occurrences: []
697
668
  };
698
669
  /** 本组件容器向上找到 composer 卡片与 Lexical contenteditable 根。 */
699
670
  function editorRootOf(el) {
@@ -728,18 +699,27 @@ ${extra}
728
699
  return applyChipCorrection(rendered, corrections);
729
700
  }
730
701
  function toCandidates(list) {
731
- return [...list.session.map((entry) => ({
732
- envName: entry.envName,
733
- name: entry.name,
734
- description: entry.description,
735
- scope: "session",
736
- once: entry.once
737
- })), ...list.global.map((entry) => ({
738
- envName: entry.envName,
739
- name: entry.name,
740
- description: entry.description,
741
- scope: "global"
742
- }))];
702
+ return [
703
+ ...list.session.map((entry) => ({
704
+ envName: entry.envName,
705
+ name: entry.name,
706
+ description: entry.description,
707
+ scope: "session",
708
+ once: entry.once
709
+ })),
710
+ ...list.global.filter((entry) => !entry.masked).map((entry) => ({
711
+ envName: entry.envName,
712
+ name: entry.name,
713
+ description: entry.description,
714
+ scope: "global"
715
+ })),
716
+ ...list.inherited.filter((entry) => !entry.masked).map((entry) => ({
717
+ envName: entry.envName,
718
+ name: entry.name,
719
+ description: "",
720
+ scope: "inherited"
721
+ }))
722
+ ];
743
723
  }
744
724
  function SecretMenu(props) {
745
725
  const { sessionId, t, insertToken } = props;
@@ -784,137 +764,589 @@ ${extra}
784
764
  const candidates = hit === null || items === null ? [] : filterCandidates(items, hit.query);
785
765
  const open = hit !== null && candidates.length > 0;
786
766
  (0, react.useEffect)(() => {
787
- const root = editorRootOf(wrapRef.current);
788
- if (root === null) return;
789
- const onStart = () => {
790
- composingRef.current = true;
791
- };
792
- const onEnd = () => {
793
- composingRef.current = false;
794
- };
795
- root.addEventListener("compositionstart", onStart);
796
- root.addEventListener("compositionend", onEnd);
797
- return () => {
798
- root.removeEventListener("compositionstart", onStart);
799
- root.removeEventListener("compositionend", onEnd);
800
- };
801
- }, []);
802
- const pick = (entry) => {
803
- if (hit === null) return;
804
- const span = {
805
- start: hit.start,
806
- end: hit.end,
807
- draftRev: input.draftRev
808
- };
809
- insertToken(`$${entry.envName} `, span);
810
- suppressedRef.current = null;
811
- setHit(null);
767
+ const root = editorRootOf(wrapRef.current);
768
+ if (root === null) return;
769
+ const onStart = () => {
770
+ composingRef.current = true;
771
+ };
772
+ const onEnd = () => {
773
+ composingRef.current = false;
774
+ };
775
+ root.addEventListener("compositionstart", onStart);
776
+ root.addEventListener("compositionend", onEnd);
777
+ return () => {
778
+ root.removeEventListener("compositionstart", onStart);
779
+ root.removeEventListener("compositionend", onEnd);
780
+ };
781
+ }, []);
782
+ const pick = (entry) => {
783
+ if (hit === null) return;
784
+ const span = {
785
+ start: hit.start,
786
+ end: hit.end,
787
+ draftRev: input.draftRev
788
+ };
789
+ insertToken(`$${entry.envName} `, span);
790
+ suppressedRef.current = null;
791
+ setHit(null);
792
+ };
793
+ const pickRef = (0, react.useRef)(pick);
794
+ pickRef.current = pick;
795
+ const candidatesRef = (0, react.useRef)(candidates);
796
+ candidatesRef.current = candidates;
797
+ const highlightRef = (0, react.useRef)(highlight);
798
+ highlightRef.current = highlight;
799
+ (0, react.useEffect)(() => {
800
+ if (!open) return;
801
+ const root = editorRootOf(wrapRef.current);
802
+ if (root === null) return;
803
+ const onKey = (event) => {
804
+ const list = candidatesRef.current;
805
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
806
+ event.preventDefault();
807
+ event.stopPropagation();
808
+ const delta = event.key === "ArrowDown" ? 1 : -1;
809
+ setHighlight((prev) => (prev + delta + list.length) % list.length);
810
+ return;
811
+ }
812
+ if (event.key === "Enter" || event.key === "Tab") {
813
+ const entry = list[highlightRef.current] ?? list[0];
814
+ if (entry !== void 0) {
815
+ event.preventDefault();
816
+ event.stopPropagation();
817
+ pickRef.current(entry);
818
+ }
819
+ return;
820
+ }
821
+ if (event.key === "Escape") {
822
+ event.preventDefault();
823
+ event.stopPropagation();
824
+ suppressedRef.current = hit;
825
+ setHit(null);
826
+ }
827
+ };
828
+ root.addEventListener("keydown", onKey, true);
829
+ return () => root.removeEventListener("keydown", onKey, true);
830
+ }, [open, hit]);
831
+ (0, react.useEffect)(() => {
832
+ if (!open) return;
833
+ const onDown = (event) => {
834
+ if (!(event.target instanceof Node)) return;
835
+ if ((wrapRef.current?.closest("[data-composer-card]"))?.contains(event.target) === true) return;
836
+ setHit(null);
837
+ };
838
+ document.addEventListener("pointerdown", onDown, true);
839
+ return () => document.removeEventListener("pointerdown", onDown, true);
840
+ }, [open]);
841
+ (0, react.useEffect)(() => {
842
+ setHighlight((prev) => candidates.length === 0 ? 0 : Math.min(prev, candidates.length - 1));
843
+ }, [candidates.length]);
844
+ if (!open) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
845
+ className: "dse-menuWrap",
846
+ ref: wrapRef
847
+ });
848
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
849
+ className: "dse-menuWrap",
850
+ ref: wrapRef,
851
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
852
+ className: "dse-menu",
853
+ role: "listbox",
854
+ "aria-label": t("menu.aria"),
855
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
856
+ className: "dse-menuTitle",
857
+ children: t("menu.title")
858
+ }), candidates.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
859
+ type: "button",
860
+ role: "option",
861
+ "aria-selected": index === highlight,
862
+ title: `$${entry.envName}`,
863
+ className: `dse-menuItem${index === highlight ? " dse-menuItemActive" : ""}`,
864
+ onMouseEnter: () => setHighlight(index),
865
+ onMouseDown: (event) => {
866
+ event.preventDefault();
867
+ pick(entry);
868
+ },
869
+ children: [
870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
871
+ className: "dse-menuName",
872
+ children: entry.name
873
+ }),
874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
875
+ className: "dse-menuDesc",
876
+ children: entry.description
877
+ }),
878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
879
+ className: "dse-menuBadges",
880
+ children: [entry.once === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
881
+ className: "dse-badge",
882
+ children: t("scopeOnce")
883
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
884
+ className: "dse-badge dse-badgeDim",
885
+ children: entry.scope === "session" ? t("scopeSession") : entry.scope === "inherited" ? t("scopeInherited") : t("scopeGlobal")
886
+ })]
887
+ })
888
+ ]
889
+ }, `${entry.scope}:${entry.name}`))]
890
+ })
891
+ });
892
+ }
893
+ //#endregion
894
+ //#region src/names.ts
895
+ /** 后缀语法:POSIX 环境变量名,大写字母开头,总长(含前缀)不超过 79。 */
896
+ const SUFFIX_PATTERN = /^[A-Z][A-Z0-9_]*$/;
897
+ const MAX_SUFFIX_LENGTH = 64;
898
+ /** 规范化用户输入:去空白并转大写。 */
899
+ function normalizeSuffix(raw) {
900
+ return raw.trim().toUpperCase();
901
+ }
902
+ /** 校验规范化后的后缀;合法返回 null,非法返回失败码。 */
903
+ function validateSuffix(suffix) {
904
+ if (suffix.length === 0) return "empty";
905
+ if (suffix.length > MAX_SUFFIX_LENGTH) return "too-long";
906
+ if (!SUFFIX_PATTERN.test(suffix)) return "bad-charset";
907
+ return null;
908
+ }
909
+ //#endregion
910
+ //#region src/client/common.ts
911
+ /**
912
+ * 浏览器半共享小件:错误码文案映射、复制助手与变量名输入规则
913
+ * (设置页与会话控件共用;名称规则与 node 半 names.ts 同源)。
914
+ * @module secret-env/client/common
915
+ */
916
+ /** 端点错误 → 本地化文案(未知码回落 internal)。 */
917
+ function errorText(t, error) {
918
+ const code = error instanceof ApiError ? error.code : "internal";
919
+ const text = t(`error.${code}`);
920
+ return text === `error.${code}` ? t("error.internal") : text;
921
+ }
922
+ /** 名称输入的实时规范化:键入即转大写(去空白交给保存时的 normalizeSuffix)。 */
923
+ function liveName(raw) {
924
+ return raw.toUpperCase();
925
+ }
926
+ /** 名称输入的实时校验:空串不标错(必填兜底),其余按 names.ts 规则判定。 */
927
+ function nameErrorOf(raw) {
928
+ if (raw.trim() === "") return null;
929
+ return validateSuffix(normalizeSuffix(raw));
930
+ }
931
+ /** 复制文本到剪贴板(clipboard API 缺席时退 execCommand)。 */
932
+ async function copyText(text) {
933
+ try {
934
+ await navigator.clipboard.writeText(text);
935
+ return true;
936
+ } catch {
937
+ const area = document.createElement("textarea");
938
+ area.value = text;
939
+ area.style.position = "fixed";
940
+ area.style.opacity = "0";
941
+ document.body.appendChild(area);
942
+ area.select();
943
+ const ok = document.execCommand("copy");
944
+ area.remove();
945
+ return ok;
946
+ }
947
+ }
948
+ //#endregion
949
+ //#region src/client/panel.tsx
950
+ /**
951
+ * 会话密钥面板(复用件):会话级密钥的列表/增删 + 全局与继承变量的
952
+ * 会话内屏蔽开关。两个宿主共用——overlay 槽的面板宿主(/secret 命令唤起,
953
+ * 传 onClose)与设置页的会话管理区(选择会话后内嵌,无头栏)。
954
+ * 值只经同源端点下行,绝不回显。
955
+ * @module secret-env/client/panel
956
+ */
957
+ const EMPTY_FORM$1 = {
958
+ name: "",
959
+ value: "",
960
+ description: "",
961
+ once: false
962
+ };
963
+ /** 复制按钮(复制成功短暂亮勾)。 */
964
+ function CopyButton$1(props) {
965
+ const { t } = props;
966
+ const [copied, setCopied] = (0, react.useState)(false);
967
+ const timer = (0, react.useRef)(null);
968
+ (0, react.useEffect)(() => () => {
969
+ if (timer.current !== null) clearTimeout(timer.current);
970
+ }, []);
971
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
972
+ type: "button",
973
+ className: "dse-iconBtn",
974
+ title: t("copy"),
975
+ "aria-label": t("copy"),
976
+ onClick: () => {
977
+ copyText(props.text).then((ok) => {
978
+ if (!ok) return;
979
+ setCopied(true);
980
+ if (timer.current !== null) clearTimeout(timer.current);
981
+ timer.current = setTimeout(() => setCopied(false), 1500);
982
+ });
983
+ },
984
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 })
985
+ });
986
+ }
987
+ /** 会话内屏蔽开关(眼睛语义:可见=注入,划掉=本会话屏蔽)。 */
988
+ function MaskToggle(props) {
989
+ const { t, masked } = props;
990
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
991
+ type: "button",
992
+ className: `dse-iconBtn dse-maskBtn${masked ? " dse-maskBtnOn" : ""}`,
993
+ title: masked ? t("unmaskSession") : t("maskSession"),
994
+ "aria-label": masked ? t("unmaskSession") : t("maskSession"),
995
+ "aria-pressed": masked,
996
+ onClick: props.onToggle,
997
+ children: masked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconEyeOff, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconEye, { size: 14 })
998
+ });
999
+ }
1000
+ function SessionRow(props) {
1001
+ const { t, entry } = props;
1002
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1003
+ className: "dse-row",
1004
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1005
+ className: "dse-rowMain",
1006
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1007
+ className: "dse-env",
1008
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1009
+ className: "dse-envName",
1010
+ children: ["$", entry.envName]
1011
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton$1, {
1012
+ t,
1013
+ text: `$${entry.envName}`
1014
+ })]
1015
+ }), entry.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1016
+ className: "dse-note",
1017
+ children: entry.description
1018
+ }) : null]
1019
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1020
+ className: "dse-badges",
1021
+ children: [entry.once ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1022
+ className: "dse-badge",
1023
+ children: t("scopeOnce")
1024
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1025
+ type: "button",
1026
+ className: "dse-iconBtn dse-delBtn",
1027
+ title: t("delete"),
1028
+ "aria-label": t("delete"),
1029
+ onClick: () => {
1030
+ unsetSession(props.sessionId, entry.name).then(() => props.onChanged()).catch((error) => props.onError(errorText(t, error)));
1031
+ },
1032
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
1033
+ })]
1034
+ })]
1035
+ });
1036
+ }
1037
+ function GlobalRow$1(props) {
1038
+ const { t, entry } = props;
1039
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1040
+ className: "dse-row",
1041
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1042
+ className: "dse-rowMain",
1043
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1044
+ className: "dse-env",
1045
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1046
+ className: `dse-envName${entry.masked ? " dse-envNameDim" : ""}`,
1047
+ children: ["$", entry.envName]
1048
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton$1, {
1049
+ t,
1050
+ text: `$${entry.envName}`
1051
+ })]
1052
+ }), entry.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1053
+ className: "dse-note",
1054
+ children: entry.description
1055
+ }) : null]
1056
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1057
+ className: "dse-badges",
1058
+ children: [entry.masked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1059
+ className: "dse-badge",
1060
+ children: t("maskedBadge")
1061
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MaskToggle, {
1062
+ t,
1063
+ masked: entry.masked,
1064
+ onToggle: () => {
1065
+ setMask(entry.name, !entry.masked, props.sessionId).then(() => props.onChanged()).catch((error) => props.onError(errorText(t, error)));
1066
+ }
1067
+ })]
1068
+ })]
1069
+ });
1070
+ }
1071
+ function InheritedRow$1(props) {
1072
+ const { t, entry } = props;
1073
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1074
+ className: "dse-row",
1075
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1076
+ className: "dse-rowMain",
1077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1078
+ className: "dse-env",
1079
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1080
+ className: `dse-envName${entry.masked ? " dse-envNameDim" : ""}`,
1081
+ children: ["$", entry.envName]
1082
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton$1, {
1083
+ t,
1084
+ text: `$${entry.envName}`
1085
+ })]
1086
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1087
+ className: "dse-note",
1088
+ children: t("inheritedNote")
1089
+ })]
1090
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1091
+ className: "dse-badges",
1092
+ children: entry.globallyMasked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1093
+ className: "dse-badge dse-badgeDim",
1094
+ children: t("maskedGlobalBadge")
1095
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [entry.masked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1096
+ className: "dse-badge",
1097
+ children: t("maskedBadge")
1098
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MaskToggle, {
1099
+ t,
1100
+ masked: entry.masked,
1101
+ onToggle: () => {
1102
+ setMask(entry.name, !entry.masked, props.sessionId).then(() => props.onChanged()).catch((error) => props.onError(errorText(t, error)));
1103
+ }
1104
+ })] })
1105
+ })]
1106
+ });
1107
+ }
1108
+ function SessionSecretsPanel(props) {
1109
+ const { sessionId, t } = props;
1110
+ const [data, setData] = (0, react.useState)(null);
1111
+ const [form, setForm] = (0, react.useState)(EMPTY_FORM$1);
1112
+ const [saving, setSaving] = (0, react.useState)(false);
1113
+ const [status, setStatus] = (0, react.useState)(null);
1114
+ const load = () => {
1115
+ fetchSecrets(sessionId).then((list) => setData(list)).catch(() => setStatus(errorText(t, /* @__PURE__ */ new Error("internal"))));
1116
+ };
1117
+ (0, react.useEffect)(() => {
1118
+ load();
1119
+ }, [sessionId]);
1120
+ const onSave = () => {
1121
+ setSaving(true);
1122
+ setStatus(null);
1123
+ setSession(sessionId, form.name, form.value, form.description, form.once).then(() => {
1124
+ setForm(EMPTY_FORM$1);
1125
+ load();
1126
+ }).catch((error) => setStatus(errorText(t, error))).finally(() => setSaving(false));
812
1127
  };
813
- const pickRef = (0, react.useRef)(pick);
814
- pickRef.current = pick;
815
- const candidatesRef = (0, react.useRef)(candidates);
816
- candidatesRef.current = candidates;
817
- const highlightRef = (0, react.useRef)(highlight);
818
- highlightRef.current = highlight;
1128
+ const onError = (text) => setStatus(text === "" ? null : text);
1129
+ const nameError = nameErrorOf(form.name);
1130
+ const canSave = form.name.trim() !== "" && nameError === null && form.value !== "" && !saving;
1131
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1132
+ className: "dse-panel",
1133
+ children: [
1134
+ props.onClose !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1135
+ className: "dse-popHead",
1136
+ children: [
1137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1138
+ className: "dse-popTitle",
1139
+ children: t("sessionSecrets")
1140
+ }),
1141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1142
+ type: "button",
1143
+ className: "dse-iconBtn",
1144
+ title: t("refresh"),
1145
+ "aria-label": t("refresh"),
1146
+ onClick: load,
1147
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 })
1148
+ }),
1149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1150
+ type: "button",
1151
+ className: "dse-iconBtn",
1152
+ title: t("close"),
1153
+ "aria-label": t("close"),
1154
+ onClick: props.onClose,
1155
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
1156
+ })
1157
+ ]
1158
+ }) : null,
1159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1160
+ className: "dse-popHint",
1161
+ children: t("sessionHint")
1162
+ }),
1163
+ status !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1164
+ className: "dse-status dse-statusError",
1165
+ children: status
1166
+ }) : null,
1167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1168
+ className: "dse-groupLabel",
1169
+ children: t("sessionList")
1170
+ }),
1171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1172
+ className: "dse-popList",
1173
+ children: data === null || data.session.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1174
+ className: "dse-empty",
1175
+ children: t("sessionEmpty")
1176
+ }) : data.session.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionRow, {
1177
+ t,
1178
+ sessionId,
1179
+ entry,
1180
+ onChanged: load,
1181
+ onError
1182
+ }, entry.name))
1183
+ }),
1184
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1185
+ className: "dse-popForm",
1186
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1187
+ className: "dse-field",
1188
+ children: [
1189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1190
+ className: "dse-head",
1191
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1192
+ className: "dse-label",
1193
+ htmlFor: "dse-s-name",
1194
+ children: t("addSession")
1195
+ })
1196
+ }),
1197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1198
+ id: "dse-s-name",
1199
+ className: `dse-input${nameError !== null ? " dse-inputError" : ""}`,
1200
+ placeholder: "API_TOKEN",
1201
+ value: form.name,
1202
+ "aria-invalid": nameError !== null,
1203
+ onChange: (event) => setForm({
1204
+ ...form,
1205
+ name: liveName(event.target.value)
1206
+ })
1207
+ }),
1208
+ nameError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1209
+ className: "dse-hint dse-hintError",
1210
+ children: t("error.invalid-name")
1211
+ }) : null,
1212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1213
+ className: "dse-input",
1214
+ type: "password",
1215
+ autoComplete: "off",
1216
+ placeholder: t("valueLabel"),
1217
+ value: form.value,
1218
+ onChange: (event) => setForm({
1219
+ ...form,
1220
+ value: event.target.value
1221
+ })
1222
+ }),
1223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1224
+ className: "dse-input",
1225
+ placeholder: t("descLabel"),
1226
+ value: form.description,
1227
+ onChange: (event) => setForm({
1228
+ ...form,
1229
+ description: event.target.value
1230
+ })
1231
+ }),
1232
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1233
+ className: "dse-checkRow",
1234
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1235
+ id: "dse-s-once",
1236
+ type: "checkbox",
1237
+ role: "switch",
1238
+ "aria-checked": form.once,
1239
+ checked: form.once,
1240
+ onChange: (event) => setForm({
1241
+ ...form,
1242
+ once: event.target.checked
1243
+ })
1244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1245
+ htmlFor: "dse-s-once",
1246
+ children: t("onceLabel")
1247
+ })]
1248
+ })
1249
+ ]
1250
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1251
+ className: "dse-foot",
1252
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1253
+ type: "button",
1254
+ className: "dse-btn dse-btnPrimary",
1255
+ disabled: !canSave,
1256
+ onClick: onSave,
1257
+ children: saving ? t("saving") : t("save")
1258
+ })
1259
+ })]
1260
+ }),
1261
+ data !== null && data.global.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1262
+ className: "dse-groupLabel",
1263
+ children: t("globalInSession")
1264
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1265
+ className: "dse-popList",
1266
+ children: data.global.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GlobalRow$1, {
1267
+ t,
1268
+ sessionId,
1269
+ entry,
1270
+ onChanged: load,
1271
+ onError
1272
+ }, entry.name))
1273
+ })] }) : null,
1274
+ data !== null && data.inherited.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1275
+ className: "dse-groupLabel",
1276
+ children: t("inheritedInSession")
1277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1278
+ className: "dse-popList",
1279
+ children: data.inherited.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InheritedRow$1, {
1280
+ t,
1281
+ sessionId,
1282
+ entry,
1283
+ onChanged: load,
1284
+ onError
1285
+ }, entry.name))
1286
+ })] }) : null
1287
+ ]
1288
+ });
1289
+ }
1290
+ //#endregion
1291
+ //#region src/client/panel-host.tsx
1292
+ /**
1293
+ * 会话变量面板的 overlay 宿主(conversation.input.overlay 官方插槽,
1294
+ * session 作用域):常驻监听打开总线,/var 命令选中后弹出面板。
1295
+ * 弹层定位于 composer 卡片上方(与 $ 菜单同一浮层语言);
1296
+ * 点外部或 Esc 关闭。闭合态不渲染面板(数据在打开时现取)。
1297
+ * @module secret-env/client/panel-host
1298
+ */
1299
+ function SessionPanelHost(props) {
1300
+ const { sessionId, t } = props;
1301
+ const [open, setOpen] = (0, react.useState)(false);
1302
+ const wrapRef = (0, react.useRef)(null);
1303
+ (0, react.useEffect)(() => onOpenSessionPanel((target) => {
1304
+ if (target === sessionId) setOpen(true);
1305
+ }), [sessionId]);
819
1306
  (0, react.useEffect)(() => {
820
1307
  if (!open) return;
821
- const root = editorRootOf(wrapRef.current);
822
- if (root === null) return;
823
1308
  const onKey = (event) => {
824
- const list = candidatesRef.current;
825
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
826
- event.preventDefault();
827
- event.stopPropagation();
828
- const delta = event.key === "ArrowDown" ? 1 : -1;
829
- setHighlight((prev) => (prev + delta + list.length) % list.length);
830
- return;
831
- }
832
- if (event.key === "Enter" || event.key === "Tab") {
833
- const entry = list[highlightRef.current] ?? list[0];
834
- if (entry !== void 0) {
835
- event.preventDefault();
836
- event.stopPropagation();
837
- pickRef.current(entry);
838
- }
839
- return;
840
- }
841
- if (event.key === "Escape") {
842
- event.preventDefault();
843
- event.stopPropagation();
844
- suppressedRef.current = hit;
845
- setHit(null);
846
- }
1309
+ if (event.key !== "Escape") return;
1310
+ event.preventDefault();
1311
+ event.stopPropagation();
1312
+ setOpen(false);
847
1313
  };
848
- root.addEventListener("keydown", onKey, true);
849
- return () => root.removeEventListener("keydown", onKey, true);
850
- }, [open, hit]);
851
- (0, react.useEffect)(() => {
852
- if (!open) return;
853
1314
  const onDown = (event) => {
854
1315
  if (!(event.target instanceof Node)) return;
855
- if ((wrapRef.current?.closest("[data-composer-card]"))?.contains(event.target) === true) return;
856
- setHit(null);
1316
+ if (wrapRef.current?.contains(event.target) === true) return;
1317
+ setOpen(false);
857
1318
  };
1319
+ document.addEventListener("keydown", onKey, true);
858
1320
  document.addEventListener("pointerdown", onDown, true);
859
- return () => document.removeEventListener("pointerdown", onDown, true);
1321
+ return () => {
1322
+ document.removeEventListener("keydown", onKey, true);
1323
+ document.removeEventListener("pointerdown", onDown, true);
1324
+ };
860
1325
  }, [open]);
861
- (0, react.useEffect)(() => {
862
- setHighlight((prev) => candidates.length === 0 ? 0 : Math.min(prev, candidates.length - 1));
863
- }, [candidates.length]);
864
- if (!open) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
865
- className: "dse-menuWrap",
866
- ref: wrapRef
867
- });
1326
+ if (!open) return null;
868
1327
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
869
- className: "dse-menuWrap",
1328
+ className: "dse-panelWrap",
870
1329
  ref: wrapRef,
871
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
872
- className: "dse-menu",
873
- role: "listbox",
874
- "aria-label": t("menu.aria"),
875
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
876
- className: "dse-menuTitle",
877
- children: t("menu.title")
878
- }), candidates.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
879
- type: "button",
880
- role: "option",
881
- "aria-selected": index === highlight,
882
- className: `dse-menuItem${index === highlight ? " dse-menuItemActive" : ""}`,
883
- onMouseEnter: () => setHighlight(index),
884
- onMouseDown: (event) => {
885
- event.preventDefault();
886
- pick(entry);
887
- },
888
- children: [
889
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
890
- className: "dse-menuName",
891
- children: ["$", entry.envName]
892
- }),
893
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
894
- className: "dse-menuDesc",
895
- children: entry.description
896
- }),
897
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
898
- className: "dse-menuBadges",
899
- children: [entry.once === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
900
- className: "dse-badge",
901
- children: t("scopeOnce")
902
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
903
- className: "dse-badge dse-badgeDim",
904
- children: entry.scope === "session" ? t("scopeSession") : t("scopeGlobal")
905
- })]
906
- })
907
- ]
908
- }, `${entry.scope}:${entry.name}`))]
1330
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1331
+ className: "dse-panelCard",
1332
+ role: "dialog",
1333
+ "aria-label": t("sessionSecrets"),
1334
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionSecretsPanel, {
1335
+ sessionId,
1336
+ t,
1337
+ onClose: () => setOpen(false)
1338
+ })
909
1339
  })
910
1340
  });
911
1341
  }
912
1342
  //#endregion
913
1343
  //#region src/client/section.tsx
914
1344
  /**
915
- * 密钥变量设置页(settings.section 官方插槽):全局密钥的列表与增删。
916
- * 只展示元数据与模型可见的完整变量名——值写入后不可回读(端点不出值)。
917
- * 响应式:≤767px 行转堆叠卡、表单转单列。
1345
+ * 环境变量设置页(settings.section 官方插槽):
1346
+ * - 全局变量:列表与增删(值写入后不可回读,端点不出值);
1347
+ * - 继承变量:识别宿主进程环境的 DSH_VAR_*(默认纳入注入),可全局屏蔽;
1348
+ * - 会话变量:经会话选择器内嵌会话面板(会话级/一次性变量的增删与屏蔽)。
1349
+ * 只展示元数据与模型可见的完整变量名。响应式:≤767px 行转堆叠卡、表单单列。
918
1350
  * @module secret-env/client/section
919
1351
  */
920
1352
  const EMPTY_FORM = {
@@ -922,23 +1354,34 @@ ${extra}
922
1354
  value: "",
923
1355
  description: ""
924
1356
  };
925
- /** 单行:完整变量名(复制)+ 描述 + 状态徽标 + 两步删除。 */
926
- function GlobalRow(props) {
927
- const { t, entry } = props;
928
- const [confirming, setConfirming] = (0, react.useState)(false);
1357
+ /** 复制按钮(复制成功短暂亮勾)。 */
1358
+ function CopyButton(props) {
1359
+ const { t } = props;
929
1360
  const [copied, setCopied] = (0, react.useState)(false);
930
1361
  const timer = (0, react.useRef)(null);
931
1362
  (0, react.useEffect)(() => () => {
932
1363
  if (timer.current !== null) clearTimeout(timer.current);
933
1364
  }, []);
934
- const onCopy = () => {
935
- copyText(`$${entry.envName}`).then((ok) => {
936
- if (!ok) return;
937
- setCopied(true);
938
- if (timer.current !== null) clearTimeout(timer.current);
939
- timer.current = setTimeout(() => setCopied(false), 1500);
940
- });
941
- };
1365
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1366
+ type: "button",
1367
+ className: "dse-iconBtn",
1368
+ title: t("copy"),
1369
+ "aria-label": t("copy"),
1370
+ onClick: () => {
1371
+ copyText(props.text).then((ok) => {
1372
+ if (!ok) return;
1373
+ setCopied(true);
1374
+ if (timer.current !== null) clearTimeout(timer.current);
1375
+ timer.current = setTimeout(() => setCopied(false), 1500);
1376
+ });
1377
+ },
1378
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 })
1379
+ });
1380
+ }
1381
+ /** 全局变量行:完整变量名(复制)+ 描述 + 状态徽标 + 两步删除。 */
1382
+ function GlobalRow(props) {
1383
+ const { t, entry } = props;
1384
+ const [confirming, setConfirming] = (0, react.useState)(false);
942
1385
  const onDelete = () => {
943
1386
  unsetGlobal(entry.name).then(() => props.onDeleted()).catch((error) => props.onError(errorText(t, error)));
944
1387
  };
@@ -951,13 +1394,9 @@ ${extra}
951
1394
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
952
1395
  className: "dse-envName",
953
1396
  children: ["$", entry.envName]
954
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
955
- type: "button",
956
- className: "dse-iconBtn",
957
- title: t("copy"),
958
- "aria-label": t("copy"),
959
- onClick: onCopy,
960
- children: copied ? "✓" : "⧉"
1397
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
1398
+ t,
1399
+ text: `$${entry.envName}`
961
1400
  })]
962
1401
  }), entry.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
963
1402
  className: "dse-note",
@@ -991,12 +1430,94 @@ ${extra}
991
1430
  title: t("delete"),
992
1431
  "aria-label": t("delete"),
993
1432
  onClick: () => setConfirming(true),
994
- children: "✕"
1433
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
995
1434
  })
996
1435
  ]
997
1436
  })]
998
1437
  });
999
1438
  }
1439
+ /** 继承变量行:来源说明 + 全局屏蔽开关(眼睛语义)。 */
1440
+ function InheritedRow(props) {
1441
+ const { t, entry } = props;
1442
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1443
+ className: "dse-row",
1444
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1445
+ className: "dse-rowMain",
1446
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1447
+ className: "dse-env",
1448
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1449
+ className: `dse-envName${entry.masked ? " dse-envNameDim" : ""}`,
1450
+ children: ["$", entry.envName]
1451
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
1452
+ t,
1453
+ text: `$${entry.envName}`
1454
+ })]
1455
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1456
+ className: "dse-note",
1457
+ children: t("inheritedNote")
1458
+ })]
1459
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1460
+ className: "dse-badges",
1461
+ children: [entry.masked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1462
+ className: "dse-badge",
1463
+ children: t("maskedBadge")
1464
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1465
+ type: "button",
1466
+ className: `dse-iconBtn dse-maskBtn${entry.masked ? " dse-maskBtnOn" : ""}`,
1467
+ title: entry.masked ? t("unmaskGlobal") : t("maskGlobal"),
1468
+ "aria-label": entry.masked ? t("unmaskGlobal") : t("maskGlobal"),
1469
+ "aria-pressed": entry.masked,
1470
+ onClick: () => {
1471
+ setMask(entry.name, !entry.masked).then(() => props.onChanged()).catch((error) => props.onError(errorText(t, error)));
1472
+ },
1473
+ children: entry.masked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconEyeOff, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconEye, { size: 14 })
1474
+ })]
1475
+ })]
1476
+ });
1477
+ }
1478
+ /** 会话变量管理块:会话选择器 + 内嵌会话面板。 */
1479
+ function SessionManageBlock(props) {
1480
+ const { t, sessions } = props;
1481
+ const snapshot = (0, react.useSyncExternalStore)((listener) => sessions.list.subscribe(listener), () => sessions.list.getSnapshot());
1482
+ const [picked, setPicked] = (0, react.useState)("");
1483
+ (0, react.useEffect)(() => {
1484
+ sessions.refresh().catch(() => {});
1485
+ }, []);
1486
+ const rows = Object.values(snapshot.byId);
1487
+ const current = rows.find((row) => row.id === picked)?.id ?? snapshot.current ?? rows[0]?.id ?? "";
1488
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1489
+ className: "dse-groupLabel",
1490
+ children: t("sessionManage")
1491
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1492
+ className: "dse-form",
1493
+ children: rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1494
+ className: "dse-empty",
1495
+ children: t("sessionPickEmpty")
1496
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1497
+ className: "dse-field",
1498
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1499
+ className: "dse-head",
1500
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1501
+ className: "dse-label",
1502
+ htmlFor: "dse-session-pick",
1503
+ children: t("sessionPick")
1504
+ })
1505
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1506
+ id: "dse-session-pick",
1507
+ className: "dse-input",
1508
+ value: current,
1509
+ onChange: (event) => setPicked(event.target.value),
1510
+ children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1511
+ value: row.id,
1512
+ children: row.displayTitle
1513
+ }, row.id))
1514
+ })]
1515
+ }), current !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionSecretsPanel, {
1516
+ sessionId: current,
1517
+ t
1518
+ }, current) : null] })
1519
+ })] });
1520
+ }
1000
1521
  function SecretsSection(props) {
1001
1522
  const { t } = props;
1002
1523
  const [data, setData] = (0, react.useState)(null);
@@ -1042,7 +1563,8 @@ ${extra}
1042
1563
  children: t("retry")
1043
1564
  }) : null]
1044
1565
  });
1045
- const canSave = form.name.trim() !== "" && form.value !== "" && !saving;
1566
+ const nameError = nameErrorOf(form.name);
1567
+ const canSave = form.name.trim() !== "" && nameError === null && form.value !== "" && !saving;
1046
1568
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1047
1569
  className: "dse-section",
1048
1570
  children: [
@@ -1075,6 +1597,29 @@ ${extra}
1075
1597
  })
1076
1598
  }, entry.name))
1077
1599
  }),
1600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1601
+ className: "dse-groupLabel",
1602
+ children: t("inheritedList")
1603
+ }),
1604
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1605
+ className: "dse-rows",
1606
+ children: data.inherited.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1607
+ className: "dse-empty",
1608
+ children: t("inheritedEmpty")
1609
+ }) : data.inherited.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InheritedRow, {
1610
+ t,
1611
+ entry,
1612
+ onChanged: load,
1613
+ onError: (text) => setStatus({
1614
+ kind: "err",
1615
+ text
1616
+ })
1617
+ }, entry.name))
1618
+ }),
1619
+ props.sessions !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionManageBlock, {
1620
+ t,
1621
+ sessions: props.sessions
1622
+ }) : null,
1078
1623
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1079
1624
  className: "dse-groupLabel",
1080
1625
  children: t("addGlobal")
@@ -1097,17 +1642,18 @@ ${extra}
1097
1642
  }),
1098
1643
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1099
1644
  id: "dse-name",
1100
- className: "dse-input",
1645
+ className: `dse-input${nameError !== null ? " dse-inputError" : ""}`,
1101
1646
  value: form.name,
1102
1647
  placeholder: "GITHUB_TOKEN",
1648
+ "aria-invalid": nameError !== null,
1103
1649
  onChange: (event) => setForm({
1104
1650
  ...form,
1105
- name: event.target.value
1651
+ name: liveName(event.target.value)
1106
1652
  })
1107
1653
  }),
1108
1654
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1109
- className: "dse-hint",
1110
- children: t("nameHint")
1655
+ className: `dse-hint${nameError !== null ? " dse-hintError" : ""}`,
1656
+ children: nameError !== null ? t("error.invalid-name") : t("nameHint")
1111
1657
  })
1112
1658
  ]
1113
1659
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1215,23 +1761,22 @@ ${extra}
1215
1761
  .dse-delBtn:hover{color:var(--dsw-alias-label-error)}
1216
1762
  .dse-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;padding:18px 14px;margin:0;background:var(--dsw-alias-bg-layer-3)}
1217
1763
  .dse-form{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);padding:4px 14px 12px;margin-top:10px}
1218
- .dse-formGrid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
1219
- /* 会话注入控件(composer 工具行胶囊,对齐官方 chip 语言) */
1220
- .dse-wrap{position:relative;align-items:center;display:inline-flex}
1221
- .dse-chip{background:var(--dsw-alias-bg-module-platform);min-width:34px;color:var(--dsw-alias-label-secondary);cursor:pointer;border:none;border-radius:999px;align-items:center;gap:4px;padding:2px 8px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}
1222
- .dse-chip:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
1223
- .dse-chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
1224
- .dse-chip:disabled{opacity:.6;cursor:default}
1225
- .dse-count{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-base);border-radius:999px;min-width:16px;text-align:center;font-size:11px;line-height:16px;padding:0 4px}
1226
- .dse-pop{position:absolute;bottom:calc(100% + 8px);right:0;z-index:60;width:320px;max-width:calc(100vw - 24px);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.18);padding:10px 12px 12px}
1764
+ .dse-inputError{border-color:var(--dsw-alias-label-error)}
1765
+ .dse-hintError{color:var(--dsw-alias-label-error)}
1766
+ /* 会话变量面板(/var 命令唤起,overlay 槽浮层)与内嵌共用件 */
1767
+ .dse-panelWrap{position:relative}
1768
+ .dse-panelCard{position:absolute;bottom:calc(100% + 6px);left:0;right:0;z-index:60;max-height:min(60vh,560px);overflow:auto;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.18);padding:10px 12px 12px}
1769
+ .dse-maskBtnOn{color:var(--dsw-alias-label-error)}
1770
+ .dse-envNameDim{opacity:.55;text-decoration:line-through}
1227
1771
  .dse-popHead{align-items:center;gap:8px;display:flex;padding:2px 0 8px}
1228
1772
  .dse-popTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;flex:1;margin:0}
1229
1773
  .dse-popHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0 0 8px}
1230
- .dse-popList{flex-direction:column;display:flex;max-height:220px;overflow:auto;margin:0 -4px;padding:0 4px}
1774
+ .dse-popList{flex-direction:column;display:flex;margin:0 -4px;padding:0 4px}
1231
1775
  .dse-popList .dse-row{background:0 0;border-radius:8px;padding:8px 8px}
1232
1776
  .dse-popList .dse-row+.dse-row{border-top:1px solid var(--dsw-alias-border-l2)}
1233
1777
  .dse-popForm{border-top:1px solid var(--dsw-alias-border-l2);margin-top:8px;padding-top:4px}
1234
1778
  .dse-foot{justify-content:flex-end;gap:8px;display:flex;padding-top:8px}
1779
+ .dse-formGrid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
1235
1780
  /* $ 触发补全菜单(仿官方 input-trigger MenuView 的浮层语言) */
1236
1781
  .dse-menuWrap{position:relative}
1237
1782
  .dse-menu{position:absolute;bottom:calc(100% + 6px);left:8px;z-index:60;min-width:280px;max-width:min(420px,calc(100vw - 32px));max-height:320px;overflow:auto;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 8px 28px rgba(0,0,0,.18);padding:4px}
@@ -1252,11 +1797,9 @@ ${extra}
1252
1797
  .dse-row{flex-wrap:wrap;padding:12px 12px}
1253
1798
  .dse-rowMain{flex-basis:100%}
1254
1799
  .dse-badges{margin-left:auto}
1255
- .dse-pop{position:fixed;left:8px;right:8px;bottom:8px;top:auto;width:auto;max-width:none;max-height:70vh;overflow:auto}
1256
- .dse-chip{min-height:32px}
1800
+ .dse-panelCard{position:fixed;left:8px;right:8px;bottom:8px;top:auto;max-height:70vh}
1257
1801
  }
1258
1802
  @media (pointer:coarse){
1259
- .dse-chip{min-height:32px}
1260
1803
  .dse-iconBtn{min-width:44px;min-height:44px;align-items:center;justify-content:center}
1261
1804
  }
1262
1805
  `;
@@ -1289,15 +1832,12 @@ ${extra}
1289
1832
  id: "dsh-plus-secret-env",
1290
1833
  order: 16,
1291
1834
  label: () => t("nav"),
1292
- inject: () => ({ t })
1835
+ inject: () => ({
1836
+ t,
1837
+ sessions: c.sessions
1838
+ })
1293
1839
  }, SecretsSection));
1294
- c.slots.inject("conversation.input.right", () => c.slots.register({
1295
- name: "conversation.input.right",
1296
- id: "dsh-plus-secret-env",
1297
- order: 40,
1298
- locale: NS,
1299
- inject: (sessionId) => ({ sessionId })
1300
- }, ComposerSecret));
1840
+ registerVarCommand(c, c.sessions, t);
1301
1841
  c.slots.inject("conversation.input.overlay", () => c.slots.register({
1302
1842
  name: "conversation.input.overlay",
1303
1843
  id: "dse-secret-menu",
@@ -1319,6 +1859,13 @@ ${extra}
1319
1859
  }
1320
1860
  })
1321
1861
  }, SecretMenu));
1862
+ c.slots.inject("conversation.input.overlay", () => c.slots.register({
1863
+ name: "conversation.input.overlay",
1864
+ id: "dse-secret-panel",
1865
+ order: 20,
1866
+ locale: NS,
1867
+ inject: (sessionId) => ({ sessionId })
1868
+ }, SessionPanelHost));
1322
1869
  }
1323
1870
  //#endregion
1324
1871
  exports.apply = apply;