@dsh-plus/secret-env 0.1.0 → 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,10 +578,339 @@ ${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
+ }
589
+ //#endregion
590
+ //#region src/client/menu-core.ts
591
+ const WORD_CHAR = /[\p{L}\p{N}_]/u;
592
+ const TOKEN_CHAR = /^[A-Za-z0-9_]$/;
593
+ /**
594
+ * 检测光标处的 `$` 触发令牌。词法:
595
+ * - 从光标向左扫描,token 字符(字母/数字/下划线)继续,空白直接判负;
596
+ * - 遇到 `$` 时校验词边界:起草开头 / 前字符为空白 / 前字符为非词字符(标点)
597
+ * 才开闸——`foo$BAR` 这类词中 $ 不触发(与官方 URL 内 / 不触发同理);
598
+ * - 其余字符(如 `{`、`(`)终止扫描。
599
+ */
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;
620
+ }
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
+ }
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);
651
+ }
652
+ //#endregion
653
+ //#region src/client/menu.tsx
654
+ /**
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
662
+ */
663
+ const FALLBACK_INPUT = {
664
+ draft: "",
665
+ draftRev: 0,
666
+ phase: "plain",
667
+ occurrences: []
668
+ };
669
+ /** 本组件容器向上找到 composer 卡片与 Lexical contenteditable 根。 */
670
+ function editorRootOf(el) {
671
+ const root = (el?.closest("[data-composer-card]"))?.querySelector("[contenteditable=\"true\"]");
672
+ return root instanceof HTMLElement ? root : null;
673
+ }
674
+ /**
675
+ * 光标的草稿投影偏移:Range 求光标前渲染文本长,再按光标前的引用芯片
676
+ * (data-composer-chip,DOM 序与 occurrences 的 offset 序一致)修正长度差。
677
+ */
678
+ function caretDraftOffset(root, occurrences) {
679
+ const sel = window.getSelection();
680
+ if (sel === null || sel.rangeCount === 0 || !sel.isCollapsed) return null;
681
+ const range = sel.getRangeAt(0);
682
+ if (!root.contains(range.startContainer)) return null;
683
+ const pre = document.createRange();
684
+ pre.selectNodeContents(root);
685
+ pre.setEnd(range.startContainer, range.startOffset);
686
+ const rendered = pre.toString().length;
687
+ const corrections = [];
688
+ const chips = root.querySelectorAll("[data-composer-chip]");
689
+ for (let i = 0; i < chips.length; i++) {
690
+ const el = chips[i];
691
+ if (range.comparePoint(el, el.childNodes.length) > 0) continue;
692
+ const occurrence = occurrences[i];
693
+ if (occurrence === void 0) continue;
694
+ corrections.push({
695
+ rendered: el.textContent?.length ?? 0,
696
+ draft: occurrence.length
697
+ });
698
+ }
699
+ return applyChipCorrection(rendered, corrections);
700
+ }
701
+ function toCandidates(list) {
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
+ ];
723
+ }
724
+ function SecretMenu(props) {
725
+ const { sessionId, t, insertToken } = props;
726
+ const input = (props.useInput ?? ((_selector) => FALLBACK_INPUT))((state) => state);
727
+ const [hit, setHit] = (0, react.useState)(null);
728
+ const [items, setItems] = (0, react.useState)(null);
729
+ const [highlight, setHighlight] = (0, react.useState)(0);
730
+ const wrapRef = (0, react.useRef)(null);
731
+ const composingRef = (0, react.useRef)(false);
732
+ /** Esc 抑制:记录被关闭的令牌,token 变化前不重复弹出。 */
733
+ const suppressedRef = (0, react.useRef)(null);
734
+ /** 候选拉取(ref 稳定;挂载、会话切换与菜单展开沿调用)。 */
735
+ const reloadRef = (0, react.useRef)((_sid) => {});
736
+ reloadRef.current = (sid) => {
737
+ fetchSecrets(sid).then((list) => setItems(toCandidates(list))).catch(() => setItems([]));
738
+ };
739
+ /** 上一次检测的展开态(展开沿判定用)。 */
740
+ const wasOpenRef = (0, react.useRef)(false);
741
+ (0, react.useEffect)(() => {
742
+ reloadRef.current(sessionId);
743
+ }, [sessionId]);
744
+ (0, react.useEffect)(() => {
745
+ const update = () => {
746
+ const root = editorRootOf(wrapRef.current);
747
+ if (root === null || composingRef.current || input.phase !== "plain") {
748
+ setHit((prev) => prev === null ? prev : null);
749
+ wasOpenRef.current = false;
750
+ return;
751
+ }
752
+ const caret = caretDraftOffset(root, input.occurrences);
753
+ const next = caret === null ? null : detectSecretTrigger(input.draft, caret);
754
+ const suppressed = suppressedRef.current;
755
+ const live = next !== null && suppressed !== null && next.start === suppressed.start && next.query === suppressed.query ? null : next;
756
+ if (live !== null && !wasOpenRef.current) reloadRef.current(sessionId);
757
+ wasOpenRef.current = live !== null;
758
+ setHit((prev) => sameHit(prev, live) ? prev : live);
759
+ };
760
+ update();
761
+ document.addEventListener("selectionchange", update);
762
+ return () => document.removeEventListener("selectionchange", update);
763
+ }, [input, sessionId]);
764
+ const candidates = hit === null || items === null ? [] : filterCandidates(items, hit.query);
765
+ const open = hit !== null && candidates.length > 0;
766
+ (0, react.useEffect)(() => {
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
+ }
226
909
  //#endregion
227
910
  //#region src/client/common.ts
228
911
  /**
229
- * 浏览器半共享小件:错误码文案映射与复制助手(设置页与会话控件共用)。
912
+ * 浏览器半共享小件:错误码文案映射、复制助手与变量名输入规则
913
+ * (设置页与会话控件共用;名称规则与 node 半 names.ts 同源)。
230
914
  * @module secret-env/client/common
231
915
  */
232
916
  /** 端点错误 → 本地化文案(未知码回落 internal)。 */
@@ -235,6 +919,15 @@ ${extra}
235
919
  const text = t(`error.${code}`);
236
920
  return text === `error.${code}` ? t("error.internal") : text;
237
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
+ }
238
931
  /** 复制文本到剪贴板(clipboard API 缺席时退 execCommand)。 */
239
932
  async function copyText(text) {
240
933
  try {
@@ -253,13 +946,13 @@ ${extra}
253
946
  }
254
947
  }
255
948
  //#endregion
256
- //#region src/client/composer.tsx
949
+ //#region src/client/panel.tsx
257
950
  /**
258
- * 会话注入控件(conversation.input.right 官方插槽,session 作用域):
259
- * composer 工具行的钥匙胶囊,弹出本会话的会话级密钥面板(列表 + 增删)。
260
- * 会话 id 由插槽 inject 工厂直接供给(框架解析的严格 session 作用域)。
261
- * 窄屏(≤767px)popover 转底部抽屉;桌面点外部或 Esc 关闭。
262
- * @module secret-env/client/composer
951
+ * 会话密钥面板(复用件):会话级密钥的列表/增删 + 全局与继承变量的
952
+ * 会话内屏蔽开关。两个宿主共用——overlay 槽的面板宿主(/secret 命令唤起,
953
+ * onClose)与设置页的会话管理区(选择会话后内嵌,无头栏)。
954
+ * 值只经同源端点下行,绝不回显。
955
+ * @module secret-env/client/panel
263
956
  */
264
957
  const EMPTY_FORM$1 = {
265
958
  name: "",
@@ -267,13 +960,45 @@ ${extra}
267
960
  description: "",
268
961
  once: false
269
962
  };
270
- function SessionRow(props) {
271
- const { t, entry } = props;
963
+ /** 复制按钮(复制成功短暂亮勾)。 */
964
+ function CopyButton$1(props) {
965
+ const { t } = props;
272
966
  const [copied, setCopied] = (0, react.useState)(false);
273
967
  const timer = (0, react.useRef)(null);
274
968
  (0, react.useEffect)(() => () => {
275
969
  if (timer.current !== null) clearTimeout(timer.current);
276
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;
277
1002
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
278
1003
  className: "dse-row",
279
1004
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -283,20 +1008,9 @@ ${extra}
283
1008
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
284
1009
  className: "dse-envName",
285
1010
  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 ? "✓" : "⧉"
1011
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton$1, {
1012
+ t,
1013
+ text: `$${entry.envName}`
300
1014
  })]
301
1015
  }), entry.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
302
1016
  className: "dse-note",
@@ -313,43 +1027,96 @@ ${extra}
313
1027
  title: t("delete"),
314
1028
  "aria-label": t("delete"),
315
1029
  onClick: () => {
316
- unsetSession(props.sessionId, entry.name).then(() => props.onDeleted()).catch((error) => props.onError(errorText(t, error)));
1030
+ unsetSession(props.sessionId, entry.name).then(() => props.onChanged()).catch((error) => props.onError(errorText(t, error)));
317
1031
  },
318
- children: "✕"
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
+ }
319
1067
  })]
