dsh-manage-session 2.0.3 → 2.0.4

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.
Files changed (43) hide show
  1. package/CHANGELOG.ja.md +33 -33
  2. package/CHANGELOG.ko.md +33 -33
  3. package/CHANGELOG.md +33 -33
  4. package/INSTALL.ja.md +164 -164
  5. package/INSTALL.ko.md +164 -164
  6. package/INSTALL.md +164 -164
  7. package/INSTALL.zh.md +164 -164
  8. package/LICENSE +21 -21
  9. package/README.en.md +76 -62
  10. package/README.ja.md +53 -39
  11. package/README.ko.md +52 -38
  12. package/README.md +18 -4
  13. package/cordis.patch.yml +19 -19
  14. package/dsh.plugin.json +2 -2
  15. package/locale/de.json +6 -6
  16. package/locale/en.json +6 -6
  17. package/locale/es.json +6 -6
  18. package/locale/fr.json +6 -6
  19. package/locale/it.json +6 -6
  20. package/locale/ja.json +6 -6
  21. package/locale/ko.json +6 -6
  22. package/locale/ru.json +6 -6
  23. package/locale/zh.json +6 -6
  24. package/package.json +9 -9
  25. package/screenshots.json +3 -3
  26. package/src/client/bridge-scope.ts +202 -202
  27. package/src/client/card.tsx +154 -154
  28. package/src/client/host-api.ts +61 -61
  29. package/src/client/index.ts +594 -594
  30. package/src/client/locales.ts +893 -893
  31. package/src/client/manage-locales.ts +178 -178
  32. package/src/client/manage.tsx +430 -430
  33. package/src/client/panel.tsx +131 -131
  34. package/src/config.ts +43 -43
  35. package/src/host/health/attribution.ts +118 -118
  36. package/src/host/health/lossless.ts +128 -128
  37. package/src/host/health/source-kind.ts +350 -350
  38. package/src/host/history/archive-source.ts +250 -250
  39. package/src/host/history/archive.ts +249 -249
  40. package/src/host/history/favorites.ts +71 -71
  41. package/src/host/history/purge.ts +370 -370
  42. package/src/host/registry/archive-registry.ts +317 -317
  43. package/src/host/settings-bridge.ts +222 -222
