@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 +1180 -633
- package/lib/index.d.ts +40 -8
- package/lib/index.js +211 -57
- package/package.json +6 -4
- package/src/api.ts +24 -1
- package/src/client/api.ts +18 -0
- package/src/client/client.ts +28 -21
- package/src/client/command.ts +62 -0
- package/src/client/common.ts +14 -1
- package/src/client/i18n.ts +66 -37
- package/src/client/menu-core.ts +2 -2
- package/src/client/menu.tsx +24 -8
- package/src/client/panel-bus.ts +23 -0
- package/src/client/panel-host.tsx +63 -0
- package/src/client/panel.tsx +385 -0
- package/src/client/section.tsx +194 -38
- package/src/client/styles.ts +10 -13
- package/src/config.ts +4 -0
- package/src/contributors.ts +115 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +101 -0
- package/src/names.ts +3 -3
- package/src/service.ts +107 -94
- package/src/client/composer.tsx +0 -260
package/src/client/client.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 浏览器半入口:
|
|
3
|
-
* - settings.section
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* - settings.section 官方插槽注册「环境变量」独立设置页
|
|
4
|
+
* (全局管理 + 继承变量屏蔽 + 会话变量管理);
|
|
5
|
+
* - conversation.input.overlay 注册两个 session 作用域浮层:
|
|
6
|
+
* `$` 触发补全菜单与 /var 命令唤起的会话变量面板;
|
|
7
|
+
* - commandUi 注册 /var 斜杠命令(官方 popupSelect 壳 → 打开总线 → 面板)。
|
|
6
8
|
* 数据均经同源 HTTP 端点(值不上行经任何对话通道)。
|
|
7
9
|
* @module @dsh-plus/secret-env/client
|
|
8
10
|
*/
|
|
9
11
|
import type { Context } from '@deepseek-ai/cordis'
|
|
10
12
|
|
|
11
|
-
import {
|
|
13
|
+
import { registerVarCommand } from './command.ts'
|
|
12
14
|
import { en, NS, zh } from './i18n.ts'
|
|
13
15
|
import { SecretMenu, type TokenSpanLike } from './menu.tsx'
|
|
14
|
-
import {
|
|
16
|
+
import { SessionPanelHost } from './panel-host.tsx'
|
|
17
|
+
import { SecretsSection, type SessionsListLike } from './section.tsx'
|
|
15
18
|
import { injectSecretEnvStyle } from './styles.ts'
|
|
16
19
|
|
|
17
20
|
export const name = 'dsh-plus-secret-env'
|
|
@@ -34,14 +37,16 @@ interface SessionScopeLike {
|
|
|
34
37
|
bail(subject: unknown, event: string, payload: Record<string, unknown>): unknown
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
interface SessionsLike {
|
|
40
|
+
interface SessionsLike extends SessionsListLike {
|
|
38
41
|
scope(sessionId: string): SessionScopeLike | undefined
|
|
42
|
+
subagentAddress?(sessionId: string): unknown
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
interface ClientContext {
|
|
42
46
|
slots: SlotsLike
|
|
43
47
|
locale: LocaleLike
|
|
44
48
|
sessions: SessionsLike
|
|
49
|
+
inject(keys: readonly string[], callback: (scope: never) => void): unknown
|
|
45
50
|
effect(execute: () => () => void, label?: string): unknown
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -65,26 +70,14 @@ export function apply(ctx: Context): void {
|
|
|
65
70
|
id: 'dsh-plus-secret-env',
|
|
66
71
|
order: 16,
|
|
67
72
|
label: () => t('nav'),
|
|
68
|
-
inject: () => ({ t }),
|
|
73
|
+
inject: () => ({ t, sessions: c.sessions }),
|
|
69
74
|
},
|
|
70
75
|
SecretsSection,
|
|
71
76
|
),
|
|
72
77
|
)
|
|
73
78
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
c.slots.inject('conversation.input.right', () =>
|
|
77
|
-
c.slots.register(
|
|
78
|
-
{
|
|
79
|
-
name: 'conversation.input.right',
|
|
80
|
-
id: 'dsh-plus-secret-env',
|
|
81
|
-
order: 40,
|
|
82
|
-
locale: NS,
|
|
83
|
-
inject: (sessionId: string) => ({ sessionId }),
|
|
84
|
-
},
|
|
85
|
-
ComposerSecret,
|
|
86
|
-
),
|
|
87
|
-
)
|
|
79
|
+
// `/var` 斜杠命令(commandUi 由 dsh-client-ui-commands 客户端半提供)。
|
|
80
|
+
registerVarCommand(c, c.sessions, t)
|
|
88
81
|
|
|
89
82
|
// `$` 触发补全菜单(conversation.input.overlay:composer 卡片内浮层)。
|
|
90
83
|
// 插入走官方会话作用域 bail 通道 slash/input-insert-text(span+draftRev CAS)。
|
|
@@ -112,4 +105,18 @@ export function apply(ctx: Context): void {
|
|
|
112
105
|
SecretMenu,
|
|
113
106
|
),
|
|
114
107
|
)
|
|
108
|
+
|
|
109
|
+
// `/var` 唤起的会话变量面板(同一 overlay 槽,监听打开总线)。
|
|
110
|
+
c.slots.inject('conversation.input.overlay', () =>
|
|
111
|
+
c.slots.register(
|
|
112
|
+
{
|
|
113
|
+
name: 'conversation.input.overlay',
|
|
114
|
+
id: 'dse-secret-panel',
|
|
115
|
+
order: 20,
|
|
116
|
+
locale: NS,
|
|
117
|
+
inject: (sessionId: string) => ({ sessionId }),
|
|
118
|
+
},
|
|
119
|
+
SessionPanelHost,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
115
122
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/var` 斜杠命令(dsh-client-ui-commands 的 commandUi 服务):
|
|
3
|
+
* contribution 仅支持官方 popupSelect 壳(kind 唯一),故命令提供单个
|
|
4
|
+
* 「打开面板」动作,选中后经打开总线唤起本会话的变量面板——
|
|
5
|
+
* 完全自定义的 React 面板只能在自己占有的 overlay 槽渲染。
|
|
6
|
+
* 子代理会话不提供本命令(对齐官方 /model 的可用性过滤)。
|
|
7
|
+
* @module secret-env/client/command
|
|
8
|
+
*/
|
|
9
|
+
import { requestOpenSessionPanel } from './panel-bus.ts'
|
|
10
|
+
|
|
11
|
+
/** commandUi 服务的结构子集(dsh-client-ui-commands CommandUiContract)。 */
|
|
12
|
+
interface CommandUiLike {
|
|
13
|
+
register(contribution: {
|
|
14
|
+
name: string
|
|
15
|
+
description: string
|
|
16
|
+
available(session: { sessionId: string }): boolean
|
|
17
|
+
ui: {
|
|
18
|
+
kind: 'popupSelect'
|
|
19
|
+
options(session: { sessionId: string }): Promise<readonly { id: string; label: string }[]>
|
|
20
|
+
onSelect(option: { id: string }, session: { sessionId: string }): void | Promise<void>
|
|
21
|
+
}
|
|
22
|
+
}): () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CommandScope {
|
|
26
|
+
commandUi: CommandUiLike
|
|
27
|
+
effect(execute: () => () => void, label?: string): unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CommandHostContext {
|
|
31
|
+
inject(keys: readonly string[], callback: (scope: CommandScope) => void): unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SessionsAvailabilityLike {
|
|
35
|
+
subagentAddress?(sessionId: string): unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 注册 /var 命令(commandUi 服务缺席时 inject 回调不触发,自然跳过)。 */
|
|
39
|
+
export function registerVarCommand(
|
|
40
|
+
ctx: CommandHostContext,
|
|
41
|
+
sessions: SessionsAvailabilityLike,
|
|
42
|
+
t: (key: string) => string,
|
|
43
|
+
): void {
|
|
44
|
+
ctx.inject(['commandUi'], (scope) => {
|
|
45
|
+
scope.effect(
|
|
46
|
+
() =>
|
|
47
|
+
scope.commandUi.register({
|
|
48
|
+
name: 'var',
|
|
49
|
+
description: t('command.description'),
|
|
50
|
+
available: (session) => sessions.subagentAddress?.(session.sessionId) === undefined,
|
|
51
|
+
ui: {
|
|
52
|
+
kind: 'popupSelect',
|
|
53
|
+
options: () => Promise.resolve([{ id: 'open', label: t('command.openPanel') }]),
|
|
54
|
+
onSelect: (_option, session) => {
|
|
55
|
+
requestOpenSessionPanel(session.sessionId)
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
'secret-env: /var contribution',
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
}
|
package/src/client/common.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 浏览器半共享小件:错误码文案映射、复制助手与变量名输入规则
|
|
3
|
+
* (设置页与会话控件共用;名称规则与 node 半 names.ts 同源)。
|
|
3
4
|
* @module secret-env/client/common
|
|
4
5
|
*/
|
|
6
|
+
import { type NameError, normalizeSuffix, validateSuffix } from '../names.ts'
|
|
5
7
|
import { ApiError } from './api.ts'
|
|
6
8
|
|
|
7
9
|
/** 端点错误 → 本地化文案(未知码回落 internal)。 */
|
|
@@ -12,6 +14,17 @@ export function errorText(t: (key: string) => string, error: unknown): string {
|
|
|
12
14
|
return text === `error.${code}` ? t('error.internal') : text
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/** 名称输入的实时规范化:键入即转大写(去空白交给保存时的 normalizeSuffix)。 */
|
|
18
|
+
export function liveName(raw: string): string {
|
|
19
|
+
return raw.toUpperCase()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 名称输入的实时校验:空串不标错(必填兜底),其余按 names.ts 规则判定。 */
|
|
23
|
+
export function nameErrorOf(raw: string): NameError | null {
|
|
24
|
+
if (raw.trim() === '') return null
|
|
25
|
+
return validateSuffix(normalizeSuffix(raw))
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/** 复制文本到剪贴板(clipboard API 缺席时退 execCommand)。 */
|
|
16
29
|
export async function copyText(text: string): Promise<boolean> {
|
|
17
30
|
try {
|
package/src/client/i18n.ts
CHANGED
|
@@ -7,22 +7,18 @@ import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
|
|
|
7
7
|
export const NS = 'dsh-plus-secret-env'
|
|
8
8
|
|
|
9
9
|
const ownZh = {
|
|
10
|
-
nav: '
|
|
11
|
-
title: '
|
|
10
|
+
nav: '环境变量',
|
|
11
|
+
title: '环境变量',
|
|
12
12
|
description:
|
|
13
|
-
'以 $
|
|
13
|
+
'以 $DSH_VAR_* 变量名向 agent 暴露值(密钥、链接等):值在每次 shell 调用时由宿主注入,不进入对话记录与提示词,不影响上下文缓存。',
|
|
14
14
|
// 设置页(全局)
|
|
15
|
-
globalList: '
|
|
16
|
-
colName: '变量名(模型可见)',
|
|
17
|
-
colDescription: '描述',
|
|
18
|
-
colState: '状态',
|
|
19
|
-
colActions: '操作',
|
|
15
|
+
globalList: '全局变量',
|
|
20
16
|
configured: '已配置',
|
|
21
17
|
unconfigured: '未配置',
|
|
22
18
|
readonly: '只读来源',
|
|
23
|
-
addGlobal: '
|
|
24
|
-
nameLabel: '
|
|
25
|
-
nameHint: '大写字母/数字/下划线,如 GITHUB_TOKEN;模型实际使用 $
|
|
19
|
+
addGlobal: '添加全局变量',
|
|
20
|
+
nameLabel: '变量名',
|
|
21
|
+
nameHint: '大写字母/数字/下划线,如 GITHUB_TOKEN;模型实际使用 $DSH_VAR_ 前缀拼接后的完整名。',
|
|
26
22
|
valueLabel: '值',
|
|
27
23
|
valueHint: '写入后不可回读;保存于宿主凭据库,不进入对话。',
|
|
28
24
|
descLabel: '描述(可选)',
|
|
@@ -30,26 +26,46 @@ const ownZh = {
|
|
|
30
26
|
save: '保存',
|
|
31
27
|
saving: '保存中…',
|
|
32
28
|
delete: '删除',
|
|
33
|
-
deleteConfirm: '确认删除 {name}?该变量将立即对后续命令失效。',
|
|
34
29
|
copy: '复制变量名',
|
|
35
30
|
copied: '已复制',
|
|
36
|
-
emptyGlobal: '
|
|
31
|
+
emptyGlobal: '暂无全局变量。添加后 agent 即可通过变量名引用。',
|
|
37
32
|
loadFailed: '加载失败,请稍后重试。',
|
|
38
33
|
retry: '重试',
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
// 继承变量
|
|
35
|
+
inheritedList: '继承变量(宿主环境)',
|
|
36
|
+
inheritedNote: '来自宿主进程环境,默认纳入注入',
|
|
37
|
+
inheritedEmpty: '宿主环境中没有可继承的 DSH_VAR_* 变量。',
|
|
38
|
+
maskGlobal: '全局屏蔽',
|
|
39
|
+
unmaskGlobal: '恢复注入',
|
|
40
|
+
maskedBadge: '已屏蔽',
|
|
41
|
+
maskedGlobalBadge: '已全局屏蔽',
|
|
42
|
+
// 设置页会话管理
|
|
43
|
+
sessionManage: '会话变量',
|
|
44
|
+
sessionPick: '选择会话',
|
|
45
|
+
sessionPickEmpty: '暂无会话',
|
|
46
|
+
// 会话面板
|
|
47
|
+
sessionSecrets: '会话变量',
|
|
48
|
+
sessionList: '会话变量',
|
|
49
|
+
sessionEmpty: '本会话暂无变量。',
|
|
42
50
|
sessionHint: '仅当前会话的 shell 命令可用;会话结束或一次性使用即失效。',
|
|
43
|
-
addSession: '
|
|
51
|
+
addSession: '添加会话变量',
|
|
44
52
|
onceLabel: '一次性(首次使用后销毁)',
|
|
53
|
+
globalInSession: '全局变量(可在本会话屏蔽)',
|
|
54
|
+
inheritedInSession: '继承变量(可在本会话屏蔽)',
|
|
55
|
+
maskSession: '在本会话屏蔽',
|
|
56
|
+
unmaskSession: '在本会话恢复',
|
|
45
57
|
close: '关闭',
|
|
46
58
|
refresh: '刷新',
|
|
47
59
|
scopeGlobal: '全局',
|
|
48
60
|
scopeSession: '会话',
|
|
49
61
|
scopeOnce: '一次性',
|
|
62
|
+
scopeInherited: '继承',
|
|
50
63
|
// `$` 触发补全菜单
|
|
51
|
-
'menu.title': '
|
|
52
|
-
'menu.aria': '
|
|
64
|
+
'menu.title': '变量',
|
|
65
|
+
'menu.aria': '变量候选',
|
|
66
|
+
// /var 斜杠命令
|
|
67
|
+
'command.description': '管理本会话的环境变量(密钥、链接等)',
|
|
68
|
+
'command.openPanel': '打开会话变量面板',
|
|
53
69
|
// 错误码
|
|
54
70
|
'error.invalid-name': '变量名不合法:大写字母开头,仅字母/数字/下划线,≤64 字符。',
|
|
55
71
|
'error.empty-value': '值不能为空。',
|
|
@@ -61,22 +77,18 @@ const ownZh = {
|
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
const ownEn = {
|
|
64
|
-
nav: '
|
|
65
|
-
title: '
|
|
80
|
+
nav: 'Env Variables',
|
|
81
|
+
title: 'Env Variables',
|
|
66
82
|
description:
|
|
67
|
-
'Expose
|
|
68
|
-
globalList: 'Global
|
|
69
|
-
colName: 'Variable (model-visible)',
|
|
70
|
-
colDescription: 'Description',
|
|
71
|
-
colState: 'State',
|
|
72
|
-
colActions: 'Actions',
|
|
83
|
+
'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.',
|
|
84
|
+
globalList: 'Global variables',
|
|
73
85
|
configured: 'Configured',
|
|
74
86
|
unconfigured: 'Not configured',
|
|
75
87
|
readonly: 'Read-only source',
|
|
76
|
-
addGlobal: 'Add global
|
|
77
|
-
nameLabel: '
|
|
88
|
+
addGlobal: 'Add global variable',
|
|
89
|
+
nameLabel: 'Variable name',
|
|
78
90
|
nameHint:
|
|
79
|
-
'Uppercase letters/digits/underscore, e.g. GITHUB_TOKEN; the model uses the full name with the $
|
|
91
|
+
'Uppercase letters/digits/underscore, e.g. GITHUB_TOKEN; the model uses the full name with the $DSH_VAR_ prefix.',
|
|
80
92
|
valueLabel: 'Value',
|
|
81
93
|
valueHint: 'Write-only after saving; stored in the host credential store, never in chat.',
|
|
82
94
|
descLabel: 'Description (optional)',
|
|
@@ -84,25 +96,42 @@ const ownEn = {
|
|
|
84
96
|
save: 'Save',
|
|
85
97
|
saving: 'Saving…',
|
|
86
98
|
delete: 'Delete',
|
|
87
|
-
deleteConfirm: 'Delete {name}? The variable stops working for subsequent commands immediately.',
|
|
88
99
|
copy: 'Copy variable name',
|
|
89
100
|
copied: 'Copied',
|
|
90
|
-
emptyGlobal: 'No global
|
|
101
|
+
emptyGlobal: 'No global variables yet. Once added, the agent can reference them by name.',
|
|
91
102
|
loadFailed: 'Failed to load. Try again later.',
|
|
92
103
|
retry: 'Retry',
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
inheritedList: 'Inherited variables (host env)',
|
|
105
|
+
inheritedNote: 'From the host process environment; injected by default',
|
|
106
|
+
inheritedEmpty: 'No inheritable DSH_VAR_* variables in the host environment.',
|
|
107
|
+
maskGlobal: 'Mask globally',
|
|
108
|
+
unmaskGlobal: 'Resume injection',
|
|
109
|
+
maskedBadge: 'Masked',
|
|
110
|
+
maskedGlobalBadge: 'Masked globally',
|
|
111
|
+
sessionManage: 'Session variables',
|
|
112
|
+
sessionPick: 'Select session',
|
|
113
|
+
sessionPickEmpty: 'No sessions',
|
|
114
|
+
sessionSecrets: 'Session variables',
|
|
115
|
+
sessionList: 'Session variables',
|
|
116
|
+
sessionEmpty: 'No session variables yet.',
|
|
95
117
|
sessionHint:
|
|
96
118
|
'Available to shell commands of this session only; expires when the session ends or after one use.',
|
|
97
|
-
addSession: 'Add session
|
|
119
|
+
addSession: 'Add session variable',
|
|
98
120
|
onceLabel: 'One-time (destroyed after first use)',
|
|
121
|
+
globalInSession: 'Global variables (can be masked in this session)',
|
|
122
|
+
inheritedInSession: 'Inherited variables (can be masked in this session)',
|
|
123
|
+
maskSession: 'Mask in this session',
|
|
124
|
+
unmaskSession: 'Resume in this session',
|
|
99
125
|
close: 'Close',
|
|
100
126
|
refresh: 'Refresh',
|
|
101
127
|
scopeGlobal: 'global',
|
|
102
128
|
scopeSession: 'session',
|
|
103
129
|
scopeOnce: 'once',
|
|
104
|
-
|
|
105
|
-
'menu.
|
|
130
|
+
scopeInherited: 'inherited',
|
|
131
|
+
'menu.title': 'Variables',
|
|
132
|
+
'menu.aria': 'Variable suggestions',
|
|
133
|
+
'command.description': "Manage this session's env variables (keys, links, etc.)",
|
|
134
|
+
'command.openPanel': 'Open the session variable panel',
|
|
106
135
|
'error.invalid-name':
|
|
107
136
|
'Invalid name: start with an uppercase letter; letters/digits/underscore only, ≤64 chars.',
|
|
108
137
|
'error.empty-value': 'Value must not be empty.',
|
package/src/client/menu-core.ts
CHANGED
|
@@ -14,12 +14,12 @@ export interface SecretHit {
|
|
|
14
14
|
readonly end: number
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/** 菜单候选项(全局/会话/继承密钥的统一视图,纯展示数据)。 */
|
|
18
18
|
export interface SecretCandidate {
|
|
19
19
|
readonly envName: string
|
|
20
20
|
readonly name: string
|
|
21
21
|
readonly description: string
|
|
22
|
-
readonly scope: 'global' | 'session'
|
|
22
|
+
readonly scope: 'global' | 'session' | 'inherited'
|
|
23
23
|
readonly once?: boolean
|
|
24
24
|
}
|
|
25
25
|
|
package/src/client/menu.tsx
CHANGED
|
@@ -92,12 +92,23 @@ function toCandidates(list: SecretList): SecretCandidate[] {
|
|
|
92
92
|
scope: 'session' as const,
|
|
93
93
|
once: entry.once,
|
|
94
94
|
})),
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
// 已屏蔽的变量不会注入,不出候选(避免误导补全)。
|
|
96
|
+
...list.global
|
|
97
|
+
.filter((entry) => !entry.masked)
|
|
98
|
+
.map((entry) => ({
|
|
99
|
+
envName: entry.envName,
|
|
100
|
+
name: entry.name,
|
|
101
|
+
description: entry.description,
|
|
102
|
+
scope: 'global' as const,
|
|
103
|
+
})),
|
|
104
|
+
...list.inherited
|
|
105
|
+
.filter((entry) => !entry.masked)
|
|
106
|
+
.map((entry) => ({
|
|
107
|
+
envName: entry.envName,
|
|
108
|
+
name: entry.name,
|
|
109
|
+
description: '',
|
|
110
|
+
scope: 'inherited' as const,
|
|
111
|
+
})),
|
|
101
112
|
]
|
|
102
113
|
}
|
|
103
114
|
|
|
@@ -262,6 +273,7 @@ export function SecretMenu(props: SecretMenuProps): ReactElement | null {
|
|
|
262
273
|
type="button"
|
|
263
274
|
role="option"
|
|
264
275
|
aria-selected={index === highlight}
|
|
276
|
+
title={`$${entry.envName}`}
|
|
265
277
|
className={`dse-menuItem${index === highlight ? ' dse-menuItemActive' : ''}`}
|
|
266
278
|
onMouseEnter={() => setHighlight(index)}
|
|
267
279
|
onMouseDown={(event) => {
|
|
@@ -269,12 +281,16 @@ export function SecretMenu(props: SecretMenuProps): ReactElement | null {
|
|
|
269
281
|
pick(entry)
|
|
270
282
|
}}
|
|
271
283
|
>
|
|
272
|
-
<span className="dse-menuName"
|
|
284
|
+
<span className="dse-menuName">{entry.name}</span>
|
|
273
285
|
<span className="dse-menuDesc">{entry.description}</span>
|
|
274
286
|
<span className="dse-menuBadges">
|
|
275
287
|
{entry.once === true ? <span className="dse-badge">{t('scopeOnce')}</span> : null}
|
|
276
288
|
<span className="dse-badge dse-badgeDim">
|
|
277
|
-
{entry.scope === 'session'
|
|
289
|
+
{entry.scope === 'session'
|
|
290
|
+
? t('scopeSession')
|
|
291
|
+
: entry.scope === 'inherited'
|
|
292
|
+
? t('scopeInherited')
|
|
293
|
+
: t('scopeGlobal')}
|
|
278
294
|
</span>
|
|
279
295
|
</span>
|
|
280
296
|
</button>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话密钥面板的打开总线(bundle 内模块级 pub/sub):
|
|
3
|
+
* 斜杠命令的 onSelect 与 overlay 槽里的面板宿主在同一 bundle,
|
|
4
|
+
* 经此按 sessionId 定向投递「打开面板」信号,避免跨 cordis 作用域布线。
|
|
5
|
+
* @module secret-env/client/panel-bus
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type Listener = (sessionId: string) => void
|
|
9
|
+
|
|
10
|
+
const listeners = new Set<Listener>()
|
|
11
|
+
|
|
12
|
+
/** 请求打开指定会话的密钥面板(由 /secret 命令的 onSelect 调用)。 */
|
|
13
|
+
export function requestOpenSessionPanel(sessionId: string): void {
|
|
14
|
+
for (const listener of listeners) listener(sessionId)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 订阅打开信号;返回退订器(随组件卸载清理)。 */
|
|
18
|
+
export function onOpenSessionPanel(listener: Listener): () => void {
|
|
19
|
+
listeners.add(listener)
|
|
20
|
+
return () => {
|
|
21
|
+
listeners.delete(listener)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话变量面板的 overlay 宿主(conversation.input.overlay 官方插槽,
|
|
3
|
+
* session 作用域):常驻监听打开总线,/var 命令选中后弹出面板。
|
|
4
|
+
* 弹层定位于 composer 卡片上方(与 $ 菜单同一浮层语言);
|
|
5
|
+
* 点外部或 Esc 关闭。闭合态不渲染面板(数据在打开时现取)。
|
|
6
|
+
* @module secret-env/client/panel-host
|
|
7
|
+
*/
|
|
8
|
+
import { type ReactElement, useEffect, useRef, useState } from 'react'
|
|
9
|
+
|
|
10
|
+
import { SessionSecretsPanel } from './panel.tsx'
|
|
11
|
+
import { onOpenSessionPanel } from './panel-bus.ts'
|
|
12
|
+
|
|
13
|
+
export interface SessionPanelHostProps {
|
|
14
|
+
/** 插槽 inject 工厂注入的当前会话 id。 */
|
|
15
|
+
sessionId: string
|
|
16
|
+
t(key: string): string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SessionPanelHost(props: SessionPanelHostProps): ReactElement | null {
|
|
20
|
+
const { sessionId, t } = props
|
|
21
|
+
const [open, setOpen] = useState(false)
|
|
22
|
+
const wrapRef = useRef<HTMLDivElement | null>(null)
|
|
23
|
+
|
|
24
|
+
// 打开总线:只响应本会话的定向信号。
|
|
25
|
+
useEffect(
|
|
26
|
+
() =>
|
|
27
|
+
onOpenSessionPanel((target) => {
|
|
28
|
+
if (target === sessionId) setOpen(true)
|
|
29
|
+
}),
|
|
30
|
+
[sessionId],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// Esc / 点外部关闭(捕获阶段,先于编辑器吞键)。
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open) return
|
|
36
|
+
const onKey = (event: KeyboardEvent): void => {
|
|
37
|
+
if (event.key !== 'Escape') return
|
|
38
|
+
event.preventDefault()
|
|
39
|
+
event.stopPropagation()
|
|
40
|
+
setOpen(false)
|
|
41
|
+
}
|
|
42
|
+
const onDown = (event: PointerEvent): void => {
|
|
43
|
+
if (!(event.target instanceof Node)) return
|
|
44
|
+
if (wrapRef.current?.contains(event.target) === true) return
|
|
45
|
+
setOpen(false)
|
|
46
|
+
}
|
|
47
|
+
document.addEventListener('keydown', onKey, true)
|
|
48
|
+
document.addEventListener('pointerdown', onDown, true)
|
|
49
|
+
return () => {
|
|
50
|
+
document.removeEventListener('keydown', onKey, true)
|
|
51
|
+
document.removeEventListener('pointerdown', onDown, true)
|
|
52
|
+
}
|
|
53
|
+
}, [open])
|
|
54
|
+
|
|
55
|
+
if (!open) return null
|
|
56
|
+
return (
|
|
57
|
+
<div className="dse-panelWrap" ref={wrapRef}>
|
|
58
|
+
<div className="dse-panelCard" role="dialog" aria-label={t('sessionSecrets')}>
|
|
59
|
+
<SessionSecretsPanel sessionId={sessionId} t={t} onClose={() => setOpen(false)} />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|