320
1068
  })]
321
1069
  });
322
1070
  }
323
- function ComposerSecret(props) {
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) {
324
1109
  const { sessionId, t } = props;
325
- const [open, setOpen] = (0, react.useState)(false);
326
- const [items, setItems] = (0, react.useState)(null);
1110
+ const [data, setData] = (0, react.useState)(null);
327
1111
  const [form, setForm] = (0, react.useState)(EMPTY_FORM$1);
328
1112
  const [saving, setSaving] = (0, react.useState)(false);
329
1113
  const [status, setStatus] = (0, react.useState)(null);
330
- const wrapRef = (0, react.useRef)(null);
331
1114
  const load = () => {
332
- fetchSecrets(sessionId).then((list) => setItems(list.session)).catch(() => setStatus(errorText(t, /* @__PURE__ */ new Error("internal"))));
1115
+ fetchSecrets(sessionId).then((list) => setData(list)).catch(() => setStatus(errorText(t, /* @__PURE__ */ new Error("internal"))));
333
1116
  };
334
1117
  (0, react.useEffect)(() => {
335
- if (!open) return;
336
1118
  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]);
1119
+ }, [sessionId]);
353
1120
  const onSave = () => {
354
1121
  setSaving(true);
355
1122
  setStatus(null);
@@ -358,265 +1125,228 @@ ${extra}
358
1125
  load();
359
1126
  }).catch((error) => setStatus(errorText(t, error))).finally(() => setSaving(false));