@@ -1,594 +1,594 @@
1
- /**
2
- * 会话管理客户端半身:样式、字典、侧栏入口(顶栏工具槽 / 底栏回落)与设置卡。
3
- *
4
- * 入口只有一个(会话管理):点开是「管理 / 体检」双页签。搜索子域已拆回
5
- * dsh-search-index —— 本文件不再注册搜索入口、搜索面板或搜索设置卡。
6
- *
7
- * 入口位次由宿主能力决定(inject 探测两席位):
8
- * - 顶栏工具行(`sidebar.panellist` 面板行 + `main` 整页槽)→ 面板行落在「插件」
9
- * 之后、「更多」之前,点击在主栏整页打开;
10
- * - 无该槽位(老线)→ 回落底栏 `sidebar.footer.action`,点击开对话框。
11
- *
12
- * 开关读取:configForms(0.1.7,entry id 键)/ legacy 宿主走自家 webServer 设置桥
13
- * (settingsScope.bind 是数据死路,见 bridge-scope.ts);配置写入同链路。
14
- */
15
- import type { Context } from 'cordis'
16
- import { createElement, useState, useSyncExternalStore, type ReactElement } from 'react'
17
- import {
18
- DEFAULT_CONFIG,
19
- STEWARD_ENTRY_ID,
20
- STEWARD_SETTINGS_NAMESPACE,
21
- type StewardConfig,
22
- } from '../config.ts'
23
- import { StewardCardScope, StewardSettingsCard, type CardTranslate } from './card.tsx'
24
- import { stewardBridgeScope, withBridgeFallback, type BridgeScope } from './bridge-scope.ts'
25
- import { dictionaries, translate, type LocaleKey } from './locales.ts'
26
- import { StewardFooter, StewardPanel, TAB_HEALTH } from './panel.tsx'
27
- import { openSessionThrough } from './host-api.ts'
28
- import {
29
- SESSION_MENU_DELETE_ID, SESSION_MENU_DELETE_ORDER, SESSION_MENU_SEAT, StewardDeleteMenuItem,
30
- } from './session-menu.tsx'
31
- import {
32
- installLegacySessionMenuDelete, makeDirectDeleteProbe, purgeSessionById,
33
- } from './session-menu-legacy.ts'
34
-
35
- export { STEWARD_ENTRY_ID, STEWARD_SETTINGS_NAMESPACE, TAB_HEALTH }
36
- export { translate } from './locales.ts'
37
-
38
- /** 字典命名空间(locale.register 用)。 */
39
- export const NS = 'dsh-manage-session'
40
-
41
- /** 槽位服务面(结构化镜像)。 */
42
- interface StewardSlotsService {
43
- inject(name: string, callback: () => unknown, label?: string): () => void
44
- register(config: Record<string, unknown>, component: unknown): unknown
45
- entries(name: string): Array<{ options: { id?: string; key?: string; order?: number; label?: unknown } }>
46
- getVersion(name: string): number
47
- subscribe(name: string, listener: () => void): () => void
48
- /** 槽位声明探测(部分宿主暴露;仅诊断用)。 */
49
- specDynamic?(name: string): unknown
50
- }
51
-
52
- /** locale 服务面。 */
53
- interface StewardLocaleService {
54
- register(ns: string, dicts: Record<string, Record<string, string>>): () => void
55
- }
56
-
57
- /** configForms 服务面(0.1.7:以 profile entry id 取句柄)。 */
58
- interface StewardConfigForms {
59
- get<U>(entryId: string): StewardCardScope & { getSnapshot(): { value: U | undefined } }
60
- }
61
-
62
- /** 客户端插件声明的注入面。 */
63
- export const inject = ['slots', 'locale']
64
-
65
- /** 侧边栏入口的 props(结构子集)。 */
66
- export interface StewardFooterProps {
67
- /** 侧边栏是否渲染宽栏内容(false = 56px 收起轨道)。 */
68
- wide?: boolean
69
- onClick?: () => void
70
- title?: string
71
- }
72
-
73
- /** 注入样式(幂等)。 */
74
- function injectStyles(): () => void {
75
- const id = 'dsh-manage-session-styles'
76
- if (typeof document === 'undefined') return () => {}
77
- if (document.getElementById(id) !== null) return () => {}
78
- const style = document.createElement('style')
79
- style.id = id
80
- style.textContent = `
81
- /* 侧栏 footer 槽位公约(2026-09-26;2026-10-01 收紧行距并强制居中):一行多
82
- 入口(第三方 dsh-context 等)会互相挤占 —— 宿主 .footerActions 是 nowrap
83
- flex 行。这里允许容器换行,并把本插件入口钉成独占一整行(flex-basis:100%);
84
- 其余入口(含第三方)自然落到后续行,各行内部自行布局,谁也不挤谁。
85
- justify-content:center 让行内所有入口(含不占满行的)统一居中;row-gap:0
86
- 配合入口自身 32px 高度压缩纵向占位。类名用 [class*=] 中段匹配:宿主是 CSS
87
- Module 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
88
- 中段跨版本稳定。 */
89
- [class*="footerActions"]{flex-wrap:wrap;justify-content:center;row-gap:0;height:auto;min-height:0}
90
- /* —— 第三方矫正:dsh-context「上下文洞察」入口(2026-10-01)——
91
- .lc-ov-entry 按"独占整行"设计(width:calc(100% + 4px)、无 justify-content、
92
- 42px 高、不对称 padding),与本槽位公约(每个入口独占一行、行内居中、32px)
93
- 冲突,表现为文字靠左、纵向松散。这里按公约强制矫正;:not() 排除收起轨道的
94
- 36px 圆钮形态。lc-ov-* 是 dsh-context 源码硬编码类名(非构建哈希),跨版本
95
- 稳定(实测 0.56.1 / 0.60.0 规则一致)。两个自有插件各自携带同一矫正块:
96
- 任一被禁用时矫正仍生效。 */
97
- .lc-ov-entry:not(.lc-ov-entry-rail){width:auto!important;flex:0 0 100%!important;min-width:0!important;justify-content:center!important;height:32px!important;margin:0!important;padding:0 10px!important}
98
- .lc-ov-entry-label{flex:0 1 auto!important}
99
- /* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
100
- .dms_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
101
- .dms_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
102
- /* 高度 32px(内容 22px 行高 + 上下各 5px):宿主默认 42px 的上下裕度在多行
103
- 堆叠后过于松散;左右内边距对称(10px/10px),否则整行居中时按钮内容会因
104
- 不对称 padding 向左偏 1px。 */
105
- .dms_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;padding:0 10px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
106
- .dms_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
107
- .dms_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
108
- .dms_footerIcon{flex:none;font-size:15px;line-height:1}
109
- .dms_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
110
- .dms_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
111
- .dms_panel{position:fixed;z-index:1001;left:50%;top:6vh;transform:translateX(-50%);width:min(860px,94vw);max-height:86vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
112
- /* 整页形态(顶栏工具行入口):取消浮动定位与遮罩观感,填满主栏。 */
113
- .dms_panel.dms_panelPage{position:relative;left:auto;top:auto;transform:none;z-index:auto;width:100%;max-width:none;height:100%;max-height:none;box-sizing:border-box;border:none;border-radius:0;box-shadow:none;background:transparent;padding:14px 18px 16px}
114
- .dms_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
115
- .dms_dialogTitle{font-weight:600;color:var(--dsw-alias-label-primary,#111827);flex:1 1 auto;min-width:0}
116
- .dms_tabRow{display:inline-flex;gap:4px}
117
- .dms_tab{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:3px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
118
- .dms_tabActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12));color:var(--dsw-alias-label-primary,#111827)}
119
- .dms_tabBody{display:flex;flex-direction:column;gap:8px;padding-top:8px}
120
- .dms_actBtn{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:4px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary,#111827)}
121
- .dms_actBtn:disabled{opacity:.5;cursor:default}
122
- .dms_btnRow{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
123
- .dms_list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
124
- .dms_row{display:flex;gap:8px;align-items:center;padding:5px 8px;border-radius:8px;border:1px solid transparent}
125
- .dms_row:hover{border-color:var(--dsw-alias-border-l2,#e5e7eb)}
126
- .dms_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
127
- .dms_uuid{font-family:var(--ds-font-family-code,monospace)}
128
- /* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
129
- .dms_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
130
- .dms_error{font-size:12px;color:var(--dsw-alias-state-error-primary,#dc2626)}
131
- .dms_empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af);padding:10px 2px}
132
- .dms_gateList{display:flex;flex-direction:column;gap:6px}
133
- .dms_gate{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 10px}
134
- .dms_gateName{font-weight:600;font-size:12px}
135
- .dms_gateEvidence{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1 1 100%}
136
- .dms_levelPill{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid transparent}
137
- .dms_level_ok{background:rgba(22,163,74,.12);color:#15803d}
138
- .dms_level_warn{background:rgba(217,119,6,.14);color:#b45309}
139
- .dms_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
140
- .dms_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
141
- .dms_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
142
- .dms_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
143
- .dms_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
144
- .dms_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
145
- .dms_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
146
- .dms_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
147
- .dms_spinner{width:11px;height:11px;margin-right:6px;border-radius:50%;display:inline-block;vertical-align:-1px;border:2px solid currentColor;border-top-color:transparent;animation:dssSpin .7s linear infinite}
148
- @keyframes dssSpin{to{transform:rotate(360deg)}}
149
- @keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
150
- @media (prefers-reduced-motion:reduce){.dms_spinner{animation:none}.dms_progressIndeterminate{animation:none;width:100%;opacity:.5}.dms_setChev{transition:none}.dms_setCard{transition:none}}
151
- .dms_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
152
- .dms_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
153
- .dms_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
154
- .dms_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
155
- .dms_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
156
- .dms_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
157
- .dms_phaseRow{display:flex;gap:8px;align-items:center}
158
- .dms_phase{font-weight:600;font-size:12px}
159
- .dms_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
160
- .dms_cmdLine{display:flex;gap:6px;align-items:center}
161
- .dms_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
162
- .dms_setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.05));border-radius:12px;transition:border-color .16s,background .16s}
163
- .dms_setHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:none;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
164
- .dms_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
165
- .dms_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
166
- .dms_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
167
- .dms_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
168
- .dms_setCardOpen .dms_setChev{transform:rotate(180deg)}
169
- .dms_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
170
- .dms_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
171
- .dms_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
172
- .dms_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
173
- .dms_setDesc{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
174
- .dms_toggle{display:inline-flex;align-items:center;cursor:pointer;position:relative}
175
- .dms_toggle input{position:absolute;opacity:0;width:0;height:0}
176
- .dms_toggleTrack{width:34px;height:18px;border-radius:999px;background:var(--dsw-alias-border-l2,#d1d5db);position:relative;transition:background .15s ease}
177
- .dms_toggleKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}
178
- .dms_toggleOn .dms_toggleTrack{background:var(--dsw-alias-button-primary-fill,#3f63d8)}
179
- .dms_toggleOn .dms_toggleKnob{left:18px}
180
- `
181
- document.head.appendChild(style)
182
- return () => { style.remove() }
183
- }
184
-
185
- /**
186
- * 客户端插件主体。
187
- * @param ctx - 客户端上下文(slots、可选 locale / configForms)。
188
- */
189
- export function apply(ctx: Context): void {
190
- ctx.effect(() => injectStyles(), 'dsh-manage-session: stylesheet')
191
-
192
- const locale = ctx.get('locale') as StewardLocaleService | undefined
193
- if (locale !== undefined && typeof locale.register === 'function') {
194
- ctx.effect(() => locale.register(NS, dictionaries), 'dsh-manage-session: dictionaries')
195
- }
196
-
197
- // 数据面回退链(台账 A2 定案 + 0.1.7 实测补丁):首选 configForms 原生句柄
198
- // (entry id 键),**渲染期**观测到 unavailable(出生死或 loading→死,A2 死
199
- // 路签名)就地落桥——自家 webServer 设置桥(settings-describe/mutate)。
200
- const configForms = ctx.get('configForms') as StewardConfigForms | undefined
201
- const bound = withBridgeFallback(
202
- configForms?.get<StewardConfig>(STEWARD_ENTRY_ID) as BridgeScope<StewardConfig> | undefined,
203
- stewardBridgeScope(),
204
- )
205
-
206
- const slots = ctx.get('slots') as StewardSlotsService | undefined
207
- if (slots === undefined) return
208
-
209
- // 管理台行点击打开会话:click 时经 openSessionThrough 解析(uiWorkspace.openSession
210
- // 0.1.7+ 优先,旧宿主回退 sessions.open)—— 应用序早于导航模块,必须惰性取。
211
- const openSession = (sessionId: string): void => {
212
- openSessionThrough((name) => ctx.get(name), sessionId)
213
- }
214
-
215
- // 顶栏页的关闭动作:回退到会话(layout 服务惰性取——装载序不保证)。
216
- const closeToolPanel = (): void => {
217
- try {
218
- const layout = ctx.get('layout') as { selectPanel?: (id: string | null) => void } | undefined
219
- layout?.selectPanel?.(null)
220
- } catch {
221
- /* 布局服务缺席:页面保留,用户可点「会话」行返回 */
222
- }
223
- }
224
-
225
- // 侧栏入口(一枚)——**位次由宿主能力决定**:顶栏工具行 or 底栏回落。
226
- // 席位能力用 inject 探测(席位被声明时才回调),不依赖 spec 查询。
227
- ctx.effect(() => {
228
- let rowSeat = false
229
- let mainSeat = false
230
- let footerOff: (() => void) | undefined
231
- let topOff: (() => void) | undefined
232
-
233
- const sync = (): void => {
234
- const topReady = rowSeat && mainSeat
235
- if (topReady && topOff === undefined) {
236
- if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
237
- topOff = mountTopToolEntries(slots, bound, openSession, closeToolPanel)
238
- return
239
- }
240
- if (!topReady && footerOff === undefined && topOff === undefined) {
241
- footerOff = mountFooterEntries(slots, bound, openSession)
242
- }
243
- }
244
-
245
- // 接线期不让 probe 回调触发 sync:两席位先后落位时先到的会误判「顶栏不齐」。
246
- let wired = false
247
- const onSeat = (): void => { if (wired) sync() }
248
-
249
- const offRowProbe = slots.inject(TOP_TOOL_SEAT, () => {
250
- rowSeat = true
251
- onSeat()
252
- return () => { rowSeat = false }
253
- }, 'dsh-manage-session: probe top tool seat')
254
-
255
- const offMainProbe = slots.inject(MAIN_SEAT, () => {
256
- mainSeat = true
257
- onSeat()
258
- return () => { mainSeat = false }
259
- }, 'dsh-manage-session: probe main page seat')
260
-
261
- wired = true
262
- sync()
263
-
264
- return () => {
265
- offRowProbe()
266
- offMainProbe()
267
- if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
268
- if (topOff !== undefined) { const off = topOff; topOff = undefined; off() }
269
- }
270
- }, 'dsh-manage-session: sidebar entry')
271
-
272
- // 插件族共用设置 tab(dsh-thinking-levels 的顶级「起子插件设置」节声明该子席位)。
273
- // thinking-levels 缺席时本 inject 静默等待,不阻塞客户端半。
274
- slots.inject('dsh-family.tab', () => slots.register(
275
- {
276
- name: 'dsh-family.tab',
277
- id: STEWARD_ENTRY_ID,
278
- order: 40,
279
- // 账本 label:读期求值,跟随当前 locale(translate 内建 zh/en 兜底)。
280
- label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'card.title'),
281
- locale: NS,
282
- },
283
- (props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
284
- scope: bound as StewardCardScope,
285
- t: props.t,
286
- }),
287
- ), 'dsh-manage-session: family settings tab')
288
-
289
- // Plugins 页配置卡(`plugins.bundle.config`,key = 包名):0.1.7 宿主的 Plugins
290
- // 页只渲染 key 与包名相等的条目。
291
- slots.inject('plugins.bundle.config', () => slots.register(
292
- { name: 'plugins.bundle.config', key: STEWARD_ENTRY_ID },
293
- (props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
294
- scope: bound as StewardCardScope,
295
- t: props.t,
296
- }),
297
- ), 'dsh-manage-session: plugins page config card')
298
-
299
- // 侧栏会话菜单删除臂(按左侧边栏换代识别):席位自 0.1.7 存在,本行 order=500
300
- // 落末位;≤0.1.5 菜单写死无席位 → inject 静默等待,删除入口由 DOM 注入臂承担。
301
- // 行可见性由组件自探针 directDelete(缺省关),非 true 不渲染。
302
- slots.inject(SESSION_MENU_SEAT, () => slots.register(
303
- { name: SESSION_MENU_SEAT, id: SESSION_MENU_DELETE_ID, order: SESSION_MENU_DELETE_ORDER, locale: NS },
304
- StewardDeleteMenuItem,
305
- ), 'dsh-manage-session: session menu delete row')
306
-
307
- // 老线(≤0.1.5)菜单无席位——DOM 注入臂补上删除行。
308
- if (typeof document !== 'undefined') {
309
- const probe = makeDirectDeleteProbe()
310
- ctx.effect(() => installLegacySessionMenuDelete({
311
- doc: document,
312
- purge: purgeSessionById,
313
- isEnabled: probe,
314
- lang: () => (typeof navigator !== 'undefined' ? navigator.language : undefined),
315
- }), 'dsh-manage-session: legacy session menu delete injector')
316
- }
317
-
318
- // 家族节宿主选举(次优先级):宿主 dsh-thinking-levels 缺席时顶上「起子插件
319
- // 设置」节。下载量排名低于 session-guard → 宽限期更长,guard 先尝试接管;
320
- // guard 也缺席时本插件按同一 id `dsh-family` 接管。
321
- const familyTabsHooks = makeFamilyTabsHooks(slots)
322
- ctx.effect(() => {
323
- let claimed: unknown
324
- const timer = setTimeout(() => {
325
- if (slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
326
- try {
327
- claimed = slots.register({
328
- name: 'settings.section',
329
- id: FAMILY_SECTION_ID,
330
- order: 40,
331
- label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'family.title'),
332
- locale: NS,
333
- inject: () => ({ hooks: { tabs: familyTabsHooks } }),
334
- children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
335
- }, StewardFamilySection)
336
- } catch {
337
- // 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
338
- }
339
- }, FAMILY_HOST_GRACE_MS)
340
- return () => { clearTimeout(timer); if (typeof claimed === 'function') (claimed as () => void)() }
341
- }, 'dsh-manage-session: family fallback host')
342
- }
343
-
344
- /** 家族节固定 id(与 thinking-levels / guard 的注册严格一致)。 */
345
- const FAMILY_SECTION_ID = 'dsh-family'
346
- /** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
347
- const FAMILY_CHILD_KEY = 'dsh-family.tab'
348
- /** 家族容器接管分级——唯一真源 `mine-dsh-plugins/_tools/family-rank.mjs`(离线实测
349
- * GitHub star + npm 周下载量加权)。延迟 = BASE + RANK * STEP。 */
350
- const FAMILY_CLAIM_RANK = 3
351
- const FAMILY_CLAIM_BASE_MS = 200
352
- const FAMILY_CLAIM_STEP_MS = 900
353
- /** 接管宽限期(由分级派生)。 */
354
- const FAMILY_HOST_GRACE_MS = FAMILY_CLAIM_BASE_MS + FAMILY_CLAIM_RANK * FAMILY_CLAIM_STEP_MS
355
-
356
- /** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
357
- function makeFamilyTabsHooks(slots: StewardSlotsService): {
358
- getSnapshot: () => readonly FamilyTabEntry[]
359
- subscribe: (listener: () => void) => () => void
360
- } {
361
- let version = -1
362
- let tabs: readonly FamilyTabEntry[] = []
363
- return {
364
- getSnapshot: () => {
365
- const next = slots.getVersion(FAMILY_CHILD_KEY)
366
- if (next !== version) {
367
- version = next
368
- tabs = slots.entries(FAMILY_CHILD_KEY)
369
- .map(entry => ({
370
- id: entry.options.id ?? '',
371
- order: entry.options.order ?? 0,
372
- label: typeof entry.options.label === 'function'
373
- ? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
374
- : String(entry.options.label ?? entry.options.id ?? ''),
375
- }))
376
- .sort((a, b) => a.order - b.order)
377
- }
378
- return tabs
379
- },
380
- subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
381
- }
382
- }
383
-
384
- /** 账本条目(id/order/label,label 已解析为字符串)。 */
385
- interface FamilyTabEntry {
386
- id: string
387
- order: number
388
- label: string
389
- }
390
-
391
- /** 家族节接管组件:纯通用渲染——把 `dsh-family.tab` 账本里的每张贡献卡按 order 渲染。 */
392
- function StewardFamilySection(props: {
393
- renderSlot: (key: string, owner?: object, opts?: { only?: string; fallback?: unknown }) => unknown
394
- hooks: { tabs: { getSnapshot: () => readonly FamilyTabEntry[]; subscribe: (listener: () => void) => () => void } }
395
- }): ReactElement {
396
- const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
397
- return createElement('div', { style: { display: 'grid', gap: '12px' } },
398
- ...tabs.map(row => createElement('div', { key: row.id },
399
- props.renderSlot(FAMILY_CHILD_KEY, {}, { only: row.id, fallback: null }) as ReactElement,
400
- )),
401
- )
402
- }
403
-
404
- /** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */
405
- export function StewardEntry(
406
- props: StewardFooterProps & {
407
- scope?: (StewardCardScope & { getSnapshot(): { value: StewardConfig | undefined } }) | undefined
408
- openSession?: (sessionId: string) => void
409
- },
410
- ): ReactElement {
411
- const [open, setOpen] = useState(false)
412
- const snapshot = useSyncExternalStore(
413
- (listener) => (props.scope !== undefined ? props.scope.subscribe(listener) : () => {}),
414
- () => (props.scope !== undefined ? props.scope.getSnapshot() : { value: DEFAULT_CONFIG }),
415
- )
416
- const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
417
- const healthCheck = config.healthCheck !== false
418
- const visible = config.enabled !== false && healthCheck
419
- const wide = props.wide === true
420
- // 入口文案与面板标题同源:字典是唯一出处,组件不再持有兜底中文。
421
- const label = translate(undefined, 'panel.title')
422
-
423
- if (!visible) return createElement('span', { className: 'dms_entryWrap' })
424
- return createElement('span', { className: wide ? 'dms_entryWrap dms_entryWrapWide' : 'dms_entryWrap' }, [
425
- createElement(StewardFooter, {
426
- key: 'btn',
427
- title: label,
428
- label,
429
- wide,
430
- onClick: () => { setOpen(!open) },
431
- }),
432
- open
433
- ? createElement(StewardPanel, {
434
- key: 'panel',
435
- healthCheck,
436
- openSession: props.openSession,
437
- onClose: () => { setOpen(false) },
438
- })
439
- : null,
440
- ])
441
- }
442
-
443
- /* ------------------------------------------------ 侧栏入口位次(顶栏工具槽 / 底栏) */
444
-
445
- /**
446
- * 顶栏工具槽位:面板行列表。宿主在「插件」(order 0)与「更多」启动器之间渲染
447
- * 这些行,行内 icon + label 默认左对齐;点击切换主栏面板。
448
- */
449
- const TOP_TOOL_SEAT = 'sidebar.panellist'
450
- /** 主栏整页槽位(keyed):面板行点击后在此渲染整页内容。 */
451
- const MAIN_SEAT = 'main'
452
- /** 管理台页 id(同时作为 `sidebar.panellist` 行的 id 与 `main` 的 key)。 */
453
- const MANAGER_PANEL_ID = 'manage-session'
454
-
455
- /**
456
- * 宿主是否提供顶栏工具槽位(诊断用):面板行席位 + 主栏整页席位同时被声明才算。
457
- * 实际位次判定走 apply 内的 inject 探测(服务面不保证暴露 specDynamic)。
458
- * @param slots - 槽位服务面。
459
- * @returns 两席都已声明时 `true`。
460
- */
461
- function hasPanelToolSeat(slots: StewardSlotsService): boolean {
462
- if (typeof slots.specDynamic !== 'function') return false
463
- try {
464
- return slots.specDynamic(TOP_TOOL_SEAT) !== undefined && slots.specDynamic(MAIN_SEAT) !== undefined
465
- } catch {
466
- return false
467
- }
468
- }
469
-
470
- export { hasPanelToolSeat }
471
-
472
- /** 组合注销函数(逆序执行,幂等)。 */
473
- function composeDisposers(offs: Array<() => void>): () => void {
474
- let done = false
475
- return () => {
476
- if (done) return
477
- done = true
478
- for (const off of offs.reverse()) {
479
- try { off() } catch { /* 单个席位解装失败不阻断其余回收 */ }
480
- }
481
- }
482
- }
483
-
484
- /**
485
- * 按开关状态维持一对注册(主栏页 + 面板行)。快照缺字段一律按「可见」处理。
486
- * @param scope - 设置 scope(缺失时视为缺省值)。
487
- * @param visible - 可见性判定。
488
- * @param mount - 可见时注册并返回注销函数。
489
- * @returns 解装函数。
490
- */
491
- function gatedMount<V extends object>(
492
- scope: BridgeScope<V> | undefined,
493
- visible: (value: Partial<V>) => boolean,
494
- mount: () => () => void,
495
- ): () => void {
496
- let dispose: (() => void) | undefined
497
- const apply = (): void => {
498
- const value = (scope?.getSnapshot?.().value ?? {}) as Partial<V>
499
- const want = visible(value)
500
- if (want && dispose === undefined) dispose = mount()
501
- else if (!want && dispose !== undefined) {
502
- const off = dispose
503
- dispose = undefined
504
- off()
505
- }
506
- }
507
- apply()
508
- const unsubscribe = scope?.subscribe(apply)
509
- return () => {
510
- unsubscribe?.()
511
- if (dispose !== undefined) {
512
- const off = dispose
513
- dispose = undefined
514
- off()
515
- }
516
- }
517
- }
518
-
519
- /** 底栏回落形态(老宿主 / 无顶栏工具行):一枚入口开对话框。 */
520
- function mountFooterEntries(
521
- slots: StewardSlotsService,
522
- bound: BridgeScope<StewardConfig>,
523
- openSession: (sessionId: string) => void,
524
- ): () => void {
525
- const off = slots.inject('sidebar.footer.action', () => slots.register(
526
- { name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 6 },
527
- (props: StewardFooterProps) => createElement(StewardEntry, {
528
- ...props,
529
- scope: bound as unknown as StewardCardScope,
530
- openSession,
531
- }),
532
- ), 'dsh-manage-session: sidebar footer entry')
533
- return composeDisposers([off])
534
- }
535
-
536
- /**
537
- * 顶栏工具行形态:面板行(order 6,落在「插件」行之后、「更多」之前)加主栏整页。
538
- * 点击行 → 宿主 `selectPanel(id)` 在主栏渲染整页;页内关闭动作回退到会话。
539
- * @param closePanel - 关闭页回退动作(`layout.selectPanel(null)`)。
540
- */
541
- function mountTopToolEntries(
542
- slots: StewardSlotsService,
543
- bound: BridgeScope<StewardConfig>,
544
- openSession: (sessionId: string) => void,
545
- closePanel: () => void,
546
- ): () => void {
547
- /** 管理页:管理台 / 体检 双页签,页签可见性与底栏入口同源。 */
548
- const ManagerPage = (): ReactElement => {
549
- const snapshot = useSyncExternalStore(
550
- (listener) => bound.subscribe(listener),
551
- () => bound.getSnapshot(),
552
- )
553
- const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
554
- return createElement(StewardPanel, {
555
- variant: 'page',
556
- healthCheck: config.healthCheck !== false,
557
- openSession,
558
- onClose: closePanel,
559
- })
560
- }
561
-
562
- const off = gatedMount(bound, value => value.enabled !== false && value.healthCheck !== false, () =>
563
- composeDisposers([
564
- slots.inject(MAIN_SEAT, () => slots.register(
565
- { name: MAIN_SEAT, key: MANAGER_PANEL_ID, locale: NS },
566
- ManagerPage,
567
- ), `dsh-manage-session: main page ${MANAGER_PANEL_ID}`),
568
- slots.inject(TOP_TOOL_SEAT, () => slots.register(
569
- {
570
- name: TOP_TOOL_SEAT,
571
- id: MANAGER_PANEL_ID,
572
- order: 6,
573
- label: () => translate(undefined, 'card.title'),
574
- locale: NS,
575
- },
576
- ManagerToolIcon,
577
- ), `dsh-manage-session: top tool row ${MANAGER_PANEL_ID}`),
578
- ]))
579
-
580
- return off
581
- }
582
-
583
- /** 顶栏行图标:会话管理(列表行 + 勾选,随行字色)。 */
584
- function ManagerToolIcon({ size }: { size?: number }): ReactElement {
585
- const edge = size ?? 16
586
- return createElement('svg', {
587
- width: edge, height: edge, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true,
588
- },
589
- createElement('path', { d: 'M2 4h12M2 8h12M2 12h7', stroke: 'currentColor', strokeWidth: 1.75, strokeLinecap: 'round' }),
590
- createElement('path', { d: 'M11.2 11.2l1.5 1.5 2.6-3', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }))
591
- }
592
-
593
- /** 导出字典查找 type 供消费者复用。 */
594
- export type { LocaleKey }
1
+ /**
2
+ * 会话管理客户端半身:样式、字典、侧栏入口(顶栏工具槽 / 底栏回落)与设置卡。
3
+ *
4
+ * 入口只有一个(会话管理):点开是「管理 / 体检」双页签。搜索子域已拆回
5
+ * dsh-search-index —— 本文件不再注册搜索入口、搜索面板或搜索设置卡。
6
+ *
7
+ * 入口位次由宿主能力决定(inject 探测两席位):
8
+ * - 顶栏工具行(`sidebar.panellist` 面板行 + `main` 整页槽)→ 面板行落在「插件」
9
+ * 之后、「更多」之前,点击在主栏整页打开;
10
+ * - 无该槽位(老线)→ 回落底栏 `sidebar.footer.action`,点击开对话框。
11
+ *
12
+ * 开关读取:configForms(0.1.7,entry id 键)/ legacy 宿主走自家 webServer 设置桥
13
+ * (settingsScope.bind 是数据死路,见 bridge-scope.ts);配置写入同链路。
14
+ */
15
+ import type { Context } from 'cordis'
16
+ import { createElement, useState, useSyncExternalStore, type ReactElement } from 'react'
17
+ import {
18
+ DEFAULT_CONFIG,
19
+ STEWARD_ENTRY_ID,
20
+ STEWARD_SETTINGS_NAMESPACE,
21
+ type StewardConfig,
22
+ } from '../config.ts'
23
+ import { StewardCardScope, StewardSettingsCard, type CardTranslate } from './card.tsx'
24
+ import { stewardBridgeScope, withBridgeFallback, type BridgeScope } from './bridge-scope.ts'
25
+ import { dictionaries, translate, type LocaleKey } from './locales.ts'
26
+ import { StewardFooter, StewardPanel, TAB_HEALTH } from './panel.tsx'
27
+ import { openSessionThrough } from './host-api.ts'
28
+ import {
29
+ SESSION_MENU_DELETE_ID, SESSION_MENU_DELETE_ORDER, SESSION_MENU_SEAT, StewardDeleteMenuItem,
30
+ } from './session-menu.tsx'
31
+ import {
32
+ installLegacySessionMenuDelete, makeDirectDeleteProbe, purgeSessionById,
33
+ } from './session-menu-legacy.ts'
34
+
35
+ export { STEWARD_ENTRY_ID, STEWARD_SETTINGS_NAMESPACE, TAB_HEALTH }
36
+ export { translate } from './locales.ts'
37
+
38
+ /** 字典命名空间(locale.register 用)。 */
39
+ export const NS = 'dsh-manage-session'
40
+
41
+ /** 槽位服务面(结构化镜像)。 */
42
+ interface StewardSlotsService {
43
+ inject(name: string, callback: () => unknown, label?: string): () => void
44
+ register(config: Record<string, unknown>, component: unknown): unknown
45
+ entries(name: string): Array<{ options: { id?: string; key?: string; order?: number; label?: unknown } }>
46
+ getVersion(name: string): number
47
+ subscribe(name: string, listener: () => void): () => void
48
+ /** 槽位声明探测(部分宿主暴露;仅诊断用)。 */
49
+ specDynamic?(name: string): unknown
50
+ }
51
+
52
+ /** locale 服务面。 */
53
+ interface StewardLocaleService {
54
+ register(ns: string, dicts: Record<string, Record<string, string>>): () => void
55
+ }
56
+
57
+ /** configForms 服务面(0.1.7:以 profile entry id 取句柄)。 */
58
+ interface StewardConfigForms {
59
+ get<U>(entryId: string): StewardCardScope & { getSnapshot(): { value: U | undefined } }
60
+ }
61
+
62
+ /** 客户端插件声明的注入面。 */
63
+ export const inject = ['slots', 'locale']
64
+
65
+ /** 侧边栏入口的 props(结构子集)。 */
66
+ export interface StewardFooterProps {
67
+ /** 侧边栏是否渲染宽栏内容(false = 56px 收起轨道)。 */
68
+ wide?: boolean
69
+ onClick?: () => void
70
+ title?: string
71
+ }
72
+
73
+ /** 注入样式(幂等)。 */
74
+ function injectStyles(): () => void {
75
+ const id = 'dsh-manage-session-styles'
76
+ if (typeof document === 'undefined') return () => {}
77
+ if (document.getElementById(id) !== null) return () => {}
78
+ const style = document.createElement('style')
79
+ style.id = id
80
+ style.textContent = `
81
+ /* 侧栏 footer 槽位公约(2026-09-26;2026-10-01 收紧行距并强制居中):一行多
82
+ 入口(第三方 dsh-context 等)会互相挤占 —— 宿主 .footerActions 是 nowrap
83
+ flex 行。这里允许容器换行,并把本插件入口钉成独占一整行(flex-basis:100%);
84
+ 其余入口(含第三方)自然落到后续行,各行内部自行布局,谁也不挤谁。
85
+ justify-content:center 让行内所有入口(含不占满行的)统一居中;row-gap:0
86
+ 配合入口自身 32px 高度压缩纵向占位。类名用 [class*=] 中段匹配:宿主是 CSS
87
+ Module 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
88
+ 中段跨版本稳定。 */
89
+ [class*="footerActions"]{flex-wrap:wrap;justify-content:center;row-gap:0;height:auto;min-height:0}
90
+ /* —— 第三方矫正:dsh-context「上下文洞察」入口(2026-10-01)——
91
+ .lc-ov-entry 按"独占整行"设计(width:calc(100% + 4px)、无 justify-content、
92
+ 42px 高、不对称 padding),与本槽位公约(每个入口独占一行、行内居中、32px)
93
+ 冲突,表现为文字靠左、纵向松散。这里按公约强制矫正;:not() 排除收起轨道的
94
+ 36px 圆钮形态。lc-ov-* 是 dsh-context 源码硬编码类名(非构建哈希),跨版本
95
+ 稳定(实测 0.56.1 / 0.60.0 规则一致)。两个自有插件各自携带同一矫正块:
96
+ 任一被禁用时矫正仍生效。 */
97
+ .lc-ov-entry:not(.lc-ov-entry-rail){width:auto!important;flex:0 0 100%!important;min-width:0!important;justify-content:center!important;height:32px!important;margin:0!important;padding:0 10px!important}
98
+ .lc-ov-entry-label{flex:0 1 auto!important}
99
+ /* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
100
+ .dms_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
101
+ .dms_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
102
+ /* 高度 32px(内容 22px 行高 + 上下各 5px):宿主默认 42px 的上下裕度在多行
103
+ 堆叠后过于松散;左右内边距对称(10px/10px),否则整行居中时按钮内容会因
104
+ 不对称 padding 向左偏 1px。 */
105
+ .dms_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;padding:0 10px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
106
+ .dms_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
107
+ .dms_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
108
+ .dms_footerIcon{flex:none;font-size:15px;line-height:1}
109
+ .dms_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
110
+ .dms_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
111
+ .dms_panel{position:fixed;z-index:1001;left:50%;top:6vh;transform:translateX(-50%);width:min(860px,94vw);max-height:86vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
112
+ /* 整页形态(顶栏工具行入口):取消浮动定位与遮罩观感,填满主栏。 */
113
+ .dms_panel.dms_panelPage{position:relative;left:auto;top:auto;transform:none;z-index:auto;width:100%;max-width:none;height:100%;max-height:none;box-sizing:border-box;border:none;border-radius:0;box-shadow:none;background:transparent;padding:14px 18px 16px}
114
+ .dms_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
115
+ .dms_dialogTitle{font-weight:600;color:var(--dsw-alias-label-primary,#111827);flex:1 1 auto;min-width:0}
116
+ .dms_tabRow{display:inline-flex;gap:4px}
117
+ .dms_tab{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:3px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
118
+ .dms_tabActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12));color:var(--dsw-alias-label-primary,#111827)}
119
+ .dms_tabBody{display:flex;flex-direction:column;gap:8px;padding-top:8px}
120
+ .dms_actBtn{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:4px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary,#111827)}
121
+ .dms_actBtn:disabled{opacity:.5;cursor:default}
122
+ .dms_btnRow{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
123
+ .dms_list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
124
+ .dms_row{display:flex;gap:8px;align-items:center;padding:5px 8px;border-radius:8px;border:1px solid transparent}
125
+ .dms_row:hover{border-color:var(--dsw-alias-border-l2,#e5e7eb)}
126
+ .dms_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
127
+ .dms_uuid{font-family:var(--ds-font-family-code,monospace)}
128
+ /* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
129
+ .dms_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
130
+ .dms_error{font-size:12px;color:var(--dsw-alias-state-error-primary,#dc2626)}
131
+ .dms_empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af);padding:10px 2px}
132
+ .dms_gateList{display:flex;flex-direction:column;gap:6px}
133
+ .dms_gate{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 10px}
134
+ .dms_gateName{font-weight:600;font-size:12px}
135
+ .dms_gateEvidence{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1 1 100%}
136
+ .dms_levelPill{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid transparent}
137
+ .dms_level_ok{background:rgba(22,163,74,.12);color:#15803d}
138
+ .dms_level_warn{background:rgba(217,119,6,.14);color:#b45309}
139
+ .dms_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
140
+ .dms_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
141
+ .dms_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
142
+ .dms_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
143
+ .dms_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
144
+ .dms_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
145
+ .dms_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
146
+ .dms_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
147
+ .dms_spinner{width:11px;height:11px;margin-right:6px;border-radius:50%;display:inline-block;vertical-align:-1px;border:2px solid currentColor;border-top-color:transparent;animation:dssSpin .7s linear infinite}
148
+ @keyframes dssSpin{to{transform:rotate(360deg)}}
149
+ @keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
150
+ @media (prefers-reduced-motion:reduce){.dms_spinner{animation:none}.dms_progressIndeterminate{animation:none;width:100%;opacity:.5}.dms_setChev{transition:none}.dms_setCard{transition:none}}
151
+ .dms_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
152
+ .dms_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
153
+ .dms_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
154
+ .dms_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
155
+ .dms_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
156
+ .dms_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
157
+ .dms_phaseRow{display:flex;gap:8px;align-items:center}
158
+ .dms_phase{font-weight:600;font-size:12px}
159
+ .dms_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
160
+ .dms_cmdLine{display:flex;gap:6px;align-items:center}
161
+ .dms_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
162
+ .dms_setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.05));border-radius:12px;transition:border-color .16s,background .16s}
163
+ .dms_setHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:none;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
164
+ .dms_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
165
+ .dms_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
166
+ .dms_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
167
+ .dms_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
168
+ .dms_setCardOpen .dms_setChev{transform:rotate(180deg)}
169
+ .dms_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
170
+ .dms_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
171
+ .dms_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
172
+ .dms_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
173
+ .dms_setDesc{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
174
+ .dms_toggle{display:inline-flex;align-items:center;cursor:pointer;position:relative}
175
+ .dms_toggle input{position:absolute;opacity:0;width:0;height:0}
176
+ .dms_toggleTrack{width:34px;height:18px;border-radius:999px;background:var(--dsw-alias-border-l2,#d1d5db);position:relative;transition:background .15s ease}
177
+ .dms_toggleKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}
178
+ .dms_toggleOn .dms_toggleTrack{background:var(--dsw-alias-button-primary-fill,#3f63d8)}
179
+ .dms_toggleOn .dms_toggleKnob{left:18px}
180
+ `
181
+ document.head.appendChild(style)
182
+ return () => { style.remove() }
183
+ }
184
+
185
+ /**
186
+ * 客户端插件主体。
187
+ * @param ctx - 客户端上下文(slots、可选 locale / configForms)。
188
+ */
189
+ export function apply(ctx: Context): void {
190
+ ctx.effect(() => injectStyles(), 'dsh-manage-session: stylesheet')
191
+
192
+ const locale = ctx.get('locale') as StewardLocaleService | undefined
193
+ if (locale !== undefined && typeof locale.register === 'function') {
194
+ ctx.effect(() => locale.register(NS, dictionaries), 'dsh-manage-session: dictionaries')
195
+ }
196
+
197
+ // 数据面回退链(台账 A2 定案 + 0.1.7 实测补丁):首选 configForms 原生句柄
198
+ // (entry id 键),**渲染期**观测到 unavailable(出生死或 loading→死,A2 死
199
+ // 路签名)就地落桥——自家 webServer 设置桥(settings-describe/mutate)。
200
+ const configForms = ctx.get('configForms') as StewardConfigForms | undefined
201
+ const bound = withBridgeFallback(
202
+ configForms?.get<StewardConfig>(STEWARD_ENTRY_ID) as BridgeScope<StewardConfig> | undefined,
203
+ stewardBridgeScope(),
204
+ )
205
+
206
+ const slots = ctx.get('slots') as StewardSlotsService | undefined
207
+ if (slots === undefined) return
208
+
209
+ // 管理台行点击打开会话:click 时经 openSessionThrough 解析(uiWorkspace.openSession
210
+ // 0.1.7+ 优先,旧宿主回退 sessions.open)—— 应用序早于导航模块,必须惰性取。
211
+ const openSession = (sessionId: string): void => {
212
+ openSessionThrough((name) => ctx.get(name), sessionId)
213
+ }
214
+
215
+ // 顶栏页的关闭动作:回退到会话(layout 服务惰性取——装载序不保证)。
216
+ const closeToolPanel = (): void => {
217
+ try {
218
+ const layout = ctx.get('layout') as { selectPanel?: (id: string | null) => void } | undefined
219
+ layout?.selectPanel?.(null)
220
+ } catch {
221
+ /* 布局服务缺席:页面保留,用户可点「会话」行返回 */
222
+ }
223
+ }
224
+
225
+ // 侧栏入口(一枚)——**位次由宿主能力决定**:顶栏工具行 or 底栏回落。
226
+ // 席位能力用 inject 探测(席位被声明时才回调),不依赖 spec 查询。
227
+ ctx.effect(() => {
228
+ let rowSeat = false
229
+ let mainSeat = false
230
+ let footerOff: (() => void) | undefined
231
+ let topOff: (() => void) | undefined
232
+
233
+ const sync = (): void => {
234
+ const topReady = rowSeat && mainSeat
235
+ if (topReady && topOff === undefined) {
236
+ if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
237
+ topOff = mountTopToolEntries(slots, bound, openSession, closeToolPanel)
238
+ return
239
+ }
240
+ if (!topReady && footerOff === undefined && topOff === undefined) {
241
+ footerOff = mountFooterEntries(slots, bound, openSession)
242
+ }
243
+ }
244
+
245
+ // 接线期不让 probe 回调触发 sync:两席位先后落位时先到的会误判「顶栏不齐」。
246
+ let wired = false
247
+ const onSeat = (): void => { if (wired) sync() }
248
+
249
+ const offRowProbe = slots.inject(TOP_TOOL_SEAT, () => {
250
+ rowSeat = true
251
+ onSeat()
252
+ return () => { rowSeat = false }
253
+ }, 'dsh-manage-session: probe top tool seat')
254
+
255
+ const offMainProbe = slots.inject(MAIN_SEAT, () => {
256
+ mainSeat = true
257
+ onSeat()
258
+ return () => { mainSeat = false }
259
+ }, 'dsh-manage-session: probe main page seat')
260
+
261
+ wired = true
262
+ sync()
263
+
264
+ return () => {
265
+ offRowProbe()
266
+ offMainProbe()
267
+ if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
268
+ if (topOff !== undefined) { const off = topOff; topOff = undefined; off() }
269
+ }
270
+ }, 'dsh-manage-session: sidebar entry')
271
+
272
+ // 插件族共用设置 tab(dsh-thinking-levels 的顶级「起子插件设置」节声明该子席位)。
273
+ // thinking-levels 缺席时本 inject 静默等待,不阻塞客户端半。
274
+ slots.inject('dsh-family.tab', () => slots.register(
275
+ {
276
+ name: 'dsh-family.tab',
277
+ id: STEWARD_ENTRY_ID,
278
+ order: 40,
279
+ // 账本 label:读期求值,跟随当前 locale(translate 内建 zh/en 兜底)。
280
+ label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'card.title'),
281
+ locale: NS,
282
+ },
283
+ (props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
284
+ scope: bound as StewardCardScope,
285
+ t: props.t,
286
+ }),
287
+ ), 'dsh-manage-session: family settings tab')
288
+
289
+ // Plugins 页配置卡(`plugins.bundle.config`,key = 包名):0.1.7 宿主的 Plugins
290
+ // 页只渲染 key 与包名相等的条目。
291
+ slots.inject('plugins.bundle.config', () => slots.register(
292
+ { name: 'plugins.bundle.config', key: STEWARD_ENTRY_ID },
293
+ (props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
294
+ scope: bound as StewardCardScope,
295
+ t: props.t,
296
+ }),
297
+ ), 'dsh-manage-session: plugins page config card')
298
+
299
+ // 侧栏会话菜单删除臂(按左侧边栏换代识别):席位自 0.1.7 存在,本行 order=500
300
+ // 落末位;≤0.1.5 菜单写死无席位 → inject 静默等待,删除入口由 DOM 注入臂承担。
301
+ // 行可见性由组件自探针 directDelete(缺省关),非 true 不渲染。
302
+ slots.inject(SESSION_MENU_SEAT, () => slots.register(
303
+ { name: SESSION_MENU_SEAT, id: SESSION_MENU_DELETE_ID, order: SESSION_MENU_DELETE_ORDER, locale: NS },
304
+ StewardDeleteMenuItem,
305
+ ), 'dsh-manage-session: session menu delete row')
306
+
307
+ // 老线(≤0.1.5)菜单无席位——DOM 注入臂补上删除行。
308
+ if (typeof document !== 'undefined') {
309
+ const probe = makeDirectDeleteProbe()
310
+ ctx.effect(() => installLegacySessionMenuDelete({
311
+ doc: document,
312
+ purge: purgeSessionById,
313
+ isEnabled: probe,
314
+ lang: () => (typeof navigator !== 'undefined' ? navigator.language : undefined),
315
+ }), 'dsh-manage-session: legacy session menu delete injector')
316
+ }
317
+
318
+ // 家族节宿主选举(次优先级):宿主 dsh-thinking-levels 缺席时顶上「起子插件
319
+ // 设置」节。下载量排名低于 session-guard → 宽限期更长,guard 先尝试接管;
320
+ // guard 也缺席时本插件按同一 id `dsh-family` 接管。
321
+ const familyTabsHooks = makeFamilyTabsHooks(slots)
322
+ ctx.effect(() => {
323
+ let claimed: unknown
324
+ const timer = setTimeout(() => {
325
+ if (slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
326
+ try {
327
+ claimed = slots.register({
328
+ name: 'settings.section',
329
+ id: FAMILY_SECTION_ID,
330
+ order: 40,
331
+ label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'family.title'),
332
+ locale: NS,
333
+ inject: () => ({ hooks: { tabs: familyTabsHooks } }),
334
+ children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
335
+ }, StewardFamilySection)
336
+ } catch {
337
+ // 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
338
+ }
339
+ }, FAMILY_HOST_GRACE_MS)
340
+ return () => { clearTimeout(timer); if (typeof claimed === 'function') (claimed as () => void)() }
341
+ }, 'dsh-manage-session: family fallback host')
342
+ }
343
+
344
+ /** 家族节固定 id(与 thinking-levels / guard 的注册严格一致)。 */
345
+ const FAMILY_SECTION_ID = 'dsh-family'
346
+ /** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
347
+ const FAMILY_CHILD_KEY = 'dsh-family.tab'
348
+ /** 家族容器接管分级——唯一真源 `mine-dsh-plugins/_tools/family-rank.mjs`(离线实测
349
+ * GitHub star + npm 周下载量加权)。延迟 = BASE + RANK * STEP。 */
350
+ const FAMILY_CLAIM_RANK = 3
351
+ const FAMILY_CLAIM_BASE_MS = 200
352
+ const FAMILY_CLAIM_STEP_MS = 900
353
+ /** 接管宽限期(由分级派生)。 */
354
+ const FAMILY_HOST_GRACE_MS = FAMILY_CLAIM_BASE_MS + FAMILY_CLAIM_RANK * FAMILY_CLAIM_STEP_MS
355
+
356
+ /** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
357
+ function makeFamilyTabsHooks(slots: StewardSlotsService): {
358
+ getSnapshot: () => readonly FamilyTabEntry[]
359
+ subscribe: (listener: () => void) => () => void
360
+ } {
361
+ let version = -1
362
+ let tabs: readonly FamilyTabEntry[] = []
363
+ return {
364
+ getSnapshot: () => {
365
+ const next = slots.getVersion(FAMILY_CHILD_KEY)
366
+ if (next !== version) {
367
+ version = next
368
+ tabs = slots.entries(FAMILY_CHILD_KEY)
369
+ .map(entry => ({
370
+ id: entry.options.id ?? '',
371
+ order: entry.options.order ?? 0,
372
+ label: typeof entry.options.label === 'function'
373
+ ? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
374
+ : String(entry.options.label ?? entry.options.id ?? ''),
375
+ }))
376
+ .sort((a, b) => a.order - b.order)
377
+ }
378
+ return tabs
379
+ },
380
+ subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
381
+ }
382
+ }
383
+
384
+ /** 账本条目(id/order/label,label 已解析为字符串)。 */
385
+ interface FamilyTabEntry {
386
+ id: string
387
+ order: number
388
+ label: string
389
+ }
390
+
391
+ /** 家族节接管组件:纯通用渲染——把 `dsh-family.tab` 账本里的每张贡献卡按 order 渲染。 */
392
+ function StewardFamilySection(props: {
393
+ renderSlot: (key: string, owner?: object, opts?: { only?: string; fallback?: unknown }) => unknown
394
+ hooks: { tabs: { getSnapshot: () => readonly FamilyTabEntry[]; subscribe: (listener: () => void) => () => void } }
395
+ }): ReactElement {
396
+ const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
397
+ return createElement('div', { style: { display: 'grid', gap: '12px' } },
398
+ ...tabs.map(row => createElement('div', { key: row.id },
399
+ props.renderSlot(FAMILY_CHILD_KEY, {}, { only: row.id, fallback: null }) as ReactElement,
400
+ )),
401
+ )
402
+ }
403
+
404
+ /** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */
405
+ export function StewardEntry(
406
+ props: StewardFooterProps & {
407
+ scope?: (StewardCardScope & { getSnapshot(): { value: StewardConfig | undefined } }) | undefined
408
+ openSession?: (sessionId: string) => void
409
+ },
410
+ ): ReactElement {
411
+ const [open, setOpen] = useState(false)
412
+ const snapshot = useSyncExternalStore(
413
+ (listener) => (props.scope !== undefined ? props.scope.subscribe(listener) : () => {}),
414
+ () => (props.scope !== undefined ? props.scope.getSnapshot() : { value: DEFAULT_CONFIG }),
415
+ )
416
+ const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
417
+ const healthCheck = config.healthCheck !== false
418
+ const visible = config.enabled !== false && healthCheck
419
+ const wide = props.wide === true
420
+ // 入口文案与面板标题同源:字典是唯一出处,组件不再持有兜底中文。
421
+ const label = translate(undefined, 'panel.title')
422
+
423
+ if (!visible) return createElement('span', { className: 'dms_entryWrap' })
424
+ return createElement('span', { className: wide ? 'dms_entryWrap dms_entryWrapWide' : 'dms_entryWrap' }, [
425
+ createElement(StewardFooter, {
426
+ key: 'btn',
427
+ title: label,
428
+ label,
429
+ wide,
430
+ onClick: () => { setOpen(!open) },
431
+ }),
432
+ open
433
+ ? createElement(StewardPanel, {
434
+ key: 'panel',
435
+ healthCheck,
436
+ openSession: props.openSession,
437
+ onClose: () => { setOpen(false) },
438
+ })
439
+ : null,
440
+ ])
441
+ }
442
+
443
+ /* ------------------------------------------------ 侧栏入口位次(顶栏工具槽 / 底栏) */
444
+
445
+ /**
446
+ * 顶栏工具槽位:面板行列表。宿主在「插件」(order 0)与「更多」启动器之间渲染
447
+ * 这些行,行内 icon + label 默认左对齐;点击切换主栏面板。
448
+ */
449
+ const TOP_TOOL_SEAT = 'sidebar.panellist'
450
+ /** 主栏整页槽位(keyed):面板行点击后在此渲染整页内容。 */
451
+ const MAIN_SEAT = 'main'
452
+ /** 管理台页 id(同时作为 `sidebar.panellist` 行的 id 与 `main` 的 key)。 */
453
+ const MANAGER_PANEL_ID = 'manage-session'
454
+
455
+ /**
456
+ * 宿主是否提供顶栏工具槽位(诊断用):面板行席位 + 主栏整页席位同时被声明才算。
457
+ * 实际位次判定走 apply 内的 inject 探测(服务面不保证暴露 specDynamic)。
458
+ * @param slots - 槽位服务面。
459
+ * @returns 两席都已声明时 `true`。
460
+ */
461
+ function hasPanelToolSeat(slots: StewardSlotsService): boolean {
462
+ if (typeof slots.specDynamic !== 'function') return false
463
+ try {
464
+ return slots.specDynamic(TOP_TOOL_SEAT) !== undefined && slots.specDynamic(MAIN_SEAT) !== undefined
465
+ } catch {
466
+ return false
467
+ }
468
+ }
469
+
470
+ export { hasPanelToolSeat }
471
+
472
+ /** 组合注销函数(逆序执行,幂等)。 */
473
+ function composeDisposers(offs: Array<() => void>): () => void {
474
+ let done = false
475
+ return () => {
476
+ if (done) return
477
+ done = true
478
+ for (const off of offs.reverse()) {
479
+ try { off() } catch { /* 单个席位解装失败不阻断其余回收 */ }
480
+ }
481
+ }
482
+ }
483
+
484
+ /**
485
+ * 按开关状态维持一对注册(主栏页 + 面板行)。快照缺字段一律按「可见」处理。
486
+ * @param scope - 设置 scope(缺失时视为缺省值)。
487
+ * @param visible - 可见性判定。
488
+ * @param mount - 可见时注册并返回注销函数。
489
+ * @returns 解装函数。
490
+ */
491
+ function gatedMount<V extends object>(
492
+ scope: BridgeScope<V> | undefined,
493
+ visible: (value: Partial<V>) => boolean,
494
+ mount: () => () => void,
495
+ ): () => void {
496
+ let dispose: (() => void) | undefined
497
+ const apply = (): void => {
498
+ const value = (scope?.getSnapshot?.().value ?? {}) as Partial<V>
499
+ const want = visible(value)
500
+ if (want && dispose === undefined) dispose = mount()
501
+ else if (!want && dispose !== undefined) {
502
+ const off = dispose
503
+ dispose = undefined
504
+ off()
505
+ }
506
+ }
507
+ apply()
508
+ const unsubscribe = scope?.subscribe(apply)
509
+ return () => {
510
+ unsubscribe?.()
511
+ if (dispose !== undefined) {
512
+ const off = dispose
513
+ dispose = undefined
514
+ off()
515
+ }
516
+ }
517
+ }
518
+
519
+ /** 底栏回落形态(老宿主 / 无顶栏工具行):一枚入口开对话框。 */
520
+ function mountFooterEntries(
521
+ slots: StewardSlotsService,
522
+ bound: BridgeScope<StewardConfig>,
523
+ openSession: (sessionId: string) => void,
524
+ ): () => void {
525
+ const off = slots.inject('sidebar.footer.action', () => slots.register(
526
+ { name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 6 },
527
+ (props: StewardFooterProps) => createElement(StewardEntry, {
528
+ ...props,
529
+ scope: bound as unknown as StewardCardScope,
530
+ openSession,
531
+ }),
532
+ ), 'dsh-manage-session: sidebar footer entry')
533
+ return composeDisposers([off])
534
+ }
535
+
536
+ /**
537
+ * 顶栏工具行形态:面板行(order 6,落在「插件」行之后、「更多」之前)加主栏整页。
538
+ * 点击行 → 宿主 `selectPanel(id)` 在主栏渲染整页;页内关闭动作回退到会话。
539
+ * @param closePanel - 关闭页回退动作(`layout.selectPanel(null)`)。
540
+ */
541
+ function mountTopToolEntries(
542
+ slots: StewardSlotsService,
543
+ bound: BridgeScope<StewardConfig>,
544
+ openSession: (sessionId: string) => void,
545
+ closePanel: () => void,
546
+ ): () => void {
547
+ /** 管理页:管理台 / 体检 双页签,页签可见性与底栏入口同源。 */
548
+ const ManagerPage = (): ReactElement => {
549
+ const snapshot = useSyncExternalStore(
550
+ (listener) => bound.subscribe(listener),
551
+ () => bound.getSnapshot(),
552
+ )
553
+ const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
554
+ return createElement(StewardPanel, {
555
+ variant: 'page',
556
+ healthCheck: config.healthCheck !== false,
557
+ openSession,
558
+ onClose: closePanel,
559
+ })
560
+ }
561
+
562
+ const off = gatedMount(bound, value => value.enabled !== false && value.healthCheck !== false, () =>
563
+ composeDisposers([
564
+ slots.inject(MAIN_SEAT, () => slots.register(
565
+ { name: MAIN_SEAT, key: MANAGER_PANEL_ID, locale: NS },
566
+ ManagerPage,
567
+ ), `dsh-manage-session: main page ${MANAGER_PANEL_ID}`),
568
+ slots.inject(TOP_TOOL_SEAT, () => slots.register(
569
+ {
570
+ name: TOP_TOOL_SEAT,
571
+ id: MANAGER_PANEL_ID,
572
+ order: 6,
573
+ label: () => translate(undefined, 'card.title'),
574
+ locale: NS,
575
+ },
576
+ ManagerToolIcon,
577
+ ), `dsh-manage-session: top tool row ${MANAGER_PANEL_ID}`),
578
+ ]))
579
+
580
+ return off
581
+ }
582
+
583
+ /** 顶栏行图标:会话管理(列表行 + 勾选,随行字色)。 */
584
+ function ManagerToolIcon({ size }: { size?: number }): ReactElement {
585
+ const edge = size ?? 16
586
+ return createElement('svg', {
587
+ width: edge, height: edge, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true,
588
+ },
589
+ createElement('path', { d: 'M2 4h12M2 8h12M2 12h7', stroke: 'currentColor', strokeWidth: 1.75, strokeLinecap: 'round' }),
590
+ createElement('path', { d: 'M11.2 11.2l1.5 1.5 2.6-3', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }))
591
+ }
592
+
593
+ /** 导出字典查找 type 供消费者复用。 */
594
+ export type { LocaleKey }