360
1127
  };
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",
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",
394
1188
  children: [
395
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
396
- className: "dse-popTitle",
397
- children: t("sessionSecrets")
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
+ })
398
1196
  }),
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: "⟳"
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
+ })
406
1207
  }),
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,
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,
472
1240
  onChange: (event) => setForm({
473
1241
  ...form,
474
- description: event.target.value
1242
+ once: event.target.checked
475
1243
  })
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]
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
+ ]
511
1288
  });
512
1289
  }
513
1290
  //#endregion
514
- //#region src/client/i18n.ts
1291
+ //#region src/client/panel-host.tsx
515
1292
  /**
516
- * 浏览器半文案(zh/en)。公共键来自 shared common,本文件只维护业务键。
517
- * @module secret-env/client/i18n
1293
+ * 会话变量面板的 overlay 宿主(conversation.input.overlay 官方插槽,
1294
+ * session 作用域):常驻监听打开总线,/var 命令选中后弹出面板。
1295
+ * 弹层定位于 composer 卡片上方(与 $ 菜单同一浮层语言);
1296
+ * 点外部或 Esc 关闭。闭合态不渲染面板(数据在打开时现取)。
1297
+ * @module secret-env/client/panel-host
518
1298
  */
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
- "error.invalid-name": "变量名不合法:大写字母开头,仅字母/数字/下划线,≤64 字符。",
559
- "error.empty-value": "值不能为空。",
560
- "error.shadowed": "进程环境中已存在同名变量且优先级更高,写入无效;请先取消该环境变量。",
561
- "error.conflict": "变量名与其他插件注册的受管变量冲突。",
562
- "error.no-session": "缺少会话标识,请刷新后重试。",
563
- "error.method": "请求方法不允许。",
564
- "error.internal": "服务内部错误,请查看宿主日志。"
565
- };
566
- const ownEn = {
567
- nav: "Secret Variables",
568
- title: "Secret Variables",
569
- 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.",
570
- globalList: "Global secrets",
571
- colName: "Variable (model-visible)",
572
- colDescription: "Description",
573
- colState: "State",
574
- colActions: "Actions",
575
- configured: "Configured",
576
- unconfigured: "Not configured",
577
- readonly: "Read-only source",
578
- addGlobal: "Add global secret",
579
- nameLabel: "Name suffix",
580
- nameHint: "Uppercase letters/digits/underscore, e.g. GITHUB_TOKEN; the model uses the full name with the $DSH_SECRET_ prefix.",
581
- valueLabel: "Value",
582
- valueHint: "Write-only after saving; stored in the host credential store, never in chat.",
583
- descLabel: "Description (optional)",
584
- descHint: "Human-readable note only; never enters prompts.",
585
- save: "Save",
586
- saving: "Saving…",
587
- delete: "Delete",
588
- deleteConfirm: "Delete {name}? The variable stops working for subsequent commands immediately.",
589
- copy: "Copy variable name",
590
- copied: "Copied",
591
- emptyGlobal: "No global secrets yet. Once added, the agent can reference them by name.",
592
- loadFailed: "Failed to load. Try again later.",
593
- retry: "Retry",
594
- sessionSecrets: "Session secrets",
595
- sessionEmpty: "No session secrets yet.",
596
- sessionHint: "Available to shell commands of this session only; expires when the session ends or after one use.",
597
- addSession: "Add session secret",
598
- onceLabel: "One-time (destroyed after first use)",
599
- close: "Close",
600
- refresh: "Refresh",
601
- scopeGlobal: "global",
602
- scopeSession: "session",
603
- scopeOnce: "once",
604
- "error.invalid-name": "Invalid name: start with an uppercase letter; letters/digits/underscore only, ≤64 chars.",
605
- "error.empty-value": "Value must not be empty.",
606
- "error.shadowed": "A same-named process environment variable shadows this write; unset it first.",
607
- "error.conflict": "The variable conflicts with a managed variable registered by another plugin.",
608
- "error.no-session": "Missing session id; refresh and retry.",
609
- "error.method": "Method not allowed.",
610
- "error.internal": "Internal error; check the host log."
611
- };
612
- const zh = mergeDict(commonZh, ownZh);
613
- const en = mergeDict(commonEn, ownEn);
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]);
1306
+ (0, react.useEffect)(() => {
1307
+ if (!open) return;
1308
+ const onKey = (event) => {
1309
+ if (event.key !== "Escape") return;
1310
+ event.preventDefault();
1311
+ event.stopPropagation();
1312
+ setOpen(false);
1313
+ };
1314
+ const onDown = (event) => {
1315
+ if (!(event.target instanceof Node)) return;
1316
+ if (wrapRef.current?.contains(event.target) === true) return;
1317
+ setOpen(false);
1318
+ };
1319
+ document.addEventListener("keydown", onKey, true);
1320
+ document.addEventListener("pointerdown", onDown, true);
1321
+ return () => {
1322
+ document.removeEventListener("keydown", onKey, true);
1323
+ document.removeEventListener("pointerdown", onDown, true);
1324
+ };
1325
+ }, [open]);
1326
+ if (!open) return null;
1327
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1328
+ className: "dse-panelWrap",
1329
+ ref: wrapRef,
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
+ })
1339
+ })
1340
+ });
1341
+ }
614
1342
  //#endregion
615
1343
  //#region src/client/section.tsx
616
1344
  /**
617
- * 密钥变量设置页(settings.section 官方插槽):全局密钥的列表与增删。
618
- * 只展示元数据与模型可见的完整变量名——值写入后不可回读(端点不出值)。
619
- * 响应式:≤767px 行转堆叠卡、表单转单列。
1345
+ * 环境变量设置页(settings.section 官方插槽):
1346
+ * - 全局变量:列表与增删(值写入后不可回读,端点不出值);
1347
+ * - 继承变量:识别宿主进程环境的 DSH_VAR_*(默认纳入注入),可全局屏蔽;
1348
+ * - 会话变量:经会话选择器内嵌会话面板(会话级/一次性变量的增删与屏蔽)。
1349
+ * 只展示元数据与模型可见的完整变量名。响应式:≤767px 行转堆叠卡、表单单列。
620
1350
  * @module secret-env/client/section
621
1351
  */
622
1352
  const EMPTY_FORM = {
@@ -624,23 +1354,34 @@ ${extra}
624
1354
  value: "",
625
1355
  description: ""
626
1356
  };
627
- /** 单行:完整变量名(复制)+ 描述 + 状态徽标 + 两步删除。 */
628
- function GlobalRow(props) {
629
- const { t, entry } = props;
630
- const [confirming, setConfirming] = (0, react.useState)(false);
1357
+ /** 复制按钮(复制成功短暂亮勾)。 */
1358
+ function CopyButton(props) {
1359
+ const { t } = props;
631
1360
  const [copied, setCopied] = (0, react.useState)(false);
632
1361
  const timer = (0, react.useRef)(null);
633
1362
  (0, react.useEffect)(() => () => {
634
1363
  if (timer.current !== null) clearTimeout(timer.current);
635
1364
  }, []);
636
- const onCopy = () => {
637
- copyText(`$${entry.envName}`).then((ok) => {
638
- if (!ok) return;
639
- setCopied(true);
640
- if (timer.current !== null) clearTimeout(timer.current);
641
- timer.current = setTimeout(() => setCopied(false), 1500);
642
- });
643
- };
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);
644
1385
  const onDelete = () => {
645
1386
  unsetGlobal(entry.name).then(() => props.onDeleted()).catch((error) => props.onError(errorText(t, error)));
646
1387
  };
@@ -653,13 +1394,9 @@ ${extra}
653
1394
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
654
1395
  className: "dse-envName",
655
1396
  children: ["$", entry.envName]
656
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
657
- type: "button",
658
- className: "dse-iconBtn",
659
- title: t("copy"),
660
- "aria-label": t("copy"),
661
- onClick: onCopy,
662
- children: copied ? "✓" : "⧉"
1397
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
1398
+ t,
1399
+ text: `$${entry.envName}`
663
1400
  })]
664
1401
  }), entry.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
665
1402
  className: "dse-note",
@@ -693,12 +1430,94 @@ ${extra}
693
1430
  title: t("delete"),
694
1431
  "aria-label": t("delete"),
695
1432
  onClick: () => setConfirming(true),
696
- children: "✕"
1433
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
697
1434
  })
698
1435
  ]
699
1436
  })]
700
1437
  });
701
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
+ }
702
1521
  function SecretsSection(props) {
703
1522
  const { t } = props;
704
1523
  const [data, setData] = (0, react.useState)(null);
@@ -744,7 +1563,8 @@ ${extra}
744
1563
  children: t("retry")
745
1564
  }) : null]
746
1565
  });
747
- 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;
748
1568
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
749
1569
  className: "dse-section",
750
1570
  children: [
@@ -777,6 +1597,29 @@ ${extra}
777
1597
  })
778
1598
  }, entry.name))
779
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,
780
1623
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
781
1624
  className: "dse-groupLabel",
782
1625
  children: t("addGlobal")
@@ -799,17 +1642,18 @@ ${extra}
799
1642
  }),
800
1643
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
801
1644
  id: "dse-name",
802
- className: "dse-input",
1645
+ className: `dse-input${nameError !== null ? " dse-inputError" : ""}`,
803
1646
  value: form.name,
804
1647
  placeholder: "GITHUB_TOKEN",
1648
+ "aria-invalid": nameError !== null,
805
1649
  onChange: (event) => setForm({
806
1650
  ...form,
807
- name: event.target.value
1651
+ name: liveName(event.target.value)
808
1652
  })
809
1653
  }),
810
1654
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
811
- className: "dse-hint",
812
- children: t("nameHint")
1655
+ className: `dse-hint${nameError !== null ? " dse-hintError" : ""}`,
1656
+ children: nameError !== null ? t("error.invalid-name") : t("nameHint")
813
1657
  })
814
1658
  ]
815
1659
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -917,34 +1761,45 @@ ${extra}
917
1761
  .dse-delBtn:hover{color:var(--dsw-alias-label-error)}
918
1762
  .dse-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;padding:18px 14px;margin:0;background:var(--dsw-alias-bg-layer-3)}
919
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}
920
- .dse-formGrid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
921
- /* 会话注入控件(composer 工具行胶囊,对齐官方 chip 语言) */
922
- .dse-wrap{position:relative;align-items:center;display:inline-flex}
923
- .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}
924
- .dse-chip:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
925
- .dse-chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
926
- .dse-chip:disabled{opacity:.6;cursor:default}
927
- .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}
928
- .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}
929
1771
  .dse-popHead{align-items:center;gap:8px;display:flex;padding:2px 0 8px}
930
1772
  .dse-popTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;flex:1;margin:0}
931
1773
  .dse-popHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0 0 8px}
932
- .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}
933
1775
  .dse-popList .dse-row{background:0 0;border-radius:8px;padding:8px 8px}
934
1776
  .dse-popList .dse-row+.dse-row{border-top:1px solid var(--dsw-alias-border-l2)}
935
1777
  .dse-popForm{border-top:1px solid var(--dsw-alias-border-l2);margin-top:8px;padding-top:4px}
936
1778
  .dse-foot{justify-content:flex-end;gap:8px;display:flex;padding-top:8px}
937
- /* 响应式:窄屏表格转堆叠,popover 转底部抽屉 */
1779
+ .dse-formGrid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
1780
+ /* $ 触发补全菜单(仿官方 input-trigger MenuView 的浮层语言) */
1781
+ .dse-menuWrap{position:relative}
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}
1783
+ .dse-menuTitle{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;padding:6px 10px 4px}
1784
+ .dse-menuItem{appearance:none;border:none;background:0 0;cursor:pointer;align-items:center;gap:8px;width:100%;padding:7px 10px;border-radius:8px;display:flex;text-align:left}
1785
+ .dse-menuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}
1786
+ .dse-menuItemActive{background:var(--dsw-alias-interactive-bg-hover)}
1787
+ .dse-menuName{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-primary);font-size:13px;flex:none}
1788
+ .dse-menuDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1789
+ .dse-menuBadges{align-items:center;gap:4px;flex:none;display:flex}
1790
+ @media (max-width:767px){
1791
+ .dse-menu{left:0;right:0;min-width:0;max-width:none;max-height:45vh}
1792
+ .dse-menuItem{min-height:44px}
1793
+ }
1794
+
938
1795
  @media (max-width:767px){
939
1796
  .dse-formGrid{grid-template-columns:1fr}
940
1797
  .dse-row{flex-wrap:wrap;padding:12px 12px}
941
1798
  .dse-rowMain{flex-basis:100%}
942
1799
  .dse-badges{margin-left:auto}
943
- .dse-pop{position:fixed;left:8px;right:8px;bottom:8px;top:auto;width:auto;max-width:none;max-height:70vh;overflow:auto}
944
- .dse-chip{min-height:32px}
1800
+ .dse-panelCard{position:fixed;left:8px;right:8px;bottom:8px;top:auto;max-height:70vh}
945
1801
  }
946
1802
  @media (pointer:coarse){
947
- .dse-chip{min-height:32px}
948
1803
  .dse-iconBtn{min-width:44px;min-height:44px;align-items:center;justify-content:center}
949
1804
  }
950
1805
  `;
@@ -956,7 +1811,11 @@ ${extra}
956
1811
  //#region src/client/client.ts
957
1812
  const name = "dsh-plus-secret-env";
958
1813
  /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
959
- const inject = ["slots", "locale"];
1814
+ const inject = [
1815
+ "slots",
1816
+ "locale",
1817
+ "sessions"
1818
+ ];
960
1819
  function apply(ctx) {
961
1820
  const c = ctx;
962
1821
  const styleTag = injectSecretEnvStyle("@dsh-plus/secret-env");
@@ -973,15 +1832,40 @@ ${extra}
973
1832
  id: "dsh-plus-secret-env",
974
1833
  order: 16,
975
1834
  label: () => t("nav"),
976
- inject: () => ({ t })
1835
+ inject: () => ({
1836
+ t,
1837
+ sessions: c.sessions
1838
+ })
977
1839
  }, SecretsSection));
978
- c.slots.inject("conversation.input.right", () => c.slots.register({
979
- name: "conversation.input.right",
980
- id: "dsh-plus-secret-env",
981
- order: 40,
1840
+ registerVarCommand(c, c.sessions, t);
1841
+ c.slots.inject("conversation.input.overlay", () => c.slots.register({
1842
+ name: "conversation.input.overlay",
1843
+ id: "dse-secret-menu",
1844
+ order: 10,
1845
+ locale: NS,
1846
+ inject: (sessionId) => ({
1847
+ sessionId,
1848
+ insertToken: (text, span) => {
1849
+ const actx = c.sessions.scope(sessionId);
1850
+ if (actx === void 0) return false;
1851
+ return actx.bail(actx, "slash/input-insert-text", {
1852
+ text,
1853
+ span: {
1854
+ start: span.start,
1855
+ end: span.end,
1856
+ draftRev: span.draftRev
1857
+ }
1858
+ }) === true;
1859
+ }
1860
+ })
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,
982
1866
  locale: NS,
983
1867
  inject: (sessionId) => ({ sessionId })
984
- }, ComposerSecret));
1868
+ }, SessionPanelHost));
985
1869
  }
986
1870
  //#endregion
987
1871
  exports.apply = apply;