dsh-sidebar-suite 0.1.3 → 0.2.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.
package/lib/client.js CHANGED
@@ -1,1276 +1,2285 @@
1
- // dsh-sidebar-suite — Client half.
2
- //
3
- // The DSH left sidebar stacks two groups in one column: the panels group
4
- // (`nav` with a `panelList` class) and the workspaces group
5
- // (`[data-slot="sidebar.workspaces"]` inside its own region). They compete for
6
- // the same vertical space, so this plugin offers three layouts for them:
7
- //
8
- // a both groups fold independently
9
- // b the groups share one space and the control bar switches between them
10
- // c the workspaces stay put and the panels open as an overlay
11
- //
12
- // All state lives in attributes on <html>, the host's markup is never
13
- // restructured, and the only node this plugin adds is one control bar inserted
14
- // as a direct child of the sidebar root.
15
- window.__ModuleLoader__.load({
16
- id: 'dsh-sidebar-suite',
17
- factory(require) {
18
- 'use strict'
19
-
20
- var ENTRY = 'dsh-sidebar-suite'
21
- var NS = 'sidebar-suite'
22
- var LS_KEY = 'dsh-sidebar-suite:state'
23
- var STYLE_ID = 'dsh-sidebar-suite-style'
24
- // The plugin detail page is a React surface. Load React defensively: if the
25
- // module loader does not hand us `require`, the sidebar still works and we
26
- // simply skip registering the graphical settings page.
27
- var React = null
28
- try {
29
- React = typeof require === 'function' ? require('react') : null
30
- } catch (error) {
31
- React = null
32
- }
33
- // A client bundle can be applied more than once in the same page (a hot
34
- // client reload after the plugin's files change, or a second Loader entry
35
- // for the same package). Both runs would insert a bar and fight over the
36
- // same <html> attributes, so the first live run owns the sidebar.
37
- var OWNER_KEY = '__dshSidebarSuiteOwner'
38
-
39
- var DEFAULTS = { mode: 'b', showQuickSwitch: true, tab: 'ext', foldExt: false, foldWs: false }
40
- var MODES = ['a', 'b', 'c']
41
-
42
- var DICTS = {
43
- en: {
44
- 'bar.label': 'Sidebar layout',
45
- 'bar.local': 'Settings are stored on this device only (the official config layer is unavailable).',
46
- 'mode.a': 'Fold',
47
- 'mode.a.hint': 'Each group folds on its own.',
48
- 'mode.b': 'Toggle',
49
- 'mode.b.hint': 'The two groups share one space; choose which one is shown.',
50
- 'mode.c': 'Overlay',
51
- 'mode.c.hint': 'Workspaces stay visible; panels open as an overlay.',
52
- 'quick.label': 'Cycle',
53
- 'quick.hint': 'Cycle the sidebar layout.',
54
- 'fold.ext.collapse': 'Hide panels',
55
- 'fold.ext.expand': 'Show panels',
56
- 'fold.ext.hint': 'Hide or show the panels group.',
57
- 'fold.ws.collapse': 'Hide workspaces',
58
- 'fold.ws.expand': 'Show workspaces',
59
- 'fold.ws.hint': 'Hide or show the workspaces group.',
60
- 'tab.to.ext': 'Show panels',
61
- 'tab.to.ext.hint': 'Switch the shared space to the panels group.',
62
- 'tab.to.ws': 'Show workspaces',
63
- 'tab.to.ws.hint': 'Switch the shared space to the workspaces group.',
64
- 'pop.open': 'Open panels',
65
- 'pop.close': 'Close panels',
66
- 'pop.hint': 'Open or close the panels overlay.',
67
- 'settings.title': 'Default layout on startup',
68
- 'settings.help': 'Pick the layout DSH starts with. This writes the plugin config; switching from the sidebar bar is session-only.',
69
- 'settings.unavailable': 'The config service is unavailable right now, so the default cannot be changed here.',
70
- 'settings.current': 'Current default',
71
- },
72
- zh: {
73
- 'bar.label': '侧栏布局',
74
- 'bar.local': '设置仅保存在本机(官方配置层不可用)。',
75
- 'mode.a': '折叠',
76
- 'mode.a.hint': '两组各自收起。',
77
- 'mode.b': '标签页',
78
- 'mode.b.hint': '两组共用一栏,用标签页切换显示。',
79
- 'mode.c': '浮层',
80
- 'mode.c.hint': '工作区常驻,扩展以浮层展开。',
81
- 'quick.label': '换布局',
82
- 'quick.hint': '循环切换侧栏布局。',
83
- 'fold.ext.collapse': '收起扩展',
84
- 'fold.ext.expand': '展开扩展',
85
- 'fold.ext.hint': '收起或展开扩展区。',
86
- 'fold.ws.collapse': '收起工作区',
87
- 'fold.ws.expand': '展开工作区',
88
- 'fold.ws.hint': '收起或展开工作区。',
89
- 'tab.to.ext': '显示扩展',
90
- 'tab.to.ext.hint': '把共用的一栏切换到扩展区。',
91
- 'tab.to.ws': '显示工作区',
92
- 'tab.to.ws.hint': '把共用的一栏切换到工作区。',
93
- 'pop.open': '展开扩展',
94
- 'pop.close': '收起扩展',
95
- 'pop.hint': '打开或收起扩展浮层。',
96
- 'settings.title': '启动时的默认布局',
97
- 'settings.help': '选择 DSH 启动时使用的布局。这里会写入插件配置;在侧栏上临时切换不会改动它。',
98
- 'settings.unavailable': '配置服务暂时不可用,无法在这里修改默认布局。',
99
- 'settings.current': '当前默认',
100
- },
101
- }
102
-
103
- // The plugin detail page must read well in both languages regardless of
104
- // the UI locale, so the settings copy is deliberately bilingual in both
105
- // dictionaries. The long detail document (DETAIL.md) does the same.
106
- var BILINGUAL_SETTINGS = {
107
- 'settings.title': '启动时的默认布局 · Default layout on startup',
108
- 'settings.help': '选择 DSH 启动时使用的布局,这里会写入插件配置;在侧栏上临时切换只影响本次会话。 / Pick the layout DSH starts with — this writes the plugin config, while switching from the sidebar bar stays session-only.',
109
- 'settings.unavailable': '配置服务暂时不可用,无法在这里修改默认布局。 / The config service is unavailable right now, so the default cannot be changed here.',
110
- 'settings.current': '当前默认 / Current',
111
- 'settings.support': '喜欢这个插件?请作者喝杯咖啡 · Enjoy it? Buy me a coffee',
112
- 'settings.supportHint': '完全自愿,无任何回报承诺。用微信扫一扫即可;「设置 → 支持贡献者」里也登记了同一个码。 / Purely voluntary, no rewards promised. Scan with WeChat Pay; the same QR is registered under Settings → Support contributors.',
113
- 'settings.qrAlt': '微信支付收款码 / WeChat Pay QR',
114
- }
115
- for (var lang in DICTS) {
116
- if (!Object.prototype.hasOwnProperty.call(DICTS, lang)) continue
117
- for (var key in BILINGUAL_SETTINGS) {
118
- if (Object.prototype.hasOwnProperty.call(BILINGUAL_SETTINGS, key)) DICTS[lang][key] = BILINGUAL_SETTINGS[key]
119
- }
120
- }
121
-
122
- var CSS = [
123
- '[data-dsbs-bar]{display:flex;flex-wrap:wrap;align-items:center;gap:5px;padding:2px 0 6px;flex:none;min-width:0}',
124
- '[data-dsbs-bar] [hidden]{display:none!important}',
125
- '[data-dsbs-sep]{width:1px;height:12px;flex:none;border-radius:1px;margin:0 1px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.3))}',
126
- '[data-dsbs-bar] button{appearance:none;-webkit-appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,inherit);font:inherit;font-size:11px;line-height:15px;padding:2px 6px;border-radius:6px;cursor:pointer;user-select:none;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}',
127
- '[data-dsbs-bar] button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));color:var(--dsw-alias-label-primary,inherit)}',
128
- '[data-dsbs-bar] button[aria-pressed="true"]{background:var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.22));color:var(--dsw-alias-label-primary,inherit);font-weight:500}',
129
- '[data-dsbs-bar] button:focus-visible{outline:2px solid var(--dsw-alias-label-primary,currentColor);outline-offset:1px}',
130
- '[data-dsbs-bar] button[data-dsbs-btn="quick"]{margin-left:auto;color:var(--dsw-alias-label-tertiary,inherit);padding:2px 6px}',
131
- 'html[data-dsbs-narrow="true"] [data-dsbs-btn="quick"]{display:none}',
132
- 'html[data-dsbs-rail="true"] [data-dsbs-bar]{display:none}',
133
- // a — each group folds on its own
134
- 'html[data-dsbs-mode="a"][data-dsbs-fold-ext="true"] nav[data-dsbs-ext]{display:none}',
135
- 'html[data-dsbs-mode="a"][data-dsbs-fold-ws="true"] div:has(> [data-slot="sidebar.workspaces"]){display:none}',
136
- // b — the two groups are mutually exclusive
137
- 'html[data-dsbs-mode="b"][data-dsbs-tab="ext"] div:has(> [data-slot="sidebar.workspaces"]){display:none}',
138
- 'html[data-dsbs-mode="b"][data-dsbs-tab="ws"] nav[data-dsbs-ext]{display:none}',
139
- // c — panels become an overlay above the workspaces
140
- 'html[data-dsbs-mode="c"]:not([data-dsbs-pop="true"]) nav[data-dsbs-ext]{display:none}',
141
- 'html[data-dsbs-mode="c"][data-dsbs-pop="true"] nav[data-dsbs-ext]{display:flex;flex-direction:column;gap:2px;position:absolute;z-index:60;left:4px;right:4px;top:var(--dsbs-pop-top,64px);max-height:60vh;overflow:auto;padding:6px;border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base,#fff));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3));box-shadow:0 12px 32px rgba(0,0,0,.24)}',
142
- // the overlay needs a positioned containing block; the sidebar root is the
143
- // only element with the panels nav as a direct child
144
- 'div:has(> nav[data-dsbs-ext]){position:relative}',
145
- // Plugin Manager detail page: `.detailSections` is already a column flex,
146
- // so `order:-1` lifts this bundle's configuration section above the long
147
- // detail document. Scoped by data-plugin-detail, no other bundle is hit.
148
- '[data-plugin-detail="dsh-sidebar-suite"] [data-plugin-config]{order:-1;box-sizing:border-box;padding:14px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.28));border-radius:12px;background:var(--dsw-alias-bg-subtle,rgba(127,127,127,.05))}',
149
- '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-support]{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:2px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))}',
150
- '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-support] img{width:96px;height:96px;object-fit:contain;box-sizing:border-box;padding:4px;border-radius:8px;background:#fff;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))}',
151
- '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-support] [data-dsbs-support-text]{flex:1 1 200px;min-width:0;display:flex;flex-direction:column;gap:4px}',
152
- ].join('\n')
153
-
154
- function assign(target) {
155
- for (var i = 1; i < arguments.length; i++) {
156
- var source = arguments[i]
157
- if (!source || typeof source !== 'object') continue
158
- for (var key in source) {
159
- if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key]
160
- }
161
- }
162
- return target
163
- }
164
-
165
- function safeGet(ctx, name) {
166
- if (!ctx) return undefined
167
- try {
168
- if (typeof ctx.get === 'function') return ctx.get(name)
169
- } catch (error) {
170
- void error
171
- }
172
- return undefined
173
- }
174
-
175
- function isExtNav(node) {
176
- return !!node && node.tagName === 'NAV' && /panelList/.test(node.className || '')
177
- }
178
-
179
- // Resolved on every sync: the sidebar is rebuilt by React on session
180
- // changes, so nothing here may be cached.
181
- function resolveSidebar() {
182
- if (typeof document === 'undefined') return null
183
- var workspaces = document.querySelector('[data-slot="sidebar.workspaces"]')
184
- if (!workspaces) return null
185
- var regionArea = workspaces.parentElement
186
- if (!regionArea) return null
187
- var node = regionArea
188
- var root = null
189
- var nav = null
190
- while (node && node !== document.body && node !== document.documentElement) {
191
- var children = node.children
192
- for (var i = 0; i < children.length; i++) {
193
- var child = children[i]
194
- if (child === regionArea) continue
195
- if (isExtNav(child)) {
196
- nav = child
197
- root = node
198
- break
199
- }
200
- }
201
- if (root) break
202
- node = node.parentElement
203
- }
204
- if (!root) {
205
- root = regionArea.parentElement
206
- if (!root) return null
207
- for (var j = 0; j < root.children.length; j++) {
208
- if (isExtNav(root.children[j])) {
209
- nav = root.children[j]
210
- break
211
- }
212
- }
213
- }
214
- return { root: root, nav: nav, regionArea: regionArea, workspaces: workspaces }
215
- }
216
-
217
- function readLocal() {
218
- try {
219
- var raw = localStorage.getItem(LS_KEY)
220
- if (!raw) return {}
221
- var parsed = JSON.parse(raw)
222
- return parsed && typeof parsed === 'object' ? parsed : {}
223
- } catch (error) {
224
- return {}
225
- }
226
- }
227
-
228
- function writeLocal(value) {
229
- try {
230
- localStorage.setItem(LS_KEY, JSON.stringify(value))
231
- } catch (error) {
232
- void error
233
- }
234
- }
235
-
236
- function adoptValues(raw) {
237
- var out = assign({}, DEFAULTS)
238
- if (!raw || typeof raw !== 'object') return out
239
- if (MODES.indexOf(raw.mode) >= 0) out.mode = raw.mode
240
- if (typeof raw.showQuickSwitch === 'boolean') out.showQuickSwitch = raw.showQuickSwitch
241
- if (raw.tab === 'ext' || raw.tab === 'ws') out.tab = raw.tab
242
- if (typeof raw.foldExt === 'boolean') out.foldExt = raw.foldExt
243
- if (typeof raw.foldWs === 'boolean') out.foldWs = raw.foldWs
244
- return out
245
- }
246
-
247
- // The locale service merges dictionaries per namespace and keeps entries
248
- // registered by earlier client generations, so a bound translator can mix
249
- // stale and current strings (that is how a page ends up with Chinese modes
250
- // and English captions). The bar's own labels only need zh/en, so they are
251
- // resolved from the active locale id and this plugin's dictionaries.
252
- function detectLang(localeService) {
253
- var candidates = []
254
- try {
255
- if (localeService && typeof localeService.getSnapshot === 'function') {
256
- var snapshot = localeService.getSnapshot()
257
- if (snapshot && snapshot.active) candidates.push(snapshot.active)
258
- }
259
- if (localeService && typeof localeService.getLocale === 'function') {
260
- var active = localeService.getLocale()
261
- if (typeof active === 'string') candidates.push(active)
262
- else if (active && active.active) candidates.push(active.active)
263
- }
264
- } catch (error) {
265
- void error
266
- }
267
- try {
268
- candidates.push(document.documentElement.lang)
269
- } catch (error) {
270
- void error
271
- }
272
- try {
273
- candidates.push(navigator.language)
274
- } catch (error) {
275
- void error
276
- }
277
- for (var i = 0; i < candidates.length; i++) {
278
- if (/^zh/i.test(String(candidates[i] || ''))) return 'zh'
279
- }
280
- return 'en'
281
- }
282
-
283
- function createTranslator(localeService) {
284
- var lang = 'en'
285
- try {
286
- if (localeService && typeof localeService.register === 'function') {
287
- localeService.register(NS, DICTS)
288
- }
289
- } catch (error) {
290
- void error
291
- }
292
- function refresh() {
293
- lang = detectLang(localeService)
294
- }
295
- refresh()
296
- var t = function (key) {
297
- return DICTS[lang][key] || DICTS.en[key] || key
298
- }
299
- t.refresh = refresh
300
- return t
301
- }
302
-
303
- function apply(ctx) {
304
- if (typeof document === 'undefined' || typeof window === 'undefined') return
305
-
306
- // Claim the sidebar before touching the DOM. A second apply takes over
307
- // instead of stacking: the run that already owns the sidebar is torn down
308
- // first, so a hot reload swaps the bar in place and never leaves stale
309
- // buttons or html attributes behind.
310
- var owner = { alive: true, teardown: null }
311
- try {
312
- var current = window[OWNER_KEY]
313
- if (current && current.alive) {
314
- try {
315
- if (typeof current.teardown === 'function') current.teardown()
316
- } catch (error) {
317
- void error
318
- }
319
- current.alive = false
320
- }
321
- window[OWNER_KEY] = owner
322
- } catch (error) {
323
- void error
324
- }
325
-
326
- var html = document.documentElement
327
- var localeService = null
328
- try {
329
- localeService = ctx.locale
330
- } catch (error) {
331
- localeService = null
332
- }
333
- if (!localeService) localeService = safeGet(ctx, 'locale')
334
- var t = createTranslator(localeService)
335
-
336
- var config = { controller: null, values: assign({}, DEFAULTS), local: false }
337
- var runtime = {
338
- disposed: false,
339
- // `mode` in the profile is the startup default. Picking a layout from
340
- // the bar is session-only and must never rewrite the config form.
341
- sessionMode: null,
342
- configMode: null,
343
- bar: null,
344
- root: null,
345
- nav: null,
346
- regionArea: null,
347
- popOpen: false,
348
- rail: false,
349
- railSaved: null,
350
- hidden: { ext: false, ws: false },
351
- observedRoot: null,
352
- resizeObserver: null,
353
- mutationObserver: null,
354
- raf: 0,
355
- }
356
-
357
- var manualCleanups = []
358
-
359
- // Register a resource and its disposal through the host context when it
360
- // offers one; otherwise keep the disposer for the end of this run.
361
- function effect(fn, label) {
362
- var disposer = null
363
- try {
364
- if (typeof ctx.effect === 'function') {
365
- ctx.effect(fn, label)
366
- return
367
- }
368
- } catch (error) {
369
- void error
370
- }
371
- try {
372
- disposer = fn()
373
- } catch (error) {
374
- void error
375
- }
376
- if (typeof disposer === 'function') manualCleanups.push(disposer)
377
- }
378
-
379
- function setFlag(element, name, on) {
380
- if (!element) return
381
- if (on) element.setAttribute(name, 'true')
382
- else element.removeAttribute(name)
383
- }
384
-
385
- // ---- markup -------------------------------------------------------
386
-
387
- function makeButton(key, labelKey, hintKey, onClick) {
388
- var button = document.createElement('button')
389
- button.type = 'button'
390
- button.setAttribute('data-dsbs-btn', key)
391
- if (labelKey) button.setAttribute('data-dsbs-label', labelKey)
392
- if (hintKey) button.setAttribute('data-dsbs-hint', hintKey)
393
- if (labelKey) button.textContent = t(labelKey)
394
- if (hintKey) button.title = t(hintKey)
395
- button.addEventListener('click', function (event) {
396
- event.preventDefault()
397
- event.stopPropagation()
398
- onClick(event)
399
- })
400
- return button
401
- }
402
-
403
- function makeSeparator() {
404
- var separator = document.createElement('span')
405
- separator.setAttribute('data-dsbs-sep', '')
406
- separator.setAttribute('aria-hidden', 'true')
407
- return separator
408
- }
409
-
410
- // One row of plain text buttons, the shape the bar had in its first
411
- // version: the filled button is the chosen layout, and everything after
412
- // the divider names the action its click performs in that layout.
413
- function buildBar() {
414
- var bar = document.createElement('div')
415
- bar.setAttribute('data-dsbs-bar', '')
416
- bar.setAttribute('role', 'toolbar')
417
-
418
- var i
419
- for (i = 0; i < MODES.length; i++) {
420
- var mode = MODES[i]
421
- bar.appendChild(makeButton('mode-' + mode, 'mode.' + mode, 'mode.' + mode + '.hint', (function (value) {
422
- return function () { setMode(value) }
423
- })(mode)))
424
- }
425
-
426
- var separator = makeSeparator()
427
- bar.appendChild(separator)
428
-
429
- var foldExt = makeButton('fold-ext', 'fold.ext.collapse', 'fold.ext.hint', function () { setField('foldExt', !config.values.foldExt) })
430
- var foldWs = makeButton('fold-ws', 'fold.ws.collapse', 'fold.ws.hint', function () { setField('foldWs', !config.values.foldWs) })
431
- var tabSwap = makeButton('tab-swap', 'tab.to.ws', 'tab.to.ws.hint', function () {
432
- setField('tab', config.values.tab === 'ext' ? 'ws' : 'ext')
433
- })
434
- var pop = makeButton('pop', 'pop.open', 'pop.hint', function () {
435
- runtime.popOpen = !runtime.popOpen
436
- scheduleSync()
437
- })
438
- pop.setAttribute('aria-expanded', 'false')
439
- var quick = makeButton('quick', 'quick.label', 'quick.hint', function () {
440
- var index = MODES.indexOf(config.values.mode)
441
- setMode(MODES[(index + 1) % MODES.length])
442
- })
443
-
444
- bar.appendChild(foldExt)
445
- bar.appendChild(foldWs)
446
- bar.appendChild(tabSwap)
447
- bar.appendChild(pop)
448
- bar.appendChild(quick)
449
-
450
- runtime.parts = { separator: separator, context: [foldExt, foldWs, tabSwap, pop] }
451
- return bar
452
- }
453
-
454
- function ensureBar(fx) {
455
- var bar = runtime.bar
456
- if (bar && bar.parentNode !== fx.root) {
457
- try {
458
- bar.parentNode.removeChild(bar)
459
- } catch (error) {
460
- void error
461
- }
462
- bar = null
463
- }
464
- if (!bar) {
465
- bar = buildBar()
466
- runtime.bar = bar
467
- }
468
- var anchor = fx.nav || fx.regionArea
469
- if (anchor && anchor.parentNode === fx.root) {
470
- if (bar.nextSibling !== anchor) fx.root.insertBefore(bar, anchor)
471
- } else if (bar.parentNode !== fx.root) {
472
- fx.root.appendChild(bar)
473
- }
474
- return bar
475
- }
476
-
477
- function barButton(key) {
478
- if (!runtime.bar) return null
479
- return runtime.bar.querySelector('[data-dsbs-btn="' + key + '"]')
480
- }
481
-
482
- function setVisible(key, visible) {
483
- var button = barButton(key)
484
- if (!button) return
485
- if (visible) button.removeAttribute('hidden')
486
- else button.setAttribute('hidden', '')
487
- }
488
-
489
- function relabel() {
490
- if (!runtime.bar) return
491
- var nodes = runtime.bar.querySelectorAll('[data-dsbs-label]')
492
- for (var i = 0; i < nodes.length; i++) {
493
- var node = nodes[i]
494
- var labelKey = node.getAttribute('data-dsbs-label')
495
- var hintKey = node.getAttribute('data-dsbs-hint')
496
- // Rewriting unchanged text on every sync would drop focus from a
497
- // button reached with the keyboard, so only touch what changed.
498
- if (labelKey) {
499
- var label = t(labelKey)
500
- if (node.textContent !== label) node.textContent = label
501
- }
502
- if (hintKey) {
503
- var hint = t(hintKey)
504
- if (node.title !== hint) node.title = hint
505
- }
506
- }
507
- var barLabel = t('bar.label')
508
- if (runtime.bar.getAttribute('aria-label') !== barLabel) {
509
- runtime.bar.setAttribute('aria-label', barLabel)
510
- }
511
- }
512
-
513
- // ---- visible state ------------------------------------------------
514
-
515
- function closeOpenOverlays(element) {
516
- try {
517
- var open = element.querySelector('dialog[open], [role="menu"][data-state="open"], [data-state="open"]')
518
- if (!open) return
519
- open.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
520
- } catch (error) {
521
- void error
522
- }
523
- }
524
-
525
- function setGroupHidden(element, group, hidden) {
526
- if (!element) return
527
- var was = runtime.hidden[group] === true
528
- if (hidden) {
529
- if (!was) closeOpenOverlays(element)
530
- element.setAttribute('aria-hidden', 'true')
531
- element.setAttribute('inert', '')
532
- } else {
533
- element.removeAttribute('aria-hidden')
534
- element.removeAttribute('inert')
535
- }
536
- if (was === hidden) return
537
- runtime.hidden[group] = hidden
538
- if (!hidden) {
539
- // Panels and menus position themselves from measured geometry; they
540
- // were hidden while that was measured, so ask them to measure again.
541
- try {
542
- window.dispatchEvent(new Event('resize'))
543
- } catch (error) {
544
- void error
545
- }
546
- }
547
- }
548
-
549
- function positionPop(fx) {
550
- if (!fx || !fx.root || !runtime.bar) return
551
- // Only the overlay is positioned; skip the two rect reads otherwise so
552
- // the heartbeat stays cheap.
553
- if (config.values.mode !== 'c' || !runtime.popOpen) return
554
- try {
555
- var rootRect = fx.root.getBoundingClientRect()
556
- var barRect = runtime.bar.getBoundingClientRect()
557
- var top = Math.round(barRect.bottom - rootRect.top + 4)
558
- if (!isFinite(top) || top < 0) top = 0
559
- fx.root.style.setProperty('--dsbs-pop-top', top + 'px')
560
- } catch (error) {
561
- void error
562
- }
563
- }
564
-
565
- function paint() {
566
- if (runtime.disposed) return
567
- var state = config.values
568
- html.setAttribute('data-dsbs-mode', state.mode)
569
- html.setAttribute('data-dsbs-tab', state.tab)
570
- html.setAttribute('data-dsbs-quick', state.showQuickSwitch ? 'on' : 'off')
571
- setFlag(html, 'data-dsbs-fold-ext', state.foldExt === true)
572
- setFlag(html, 'data-dsbs-fold-ws', state.foldWs === true)
573
- setFlag(html, 'data-dsbs-pop', runtime.popOpen && state.mode === 'c')
574
- setFlag(html, 'data-dsbs-rail', runtime.rail)
575
-
576
- var fx = resolveSidebar()
577
- runtime.root = fx ? fx.root : null
578
- runtime.nav = fx ? fx.nav : null
579
- runtime.regionArea = fx ? fx.regionArea : null
580
- if (!fx) return
581
-
582
- if (fx.nav && !fx.nav.hasAttribute('data-dsbs-ext')) fx.nav.setAttribute('data-dsbs-ext', '')
583
- ensureBar(fx)
584
- // Bars left behind by an earlier client generation are dead weight and
585
- // read as a duplicated control; keep exactly one.
586
- var strayBars = document.querySelectorAll('[data-dsbs-bar]')
587
- for (var b = 0; b < strayBars.length; b++) {
588
- if (strayBars[b] === runtime.bar) continue
589
- if (strayBars[b].parentNode) strayBars[b].parentNode.removeChild(strayBars[b])
590
- }
591
-
592
- // With no panels to show, the buttons that act on that group are
593
- // pointless, and hiding the workspaces would leave an empty column.
594
- var hasExt = !!fx.nav
595
- var showFold = state.mode === 'a'
596
- var showTabs = state.mode === 'b'
597
- var showPop = state.mode === 'c'
598
- setVisible('fold-ext', showFold && hasExt)
599
- setVisible('fold-ws', showFold)
600
- setVisible('tab-swap', showTabs && hasExt)
601
- setVisible('pop', showPop && hasExt)
602
- setVisible('quick', state.showQuickSwitch === true)
603
-
604
- // The label states what the click will do, so "收起扩展" and "展开扩展"
605
- // never coexist with a stale state.
606
- var foldExtButton = barButton('fold-ext')
607
- if (foldExtButton) foldExtButton.setAttribute('data-dsbs-label', state.foldExt ? 'fold.ext.expand' : 'fold.ext.collapse')
608
- var foldWsButton = barButton('fold-ws')
609
- if (foldWsButton) foldWsButton.setAttribute('data-dsbs-label', state.foldWs ? 'fold.ws.expand' : 'fold.ws.collapse')
610
- var popButton = barButton('pop')
611
- if (popButton) popButton.setAttribute('data-dsbs-label', runtime.popOpen && showPop ? 'pop.close' : 'pop.open')
612
- var tabSwapButton = barButton('tab-swap')
613
- if (tabSwapButton) {
614
- tabSwapButton.setAttribute('data-dsbs-label', state.tab === 'ext' ? 'tab.to.ws' : 'tab.to.ext')
615
- tabSwapButton.setAttribute('data-dsbs-hint', state.tab === 'ext' ? 'tab.to.ws.hint' : 'tab.to.ext.hint')
616
- }
617
-
618
- // The divider only makes sense while at least one action button is
619
- // visible; an empty layout (no panels) can leave nothing after it.
620
- var parts = runtime.parts
621
- if (parts) {
622
- var visibleContext = 0
623
- for (var c = 0; c < parts.context.length; c++) {
624
- if (!parts.context[c].hasAttribute('hidden')) visibleContext++
625
- }
626
- if (visibleContext > 0) parts.separator.removeAttribute('hidden')
627
- else parts.separator.setAttribute('hidden', '')
628
- }
629
-
630
- relabel()
631
-
632
- var i
633
- for (i = 0; i < MODES.length; i++) {
634
- var modeButton = barButton('mode-' + MODES[i])
635
- if (modeButton) modeButton.setAttribute('aria-pressed', state.mode === MODES[i] ? 'true' : 'false')
636
- }
637
- if (foldExtButton) foldExtButton.setAttribute('aria-expanded', state.foldExt ? 'false' : 'true')
638
- if (foldWsButton) foldWsButton.setAttribute('aria-expanded', state.foldWs ? 'false' : 'true')
639
- if (popButton) {
640
- popButton.setAttribute('aria-expanded', runtime.popOpen && showPop ? 'true' : 'false')
641
- }
642
- if (runtime.bar) {
643
- runtime.bar.title = config.local ? t('bar.local') : t('bar.label')
644
- runtime.bar.setAttribute('aria-label', t('bar.label'))
645
- }
646
-
647
- var extHidden
648
- var wsHidden
649
- if (state.mode === 'a') {
650
- extHidden = state.foldExt === true
651
- wsHidden = state.foldWs === true
652
- } else if (state.mode === 'b') {
653
- extHidden = state.tab !== 'ext'
654
- // No panels: keep the workspaces visible rather than the empty half.
655
- wsHidden = hasExt && state.tab === 'ext'
656
- } else {
657
- extHidden = !runtime.popOpen
658
- wsHidden = false
659
- }
660
- setGroupHidden(fx.nav, 'ext', extHidden)
661
- setGroupHidden(fx.regionArea, 'ws', wsHidden)
662
-
663
- observeRoot(fx.root)
664
- positionPop(fx)
665
- }
666
-
667
- // ---- state changes ------------------------------------------------
668
-
669
- function setMode(mode) {
670
- if (MODES.indexOf(mode) < 0) return
671
- if (mode !== 'c' && runtime.popOpen) runtime.popOpen = false
672
- runtime.sessionMode = mode
673
- setField('mode', mode)
674
- }
675
-
676
- function setField(field, value) {
677
- var patch = {}
678
- patch[field] = value
679
- config.values = assign({}, config.values, patch)
680
-
681
- // `mode` is the startup default: keep it out of both the config form
682
- // and localStorage, so an in-session pick lives only until reload.
683
- // The other fields (tab / folds) still persist as before.
684
- if (field !== 'mode') {
685
- if (config.controller && typeof config.controller.mutate === 'function') {
686
- var revision
687
- try {
688
- var snapshot = config.controller.getSnapshot()
689
- revision = snapshot && snapshot.revision
690
- } catch (error) {
691
- revision = undefined
692
- }
693
- var ops = [{ op: 'set', path: [field], value: value }]
694
- try {
695
- var result = config.controller.mutate(ops, revision)
696
- if (result && typeof result.catch === 'function') result.catch(function () {})
697
- } catch (error) {
698
- void error
699
- }
700
- } else {
701
- var stored = assign({}, DEFAULTS, readLocal())
702
- stored[field] = value
703
- writeLocal(stored)
704
- config.local = true
705
- }
706
- }
707
- scheduleSync()
708
- }
709
-
710
- // ---- observation --------------------------------------------------
711
-
712
- function measureRail(fx) {
713
- var root = (fx && fx.root) || runtime.root
714
- if (!root) return
715
- var width = 0
716
- try {
717
- width = root.getBoundingClientRect().width
718
- } catch (error) {
719
- width = 0
720
- }
721
- var rail = width > 0 && width < 100
722
- // Below this width the bar cannot hold a full row once the cycle icon
723
- // is included, and a wrapped icon on its own line reads as debris.
724
- var narrow = width > 0 && width < 300
725
- if (narrow !== runtime.narrow) {
726
- runtime.narrow = narrow
727
- setFlag(html, 'data-dsbs-narrow', narrow)
728
- }
729
- if (rail === runtime.rail) return
730
- runtime.rail = rail
731
- if (rail) {
732
- runtime.railSaved = {
733
- mode: config.values.mode,
734
- tab: config.values.tab,
735
- foldExt: config.values.foldExt,
736
- foldWs: config.values.foldWs,
737
- }
738
- } else if (runtime.railSaved) {
739
- // Leaving the rail restores what the user had before the column was
740
- // squeezed; the bar is not reachable while the rail is showing.
741
- config.values = assign({}, config.values, runtime.railSaved)
742
- runtime.railSaved = null
743
- }
744
- }
745
-
746
- var pending = false
747
- function scheduleSync() {
748
- if (runtime.disposed || pending) return
749
- pending = true
750
- Promise.resolve().then(function () {
751
- pending = false
752
- if (!runtime.disposed) sync()
753
- })
754
- }
755
-
756
- function sync() {
757
- if (runtime.disposed) return
758
- var fx = resolveSidebar()
759
- measureRail(fx)
760
- paint()
761
- }
762
-
763
- function observeRoot(root) {
764
- if (!root || runtime.observedRoot === root) return
765
- if (runtime.resizeObserver) {
766
- try {
767
- runtime.resizeObserver.disconnect()
768
- } catch (error) {
769
- void error
770
- }
771
- }
772
- if (runtime.mutationObserver) {
773
- try {
774
- runtime.mutationObserver.disconnect()
775
- } catch (error) {
776
- void error
777
- }
778
- }
779
- runtime.observedRoot = root
780
- if (typeof ResizeObserver === 'function') {
781
- if (!runtime.resizeObserver) {
782
- runtime.resizeObserver = new ResizeObserver(function () { scheduleSync() })
783
- }
784
- try {
785
- runtime.resizeObserver.observe(root)
786
- } catch (error) {
787
- void error
788
- }
789
- }
790
- if (typeof MutationObserver === 'function') {
791
- if (!runtime.mutationObserver) {
792
- runtime.mutationObserver = new MutationObserver(function () { scheduleSync() })
793
- }
794
- try {
795
- // Only the root's own child list: React rebuilds the groups there,
796
- // and watching anything wider would turn every keystroke into work.
797
- runtime.mutationObserver.observe(root, { childList: true })
798
- } catch (error) {
799
- void error
800
- }
801
- }
802
- }
803
-
804
- // ---- configuration -------------------------------------------------
805
-
806
- function syncFromController() {
807
- if (!config.controller) return
808
- var snapshot = null
809
- try {
810
- snapshot = config.controller.getSnapshot()
811
- } catch (error) {
812
- snapshot = null
813
- }
814
- if (!snapshot || typeof snapshot !== 'object') return
815
- var next = adoptValues(snapshot.value)
816
- // The config form owns the startup default. A push caused by another
817
- // field must not undo the mode picked in this session; but a genuine
818
- // external change (settings page or cordis.patch.yml) wins and becomes
819
- // the new default for the next launch.
820
- if (runtime.configMode === null) {
821
- runtime.configMode = next.mode
822
- } else if (next.mode !== runtime.configMode) {
823
- runtime.configMode = next.mode
824
- runtime.sessionMode = null
825
- } else if (runtime.sessionMode) {
826
- next.mode = runtime.sessionMode
827
- }
828
- config.values = next
829
- config.local = false
830
- }
831
-
832
- function tryAdopt(service) {
833
- if (!service || typeof service.get !== 'function') return false
834
- if (config.controller) return true
835
- var controller = null
836
- try {
837
- controller = service.get(ENTRY)
838
- } catch (error) {
839
- controller = null
840
- }
841
- if (!controller) return false
842
- config.controller = controller
843
- config.local = false
844
- syncFromController()
845
- var off = null
846
- try {
847
- if (typeof controller.subscribe === 'function') {
848
- off = controller.subscribe(function () {
849
- syncFromController()
850
- scheduleSync()
851
- })
852
- }
853
- } catch (error) {
854
- void error
855
- }
856
- effect(function () {
857
- return function () {
858
- if (typeof off !== 'function') return
859
- try {
860
- off()
861
- } catch (error) {
862
- void error
863
- }
864
- }
865
- }, 'sidebar-suite: config subscription')
866
- scheduleSync()
867
- return true
868
- }
869
-
870
- function fallbackToLocal() {
871
- if (config.controller) return
872
- config.local = true
873
- config.values = adoptValues(readLocal())
874
- scheduleSync()
875
- }
876
-
877
- // ---- plugin detail page: graphical default-mode picker -------------
878
-
879
- function configController() {
880
- if (config.controller) return config.controller
881
- try {
882
- var service = safeGet(ctx, 'configForms')
883
- if (service && typeof service.get === 'function') return service.get(ENTRY)
884
- } catch (error) {
885
- void error
886
- }
887
- return null
888
- }
889
-
890
- function readDefaultMode() {
891
- var controller = configController()
892
- try {
893
- var snapshot = controller && controller.getSnapshot && controller.getSnapshot()
894
- var value = snapshot && snapshot.value
895
- return MODES.indexOf(value && value.mode) >= 0 ? value.mode : DEFAULTS.mode
896
- } catch (error) {
897
- return DEFAULTS.mode
898
- }
899
- }
900
-
901
- function writeDefaultMode(mode) {
902
- if (MODES.indexOf(mode) < 0) return false
903
- var controller = configController()
904
- if (!controller || typeof controller.mutate !== 'function') return false
905
- try {
906
- var snapshot = controller.getSnapshot && controller.getSnapshot()
907
- var result = controller.mutate(
908
- [{ op: 'set', path: ['mode'], value: mode }],
909
- snapshot && snapshot.revision
910
- )
911
- if (result && typeof result.catch === 'function') result.catch(function () {})
912
- // The settings page and the sidebar share this client run, so adopt
913
- // the new default immediately instead of waiting for a config push.
914
- runtime.sessionMode = null
915
- runtime.configMode = mode
916
- config.values = assign({}, config.values, { mode: mode })
917
- if (runtime.popOpen && mode !== 'c') runtime.popOpen = false
918
- scheduleSync()
919
- return true
920
- } catch (error) {
921
- return false
922
- }
923
- }
924
-
925
- // The Host serves images that sit beside a bundle's detail document.
926
- // This is the same URL the Plugin Manager's own markdown renderer uses,
927
- // so the small QR in the pinned settings card needs no base64 payload.
928
- function supportQrUrl() {
929
- try {
930
- if (typeof document === 'undefined' || !document.baseURI) return ''
931
- var query = 'bundle=' + encodeURIComponent(ENTRY) +
932
- '&document=' + encodeURIComponent('DETAIL.md') +
933
- '&path=' + encodeURIComponent('assets/wechat-qr.png')
934
- return new URL('api/plugin-manager/detail-file?' + query, document.baseURI).href
935
- } catch (error) {
936
- return ''
937
- }
938
- }
939
-
940
- // `plugins.bundle.config` is the keyed slot the Plugin Manager declares
941
- // for a bundle's own configuration page. The component reads and writes
942
- // the same configForms controller as the sidebar, so a click here becomes
943
- // the new startup default and the sidebar follows it immediately.
944
- function makeModePicker(h, ReactLib) {
945
- var box = function (extra) {
946
- return h('span', { style: assign({
947
- display: 'block', borderRadius: '2px',
948
- background: 'var(--dsw-alias-label-tertiary,rgba(127,127,127,.5))'
949
- }, extra || {}) })
950
- }
951
- var frame = function (extra, first, second) {
952
- return h('span', { style: assign({
953
- display: 'flex', flexDirection: 'column', width: '36px', height: '24px',
954
- padding: '2px', gap: '2px', boxSizing: 'border-box', position: 'relative',
955
- borderRadius: '4px',
956
- border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))'
957
- }, extra || {}) }, first || null, second || null)
958
- }
959
- function mini(mode) {
960
- if (mode === 'a') {
961
- return frame(null, box({ width: '100%', height: '7px' }), box({ width: '100%', height: '7px' }))
962
- }
963
- if (mode === 'b') {
964
- return frame(null, box({ width: '60%', height: '4px' }), box({ width: '100%', height: '11px' }))
965
- }
966
- return frame(null,
967
- box({ width: '100%', height: '100%', opacity: '0.35' }),
968
- box({
969
- position: 'absolute', left: '9px', top: '5px', width: '18px', height: '13px',
970
- background: 'var(--dsw-alias-label-primary,currentColor)', opacity: '0.85',
971
- boxShadow: '0 2px 6px rgba(0,0,0,.28)'
972
- })
973
- )
974
- }
975
-
976
- return function ModePicker() {
977
- var state = ReactLib.useState(readDefaultMode())
978
- var mode = state[0]
979
- var setMode = state[1]
980
- ReactLib.useEffect(function () {
981
- var controller = configController()
982
- if (!controller || typeof controller.subscribe !== 'function') return
983
- var off = null
984
- try {
985
- off = controller.subscribe(function () { setMode(readDefaultMode()) })
986
- } catch (error) {
987
- void error
988
- }
989
- return function () {
990
- try { if (typeof off === 'function') off() } catch (error) { void error }
991
- }
992
- }, [])
993
-
994
- var available = !!configController()
995
- var cardBase = {
996
- display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '5px',
997
- flex: '1 1 140px', minWidth: '140px', maxWidth: '230px', padding: '10px',
998
- borderRadius: '10px', font: 'inherit', textAlign: 'left', cursor: 'pointer',
999
- color: 'var(--dsw-alias-label-primary,inherit)',
1000
- border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))',
1001
- background: 'transparent'
1002
- }
1003
- return h('div', { 'data-dsbs-config': ENTRY, style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
1004
- h('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('settings.title')),
1005
- h('div', { style: { fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary,inherit)' } }, t('settings.help')),
1006
- h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
1007
- MODES.map(function (value) {
1008
- var active = value === mode
1009
- return h('button', {
1010
- key: value, type: 'button',
1011
- 'aria-pressed': active ? 'true' : 'false',
1012
- title: t('mode.' + value + '.hint'),
1013
- disabled: !available,
1014
- style: assign({}, cardBase, active ? {
1015
- borderColor: 'var(--dsw-alias-label-primary,currentColor)',
1016
- background: 'var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.22))'
1017
- } : {}),
1018
- onClick: function () {
1019
- if (writeDefaultMode(value)) setMode(value)
1020
- }
1021
- },
1022
- mini(value),
1023
- h('span', { style: { fontSize: '12px', fontWeight: active ? '600' : '400' } },
1024
- t('mode.' + value) + (active ? ' · ' + t('settings.current') : '')),
1025
- h('span', { style: { fontSize: '11px', lineHeight: '15px', color: 'var(--dsw-alias-label-secondary,inherit)' } },
1026
- t('mode.' + value + '.hint'))
1027
- )
1028
- })
1029
- ),
1030
- available ? null : h('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary,inherit)' } }, t('settings.unavailable')),
1031
- h('div', { 'data-dsbs-support': ENTRY },
1032
- supportQrUrl() ? h('img', {
1033
- src: supportQrUrl(),
1034
- alt: t('settings.qrAlt'),
1035
- onError: function (event) {
1036
- try { event.currentTarget.style.display = 'none' } catch (error) { void error }
1037
- }
1038
- }) : null,
1039
- h('div', { 'data-dsbs-support-text': ENTRY },
1040
- h('div', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('settings.support')),
1041
- h('div', { style: { fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary,inherit)' } }, t('settings.supportHint'))
1042
- )
1043
- )
1044
- )
1045
- }
1046
- }
1047
-
1048
- // Register only when React and the slots service are both available.
1049
- // Any failure here is contained: the sidebar control bar is unaffected.
1050
- if (React && React.createElement && React.useState && React.useEffect && typeof ctx.inject === 'function') {
1051
- try {
1052
- ctx.inject(['slots'], function (scope) {
1053
- var slots = scope && scope.slots
1054
- if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') return
1055
- try {
1056
- slots.inject('plugins.bundle.config', function () {
1057
- return slots.register({
1058
- name: 'plugins.bundle.config',
1059
- key: ENTRY,
1060
- order: 0,
1061
- }, makeModePicker(React.createElement, React))
1062
- })
1063
- } catch (error) {
1064
- void error
1065
- }
1066
- })
1067
- } catch (error) {
1068
- void error
1069
- }
1070
- }
1071
-
1072
- // ---- wiring -------------------------------------------------------
1073
-
1074
- effect(function () {
1075
- var existing = document.getElementById(STYLE_ID)
1076
- if (existing && existing.parentNode) existing.parentNode.removeChild(existing)
1077
- var style = document.createElement('style')
1078
- style.id = STYLE_ID
1079
- style.setAttribute('data-plugin', ENTRY)
1080
- style.textContent = CSS
1081
- ;(document.head || document.documentElement).appendChild(style)
1082
- return function () {
1083
- if (style.parentNode) style.parentNode.removeChild(style)
1084
- }
1085
- }, 'sidebar-suite: styles')
1086
-
1087
- effect(function () {
1088
- if (!localeService || typeof localeService.subscribe !== 'function') return
1089
- var off = localeService.subscribe(function () {
1090
- t.refresh()
1091
- relabel()
1092
- scheduleSync()
1093
- })
1094
- return function () {
1095
- try {
1096
- off()
1097
- } catch (error) {
1098
- void error
1099
- }
1100
- }
1101
- }, 'sidebar-suite: locale')
1102
-
1103
- effect(function () {
1104
- var stopped = false
1105
- var timer = null
1106
-
1107
- function attempt() {
1108
- if (stopped) return true
1109
- if (tryAdopt(safeGet(ctx, 'configForms'))) return true
1110
- return false
1111
- }
1112
-
1113
- try {
1114
- if (typeof ctx.inject === 'function') {
1115
- ctx.inject(['configForms'], function (scope) {
1116
- if (stopped) return
1117
- try {
1118
- tryAdopt(scope && scope.configForms)
1119
- } catch (error) {
1120
- void error
1121
- }
1122
- })
1123
- }
1124
- } catch (error) {
1125
- void error
1126
- }
1127
-
1128
- if (attempt()) return function () { stopped = true }
1129
-
1130
- // The client's config service may come up after this plugin: retry for
1131
- // ten seconds, then fall back to device-local storage.
1132
- var tries = 0
1133
- timer = setInterval(function () {
1134
- tries += 1
1135
- if (attempt() || tries > 40) {
1136
- clearInterval(timer)
1137
- timer = null
1138
- if (!config.controller) fallbackToLocal()
1139
- }
1140
- }, 250)
1141
- if (timer && typeof timer.unref === 'function') timer.unref()
1142
-
1143
- return function () {
1144
- stopped = true
1145
- if (timer) {
1146
- clearInterval(timer)
1147
- timer = null
1148
- }
1149
- }
1150
- }, 'sidebar-suite: configuration')
1151
-
1152
- effect(function () {
1153
- var onPointerDown = function (event) {
1154
- if (config.values.mode !== 'c' || !runtime.popOpen) return
1155
- var target = event.target
1156
- if (!target) return
1157
- if (runtime.bar && runtime.bar.contains(target)) return
1158
- if (runtime.nav && runtime.nav.contains(target)) return
1159
- runtime.popOpen = false
1160
- scheduleSync()
1161
- }
1162
- var onKeyDown = function (event) {
1163
- if (event.key !== 'Escape' || !runtime.popOpen) return
1164
- runtime.popOpen = false
1165
- // Esc must hand focus back to the trigger, or the next Tab starts
1166
- // from wherever the pointer last landed.
1167
- var trigger = barButton('pop')
1168
- if (trigger && typeof trigger.focus === 'function') trigger.focus()
1169
- scheduleSync()
1170
- }
1171
- var onResize = function () {
1172
- scheduleSync()
1173
- }
1174
- document.addEventListener('pointerdown', onPointerDown, true)
1175
- document.addEventListener('keydown', onKeyDown, true)
1176
- window.addEventListener('resize', onResize)
1177
- return function () {
1178
- document.removeEventListener('pointerdown', onPointerDown, true)
1179
- document.removeEventListener('keydown', onKeyDown, true)
1180
- window.removeEventListener('resize', onResize)
1181
- }
1182
- }, 'sidebar-suite: dismissal')
1183
-
1184
- effect(function () {
1185
- // Safety net: the sidebar root is created and destroyed by the host, so
1186
- // a root replacement is invisible to an observer that watches the old
1187
- // root's children. A slow heartbeat re-resolves it (one query and one
1188
- // rect read) and is skipped while the page is hidden.
1189
- var timer = setInterval(function () {
1190
- if (typeof document.hidden === 'boolean' && document.hidden) return
1191
- sync()
1192
- }, 1500)
1193
- if (timer && typeof timer.unref === 'function') timer.unref()
1194
- sync()
1195
- return function () {
1196
- clearInterval(timer)
1197
- }
1198
- }, 'sidebar-suite: sync')
1199
-
1200
- var teardown = null
1201
- effect(function () {
1202
- teardown = function () {
1203
- if (runtime.disposed) return
1204
- runtime.disposed = true
1205
- try {
1206
- if (runtime.bar && runtime.bar.parentNode) runtime.bar.parentNode.removeChild(runtime.bar)
1207
- } catch (error) {
1208
- void error
1209
- }
1210
- try {
1211
- if (window[OWNER_KEY] === owner) delete window[OWNER_KEY]
1212
- } catch (error) {
1213
- void error
1214
- }
1215
- try {
1216
- if (runtime.root) runtime.root.style.removeProperty('--dsbs-pop-top')
1217
- } catch (error) {
1218
- void error
1219
- }
1220
- try {
1221
- if (runtime.nav) runtime.nav.removeAttribute('data-dsbs-ext')
1222
- } catch (error) {
1223
- void error
1224
- }
1225
- try {
1226
- if (runtime.nav) {
1227
- runtime.nav.removeAttribute('aria-hidden')
1228
- runtime.nav.removeAttribute('inert')
1229
- }
1230
- if (runtime.regionArea) {
1231
- runtime.regionArea.removeAttribute('aria-hidden')
1232
- runtime.regionArea.removeAttribute('inert')
1233
- }
1234
- } catch (error) {
1235
- void error
1236
- }
1237
- var attributes = [
1238
- 'data-dsbs-mode',
1239
- 'data-dsbs-tab',
1240
- 'data-dsbs-fold-ext',
1241
- 'data-dsbs-fold-ws',
1242
- 'data-dsbs-pop',
1243
- 'data-dsbs-quick',
1244
- 'data-dsbs-rail',
1245
- 'data-dsbs-narrow',
1246
- ]
1247
- for (var i = 0; i < attributes.length; i++) {
1248
- html.removeAttribute(attributes[i])
1249
- }
1250
- try {
1251
- if (runtime.resizeObserver) runtime.resizeObserver.disconnect()
1252
- if (runtime.mutationObserver) runtime.mutationObserver.disconnect()
1253
- } catch (error) {
1254
- void error
1255
- }
1256
- runtime.bar = null
1257
- runtime.root = null
1258
- runtime.observedRoot = null
1259
- for (var j = 0; j < manualCleanups.length; j++) {
1260
- try {
1261
- manualCleanups[j]()
1262
- } catch (error) {
1263
- void error
1264
- }
1265
- }
1266
- manualCleanups.length = 0
1267
- }
1268
- return teardown
1269
- }, 'sidebar-suite: teardown')
1270
- owner.teardown = teardown
1271
- }
1272
-
1273
- return { inject: ['locale'], apply: apply }
1274
- },
1275
- })
1276
-
1
+ // dsh-sidebar-suite — Client half.
2
+ //
3
+ // The DSH left sidebar stacks two groups in one column: the panels group
4
+ // (`nav` with a `panelList` class) and the workspaces group
5
+ // (`[data-slot="sidebar.workspaces"]` inside its own region). They compete for
6
+ // the same vertical space, so this plugin offers three layouts for them:
7
+ //
8
+ // a both groups fold independently
9
+ // b the groups share one space and the control bar switches between them
10
+ // c the workspaces stay put and the panels open as an overlay
11
+ //
12
+ // All layout state lives in attributes on <html>, and the host's markup is
13
+ // never restructured or moved: this plugin only ADDS one control bar as a
14
+ // direct child of the sidebar root, one Temp button as a sibling of the host's
15
+ // New Session button, and (on demand) a temporary-chat dialog + scrim on body.
16
+ window.__ModuleLoader__.load({
17
+ id: 'dsh-sidebar-suite',
18
+ factory(require) {
19
+ 'use strict'
20
+
21
+ var ENTRY = 'dsh-sidebar-suite'
22
+ var NS = 'sidebar-suite'
23
+ var LS_KEY = 'dsh-sidebar-suite:state'
24
+ var STYLE_ID = 'dsh-sidebar-suite-style'
25
+ // Where feedback goes. A plugin bundle cannot open a mail client or post a
26
+ // form without a server, so the entry point assembles a ready-to-send
27
+ // report and offers the three channels that need no backend:
28
+ // 1. a pre-filled GitHub issue in THIS repository (the desktop shell opens
29
+ // http(s) links in the system browser — app.asar lib/main.js
30
+ // setWindowOpenHandler),
31
+ // 2. the marketplace's own comment thread. dsh-market keys every plugin's
32
+ // thread on `plugin:<owner/repo>` inside the public catalog repo
33
+ // (verified in dshmarket/client/client.js: GISCUS.repo, commentsTerm).
34
+ // It is the feedback entry the plugin market actually defines, so the
35
+ // detail page links to it instead of inventing a fourth channel,
36
+ // 3. copy to clipboard, for anyone who does not use GitHub.
37
+ var FEEDBACK_REPO = 'https://github.com/YONAN1269/dsh-sidebar-suite'
38
+ var FEEDBACK_DRAFT_KEY = 'dsh-sidebar-suite:feedback-draft'
39
+ var FEEDBACK_MARKET_TERM = 'plugin:YONAN1269/dsh-sidebar-suite'
40
+ var FEEDBACK_MARKET_URL = 'https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/discussions'
41
+ + '?discussions_q=' + encodeURIComponent('"' + FEEDBACK_MARKET_TERM + '"')
42
+ // The plugin detail page is a React surface. Load React defensively: if the
43
+ // module loader does not hand us `require`, the sidebar still works and we
44
+ // simply skip registering the graphical settings page.
45
+ var React = null
46
+ try {
47
+ React = typeof require === 'function' ? require('react') : null
48
+ } catch (error) {
49
+ React = null
50
+ }
51
+ // A client bundle can be applied more than once in the same page (a hot
52
+ // client reload after the plugin's files change, or a second Loader entry
53
+ // for the same package). Both runs would insert a bar and fight over the
54
+ // same <html> attributes, so the first live run owns the sidebar.
55
+ var OWNER_KEY = '__dshSidebarSuiteOwner'
56
+
57
+ var DEFAULTS = { mode: 'b', showQuickSwitch: true, tab: 'ext', foldExt: false, foldWs: false }
58
+ var MODES = ['a', 'b', 'c']
59
+
60
+ var DICTS = {
61
+ en: {
62
+ 'bar.label': 'Sidebar layout',
63
+ 'bar.local': 'Settings are stored on this device only (the official config layer is unavailable).',
64
+ 'mode.a': 'Fold',
65
+ 'mode.a.hint': 'Each group folds on its own.',
66
+ 'mode.b': 'Toggle',
67
+ 'mode.b.hint': 'The two groups share one space; choose which one is shown.',
68
+ 'mode.c': 'Overlay',
69
+ 'mode.c.hint': 'Workspaces stay visible; panels open as an overlay.',
70
+ 'quick.label': 'Cycle',
71
+ 'quick.hint': 'Cycle the sidebar layout.',
72
+ 'fold.ext.collapse': 'Hide panels',
73
+ 'fold.ext.expand': 'Show panels',
74
+ 'fold.ext.hint': 'Hide or show the panels group.',
75
+ 'fold.ws.collapse': 'Hide workspaces',
76
+ 'fold.ws.expand': 'Show workspaces',
77
+ 'fold.ws.hint': 'Hide or show the workspaces group.',
78
+ 'tab.to.ext': 'Show panels',
79
+ 'tab.to.ext.hint': 'Switch the shared space to the panels group.',
80
+ 'tab.to.ws': 'Show workspaces',
81
+ 'tab.to.ws.hint': 'Switch the shared space to the workspaces group.',
82
+ 'pop.open': 'Open panels',
83
+ 'pop.close': 'Close panels',
84
+ 'pop.hint': 'Open or close the panels overlay.',
85
+ 'settings.title': 'Default layout on startup',
86
+ 'settings.help': 'Pick the layout DSH starts with. This writes the plugin config; switching from the sidebar bar is session-only.',
87
+ 'settings.unavailable': 'The config service is unavailable right now, so the default cannot be changed here.',
88
+ 'settings.current': 'Current default',
89
+ 'temp.label': 'Temp',
90
+ 'temp.hint': 'Open a temporary session: a real DSH session in this sidebar that is deleted when you quit.',
91
+ 'temp.creating': 'Creating…',
92
+ 'temp.created': 'Temporary session ready. It lives in the sidebar and is deleted when you quit DSH.',
93
+ 'temp.noNative': 'This DSH build did not expose its session navigation service.',
94
+ 'temp.failed': 'Could not create the temporary session: ',
95
+ 'temp.feedbackHint': 'Report it on the plugin detail page.',
96
+ 'temp.restartHint': 'Fully quit DSH (tray icon → Quit) and reopen it — a hot reload does not reload the plugin host.',
97
+ 'temp.badge': 'TEMP',
98
+ 'policy.title': 'Temporary session cleanup',
99
+ 'policy.help': 'Choose when temporary sessions are removed. Saved to the plugin settings file and applied immediately — no restart.',
100
+ 'policy.onQuit': 'Delete when DSH quits',
101
+ 'policy.schedule': 'Clean up every day at',
102
+ 'policy.on': 'On',
103
+ 'policy.off': 'Off',
104
+ 'policy.note': 'A session in use right now is skipped and retried 5 minutes later.',
105
+ 'policy.saving': 'Saving…',
106
+ 'policy.saved': 'Saved',
107
+ 'policy.failed': 'Could not save: ',
108
+ 'policy.loading': 'Loading current settings…',
109
+ 'policy.unavailable': 'Cleanup settings are unavailable — the plugin host did not answer. Fully quit DSH and reopen it.',
110
+ },
111
+ zh: {
112
+ 'bar.label': '侧栏布局',
113
+ 'bar.local': '设置仅保存在本机(官方配置层不可用)。',
114
+ 'mode.a': '折叠',
115
+ 'mode.a.hint': '两组各自收起。',
116
+ 'mode.b': '标签页',
117
+ 'mode.b.hint': '两组共用一栏,用标签页切换显示。',
118
+ 'mode.c': '浮层',
119
+ 'mode.c.hint': '工作区常驻,扩展以浮层展开。',
120
+ 'quick.label': '换布局',
121
+ 'quick.hint': '循环切换侧栏布局。',
122
+ 'fold.ext.collapse': '收起扩展',
123
+ 'fold.ext.expand': '展开扩展',
124
+ 'fold.ext.hint': '收起或展开扩展区。',
125
+ 'fold.ws.collapse': '收起工作区',
126
+ 'fold.ws.expand': '展开工作区',
127
+ 'fold.ws.hint': '收起或展开工作区。',
128
+ 'tab.to.ext': '显示扩展',
129
+ 'tab.to.ext.hint': '把共用的一栏切换到扩展区。',
130
+ 'tab.to.ws': '显示工作区',
131
+ 'tab.to.ws.hint': '把共用的一栏切换到工作区。',
132
+ 'pop.open': '展开扩展',
133
+ 'pop.close': '收起扩展',
134
+ 'pop.hint': '打开或收起扩展浮层。',
135
+ 'settings.title': '启动时的默认布局',
136
+ 'settings.help': '选择 DSH 启动时使用的布局。这里会写入插件配置;在侧栏上临时切换不会改动它。',
137
+ 'settings.unavailable': '配置服务暂时不可用,无法在这里修改默认布局。',
138
+ 'settings.current': '当前默认',
139
+ 'temp.label': '临时',
140
+ 'temp.hint': '开一个临时会话:就是左侧栏里的真实会话,退出 DSH 时删除。',
141
+ 'temp.creating': '正在创建…',
142
+ 'temp.created': '临时会话已就绪:它在左侧栏里,退出 DSH 时自动删除。',
143
+ 'temp.noNative': '当前 DSH 版本没有暴露会话导航服务。',
144
+ 'temp.failed': '创建临时会话失败:',
145
+ 'temp.feedbackHint': '可在插件详情页反馈。',
146
+ 'temp.restartHint': '请从托盘图标彻底退出 DSH 再打开 —— 热重载不会重载插件后端。',
147
+ 'temp.badge': '临时',
148
+ 'policy.title': '临时会话自动删除',
149
+ 'policy.help': '决定临时会话什么时候被清掉。写入插件设置文件,改完立即生效,不用重启。',
150
+ 'policy.onQuit': '退出 DSH 时删除',
151
+ 'policy.schedule': '每天定时清理,时间',
152
+ 'policy.on': '开',
153
+ 'policy.off': '关',
154
+ 'policy.note': '正在使用的那个会话会跳过,5 分钟后再试一次。',
155
+ 'policy.saving': '保存中…',
156
+ 'policy.saved': '已保存',
157
+ 'policy.failed': '保存失败:',
158
+ 'policy.loading': '正在读取当前设置…',
159
+ 'policy.unavailable': '读取不到清理设置 —— 插件后端没有响应。请从托盘彻底退出 DSH 再打开。',
160
+ },
161
+ }
162
+
163
+ // The plugin detail page must read well in both languages regardless of
164
+ // the UI locale, so the settings copy is deliberately bilingual in both
165
+ // dictionaries. The long detail document (DETAIL.md) does the same.
166
+ var BILINGUAL_SETTINGS = {
167
+ // Pinned at the top of the plugin detail page: the one-line positioning
168
+ // statement. The Host renders locale/meta.description above the sections,
169
+ // but the settings card is lifted to order:-1, so without this line the
170
+ // first thing a reader sees would be a control with no context.
171
+ 'settings.intro': '一款专门针对改进 DSH 左侧栏体验的插件 —— 强实用性 + 美观性:沿用宿主原生观感,不另起一套皮肤。 / A plugin dedicated to the DSH sidebar experience — seriously practical and good-looking, built from native host design tokens.',
172
+ 'settings.title': '启动时的默认布局 · Default layout on startup',
173
+ 'settings.help': '选择 DSH 启动时使用的布局,这里会写入插件配置;在侧栏上临时切换只影响本次会话。 / Pick the layout DSH starts with — this writes the plugin config, while switching from the sidebar bar stays session-only.',
174
+ 'settings.unavailable': '配置服务暂时不可用,无法在这里修改默认布局。 / The config service is unavailable right now, so the default cannot be changed here.',
175
+ 'settings.current': '当前默认 / Current',
176
+ 'settings.support': '喜欢这个插件?请作者喝杯咖啡 · Enjoy it? Buy me a coffee',
177
+ 'settings.supportHint': '完全自愿,无任何回报承诺。用微信扫一扫即可;「设置 → 支持贡献者」里也登记了同一个码。 / Purely voluntary, no rewards promised. Scan with WeChat Pay; the same QR is registered under Settings → Support contributors.',
178
+ 'settings.qrAlt': '微信支付收款码 / WeChat Pay QR',
179
+ 'feedback.title': '反馈 · Feedback',
180
+ 'feedback.help': '遇到问题,或者想要某个功能?写在下面,然后选一个入口。 / Hit a bug or want a feature? Write it below, then pick a channel.',
181
+ 'feedback.placeholder': '例如:点了「临时」之后,侧栏没有出现新会话…… / e.g. clicking Temp adds no session to the sidebar…',
182
+ 'feedback.copy': '复制反馈内容 / Copy report',
183
+ 'feedback.issue': '在 GitHub 提 issue / Open an issue',
184
+ 'feedback.market': '插件市场评论区 / Marketplace comments',
185
+ 'feedback.working': '正在准备… / Preparing…',
186
+ 'feedback.copied': '已复制,粘贴发送即可 / Copied — paste and send',
187
+ 'feedback.issueOpened': '已在浏览器打开 / Opened in your browser',
188
+ 'feedback.failed': '复制被浏览器拒绝了,请手动选中再复制 / The browser blocked the copy — select and copy manually',
189
+ 'feedback.hint': '报告会自动带上版本、系统、布局和最近一次错误,不用再补一遍环境信息。 / The report already includes your version, OS, layout and last error.',
190
+ 'feedback.where': '作者在哪收到:issue 进本仓库的 Issues(仓库所有者默认会收到通知);市场评论进 awesome-dsh-plugin 仓库的 Discussions(作者需要 watch 那条讨论);「复制反馈内容」不会自动发送,粘到微信 / QQ / 邮件都行。 / Where the author sees it: issues land in this repo (the owner is normally notified); marketplace comments land in the awesome-dsh-plugin Discussions (the author must watch that thread); copied text is never sent automatically — paste it anywhere you can reach me.',
191
+ 'feedback.marketHint': '市场评论区如果显示「没有匹配的讨论」,说明还没人留言:回 DSH 的插件市场,找到 dsh-sidebar-suite 卡片点「评论」,第一条留言会自动建帖。 / If GitHub says there are no matching discussions yet, open the plugin market in DSH, find the dsh-sidebar-suite card and press "评论 / Comments" — the first comment creates that thread.',
192
+ }
193
+ for (var lang in DICTS) {
194
+ if (!Object.prototype.hasOwnProperty.call(DICTS, lang)) continue
195
+ for (var key in BILINGUAL_SETTINGS) {
196
+ if (Object.prototype.hasOwnProperty.call(BILINGUAL_SETTINGS, key)) DICTS[lang][key] = BILINGUAL_SETTINGS[key]
197
+ }
198
+ }
199
+
200
+ var CSS = [
201
+ '[data-dsbs-bar]{display:flex;flex-wrap:wrap;align-items:center;gap:5px;padding:2px 0 6px;flex:none;min-width:0}',
202
+ '[data-dsbs-bar] [hidden]{display:none!important}',
203
+ '[data-dsbs-sep]{width:1px;height:12px;flex:none;border-radius:1px;margin:0 1px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.3))}',
204
+ '[data-dsbs-bar] button{appearance:none;-webkit-appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,inherit);font:inherit;font-size:11px;line-height:15px;padding:2px 6px;border-radius:6px;cursor:pointer;user-select:none;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}',
205
+ '[data-dsbs-bar] button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));color:var(--dsw-alias-label-primary,inherit)}',
206
+ '[data-dsbs-bar] button[aria-pressed="true"]{background:var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.22));color:var(--dsw-alias-label-primary,inherit);font-weight:500}',
207
+ '[data-dsbs-bar] button:focus-visible{outline:2px solid var(--dsw-alias-label-primary,currentColor);outline-offset:1px}',
208
+ '[data-dsbs-bar] button[data-dsbs-btn="quick"]{margin-left:auto;color:var(--dsw-alias-label-tertiary,inherit);padding:2px 6px}',
209
+ 'html[data-dsbs-narrow="true"] [data-dsbs-btn="quick"]{display:none}',
210
+ 'html[data-dsbs-rail="true"] [data-dsbs-bar]{display:none}',
211
+ // a — each group folds on its own
212
+ 'html[data-dsbs-mode="a"][data-dsbs-fold-ext="true"] nav[data-dsbs-ext]{display:none}',
213
+ 'html[data-dsbs-mode="a"][data-dsbs-fold-ws="true"] div:has(> [data-slot="sidebar.workspaces"]){display:none}',
214
+ // b — the two groups are mutually exclusive
215
+ 'html[data-dsbs-mode="b"][data-dsbs-tab="ext"] div:has(> [data-slot="sidebar.workspaces"]){display:none}',
216
+ 'html[data-dsbs-mode="b"][data-dsbs-tab="ws"] nav[data-dsbs-ext]{display:none}',
217
+ // c — panels become an overlay above the workspaces
218
+ 'html[data-dsbs-mode="c"]:not([data-dsbs-pop="true"]) nav[data-dsbs-ext]{display:none}',
219
+ 'html[data-dsbs-mode="c"][data-dsbs-pop="true"] nav[data-dsbs-ext]{display:flex;flex-direction:column;gap:2px;position:absolute;z-index:60;left:4px;right:4px;top:var(--dsbs-pop-top,64px);max-height:60vh;overflow:auto;padding:6px;border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base,#fff));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3));box-shadow:0 12px 32px rgba(0,0,0,.24)}',
220
+ // the overlay needs a positioned containing block; the sidebar root is the
221
+ // only element with the panels nav as a direct child
222
+ 'div:has(> nav[data-dsbs-ext]){position:relative}',
223
+ // Plugin Manager detail page: `.detailSections` is already a column flex,
224
+ // so `order:-1` lifts this bundle's configuration section above the long
225
+ // detail document. Scoped by data-plugin-detail, no other bundle is hit.
226
+ '[data-plugin-detail="dsh-sidebar-suite"] [data-plugin-config]{order:-1;box-sizing:border-box;padding:14px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.28));border-radius:12px;background:var(--dsw-alias-bg-subtle,rgba(127,127,127,.05))}',
227
+ '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-support]{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:2px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))}',
228
+ '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-support] img{width:96px;height:96px;object-fit:contain;box-sizing:border-box;padding:4px;border-radius:8px;background:#fff;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))}',
229
+ '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-support] [data-dsbs-support-text]{flex:1 1 200px;min-width:0;display:flex;flex-direction:column;gap:4px}',
230
+ // Feedback box (0.2.3). The element itself is styled inline so it still
231
+ // looks right if this stylesheet is ever blocked; these three rules only
232
+ // cover what inline styles cannot express.
233
+ '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-feedback]{width:100%;box-sizing:border-box}',
234
+ '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-feedback] textarea::placeholder{color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.75))}',
235
+ '[data-plugin-detail="dsh-sidebar-suite"] [data-dsbs-feedback] textarea:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,currentColor);outline-offset:1px}',
236
+ // ---- always-expanded session list (0.2.4) -----------------------
237
+ // The host workspace browser renders one overflow button per group as
238
+ // <button data-row-key="overflow:<group>"> when its 5-row watermark
239
+ // hides the rest (verified in @deepseek-ai/dsh-client-ui-workspace:
240
+ // "sessions.expand" / "展开其余 {n} 个会话"). We drive that button until
241
+ // every group is expanded, then hide it: with all sessions visible the
242
+ // "收起" half of the control is pointless too. Only active while the
243
+ // config says on, so EXPAND_ALL_SESSIONS=off restores the host's UI.
244
+ 'html[data-dsbs-expand-all="true"] button[data-row-key^="overflow:"]{display:none!important}',
245
+ // ---- temporary chat (0.1.7) --------------------------------------
246
+ // The Temp button sits on the same row as the host's "New Session"
247
+ // button. Host markup is never moved: we add one sibling and pull it back
248
+ // onto the previous row with a negative margin, so the net flow height is
249
+ // zero and every following sidebar group keeps its original position.
250
+ // The whole two-button row only applies in HARNESS mode. dsh-deepseek-chat
251
+ // swaps the sidebar for its own website navigation in CHAT mode, where
252
+ // shrinking "new chat" would break the host's own layout, so every rule
253
+ // here is gated on html:not([data-dsbs-chat-mode]).
254
+ 'html:not([data-dsbs-chat-mode]) [data-dsbs-newchat] button[class*="newSession"]{height:34px!important;width:calc(100% - 92px)!important;min-width:0!important;flex:0 0 auto!important;margin:0 0 10px!important;font-size:12px!important;line-height:20px!important}',
255
+ 'html:not([data-dsbs-chat-mode]) [data-dsbs-newchat] button[class*="newSession"] > span[class*="newSessionShortcut"]{display:none!important}',
256
+ // Escape hatch for layouts that fight the two-button row (e.g. another
257
+ // plugin's header row above the sidebar). TEMP_BUTTON_STYLE=own puts the
258
+ // Temp button on its own full-width row and leaves New Session untouched.
259
+ 'html[data-dsbs-temp-style="own"] [data-dsbs-newchat] button[class*="newSession"]{height:38px!important;width:auto!important;font-size:14px!important;line-height:22px!important;margin:0 calc(var(--dsh-sidebar-paint-inset,3px) + 1px) 10px!important}',
260
+ 'html[data-dsbs-temp-style="own"] [data-dsbs-newchat] > [data-dsbs-temp-slot]{width:auto!important;align-self:stretch!important;margin:0 calc(var(--dsh-sidebar-paint-inset,3px) + 1px) 10px!important}',
261
+ 'html[data-dsbs-temp-style="own"] [data-dsbs-temp-slot] > [data-dsbs-btn="temp"]{width:auto!important;height:32px!important;text-align:center!important}',
262
+ // The Temp button is never a direct child of the sidebar root: it sits in
263
+ // its own <div>. dsh-deepseek-chat walks that root and requires exactly
264
+ // ONE direct <button> child (the host's New Session); a second one makes
265
+ // its sidebar adapter bail out and it paints the "当前侧栏不兼容" notice
266
+ // above the CHAT / HARNESS switcher. The wrapper keeps that count at one.
267
+ '[data-dsbs-newchat] > [data-dsbs-temp-slot]{display:flex;flex:none;align-self:flex-end;box-sizing:border-box;margin:-44px 4px 10px 0;width:80px;z-index:2}',
268
+ '[data-dsbs-temp-slot][hidden]{display:none!important}',
269
+ '[data-dsbs-temp-slot] > [data-dsbs-btn="temp"]{appearance:none;-webkit-appearance:none;box-sizing:border-box;width:100%;height:34px;flex:none;border:.5px solid var(--dsw-alias-border-l3,rgba(127,127,127,.35));border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-button-elevated-fill,transparent);color:var(--dsw-alias-label-primary,inherit);font:inherit;font-size:12px;font-weight:500;line-height:20px;cursor:pointer;user-select:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:6px 8px}',
270
+ '[data-dsbs-temp-slot] > [data-dsbs-btn="temp"]:hover{background:var(--dsw-alias-button-floating-hover,var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12)))}',
271
+ '[data-dsbs-temp-slot] > [data-dsbs-btn="temp"]:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,currentColor);outline-offset:1px}',
272
+ '[data-dsbs-temp-slot] > [data-dsbs-btn="temp"][hidden]{display:none!important}',
273
+ // dsh-deepseek-chat is in CHAT (official website) mode: its sidebar and
274
+ // its own "new chat" button own that space, so the Temp button steps
275
+ // aside instead of fighting it. Switchable via temp-session-config.txt.
276
+ 'html[data-dsbs-chat-mode] [data-dsbs-temp-slot]{display:none!important}',
277
+ // In the collapsed rail there is no horizontal room for two buttons.
278
+ 'html[data-dsbs-temp-solo="true"] [data-dsbs-temp-slot]{display:none!important}',
279
+ 'html[data-dsbs-temp-solo="true"]:not([data-dsbs-chat-mode]) [data-dsbs-newchat] button[class*="newSession"]{width:auto!important;height:38px!important;font-size:14px!important;line-height:22px!important;margin:0 calc(var(--dsh-sidebar-paint-inset,3px) + 1px) 10px!important}',
280
+ // A temporary session is a NORMAL DSH session, so its sidebar row is the
281
+ // host's own row. Nothing is added to the DOM: we tag the row and let CSS
282
+ // draw the badge, which survives every React re-render of the list.
283
+ '[data-row-key^="session:"][data-dsbs-temp-row]::after{content:attr(data-dsbs-temp-badge);margin-left:6px;flex:none;box-sizing:border-box;padding:0 5px;border:1px solid currentColor;border-radius:5px;font-size:10px;line-height:14px;font-weight:500;white-space:nowrap;opacity:.6}',
284
+ // The collapsed rail has no room for a word; the row keeps its own look.
285
+ 'html[data-dsbs-rail="true"] [data-dsbs-temp-row]::after{display:none}',
286
+ '[data-dsbs-temp-toast]{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:9500;max-width:min(560px,calc(100vw - 40px));box-sizing:border-box;padding:9px 14px;border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base,#1f1f1f));color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));box-shadow:0 12px 34px rgba(0,0,0,.34);font:inherit;font-size:12px;line-height:1.6;word-break:break-word}',
287
+ '[data-dsbs-temp-toast][data-dsbs-temp-toast-error="true"]{color:var(--dsw-alias-state-business-danger,#d9534f);border-color:rgba(217,83,79,.45)}',
288
+ ].join('\n')
289
+
290
+ function assign(target) {
291
+ for (var i = 1; i < arguments.length; i++) {
292
+ var source = arguments[i]
293
+ if (!source || typeof source !== 'object') continue
294
+ for (var key in source) {
295
+ if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key]
296
+ }
297
+ }
298
+ return target
299
+ }
300
+
301
+ function safeGet(ctx, name) {
302
+ if (!ctx) return undefined
303
+ try {
304
+ if (typeof ctx.get === 'function') return ctx.get(name)
305
+ } catch (error) {
306
+ void error
307
+ }
308
+ return undefined
309
+ }
310
+
311
+ function isExtNav(node) {
312
+ return !!node && node.tagName === 'NAV' && /panelList/.test(node.className || '')
313
+ }
314
+
315
+ // Resolved on every sync: the sidebar is rebuilt by React on session
316
+ // changes, so nothing here may be cached.
317
+ function resolveSidebar() {
318
+ if (typeof document === 'undefined') return null
319
+ var workspaces = document.querySelector('[data-slot="sidebar.workspaces"]')
320
+ if (!workspaces) return null
321
+ var regionArea = workspaces.parentElement
322
+ if (!regionArea) return null
323
+ var node = regionArea
324
+ var root = null
325
+ var nav = null
326
+ while (node && node !== document.body && node !== document.documentElement) {
327
+ var children = node.children
328
+ for (var i = 0; i < children.length; i++) {
329
+ var child = children[i]
330
+ if (child === regionArea) continue
331
+ if (isExtNav(child)) {
332
+ nav = child
333
+ root = node
334
+ break
335
+ }
336
+ }
337
+ if (root) break
338
+ node = node.parentElement
339
+ }
340
+ if (!root) {
341
+ root = regionArea.parentElement
342
+ if (!root) return null
343
+ for (var j = 0; j < root.children.length; j++) {
344
+ if (isExtNav(root.children[j])) {
345
+ nav = root.children[j]
346
+ break
347
+ }
348
+ }
349
+ }
350
+ return { root: root, nav: nav, regionArea: regionArea, workspaces: workspaces }
351
+ }
352
+
353
+ function readLocal() {
354
+ try {
355
+ var raw = localStorage.getItem(LS_KEY)
356
+ if (!raw) return {}
357
+ var parsed = JSON.parse(raw)
358
+ return parsed && typeof parsed === 'object' ? parsed : {}
359
+ } catch (error) {
360
+ return {}
361
+ }
362
+ }
363
+
364
+ function writeLocal(value) {
365
+ try {
366
+ localStorage.setItem(LS_KEY, JSON.stringify(value))
367
+ } catch (error) {
368
+ void error
369
+ }
370
+ }
371
+
372
+ function adoptValues(raw) {
373
+ var out = assign({}, DEFAULTS)
374
+ if (!raw || typeof raw !== 'object') return out
375
+ if (MODES.indexOf(raw.mode) >= 0) out.mode = raw.mode
376
+ if (typeof raw.showQuickSwitch === 'boolean') out.showQuickSwitch = raw.showQuickSwitch
377
+ if (raw.tab === 'ext' || raw.tab === 'ws') out.tab = raw.tab
378
+ if (typeof raw.foldExt === 'boolean') out.foldExt = raw.foldExt
379
+ if (typeof raw.foldWs === 'boolean') out.foldWs = raw.foldWs
380
+ return out
381
+ }
382
+
383
+ // The locale service merges dictionaries per namespace and keeps entries
384
+ // registered by earlier client generations, so a bound translator can mix
385
+ // stale and current strings (that is how a page ends up with Chinese modes
386
+ // and English captions). The bar's own labels only need zh/en, so they are
387
+ // resolved from the active locale id and this plugin's dictionaries.
388
+ function detectLang(localeService) {
389
+ var candidates = []
390
+ try {
391
+ if (localeService && typeof localeService.getSnapshot === 'function') {
392
+ var snapshot = localeService.getSnapshot()
393
+ if (snapshot && snapshot.active) candidates.push(snapshot.active)
394
+ }
395
+ if (localeService && typeof localeService.getLocale === 'function') {
396
+ var active = localeService.getLocale()
397
+ if (typeof active === 'string') candidates.push(active)
398
+ else if (active && active.active) candidates.push(active.active)
399
+ }
400
+ } catch (error) {
401
+ void error
402
+ }
403
+ try {
404
+ candidates.push(document.documentElement.lang)
405
+ } catch (error) {
406
+ void error
407
+ }
408
+ try {
409
+ candidates.push(navigator.language)
410
+ } catch (error) {
411
+ void error
412
+ }
413
+ for (var i = 0; i < candidates.length; i++) {
414
+ if (/^zh/i.test(String(candidates[i] || ''))) return 'zh'
415
+ }
416
+ return 'en'
417
+ }
418
+
419
+ function createTranslator(localeService) {
420
+ var lang = 'en'
421
+ try {
422
+ if (localeService && typeof localeService.register === 'function') {
423
+ localeService.register(NS, DICTS)
424
+ }
425
+ } catch (error) {
426
+ void error
427
+ }
428
+ function refresh() {
429
+ lang = detectLang(localeService)
430
+ }
431
+ refresh()
432
+ var t = function (key) {
433
+ return DICTS[lang][key] || DICTS.en[key] || key
434
+ }
435
+ t.refresh = refresh
436
+ return t
437
+ }
438
+
439
+ function apply(ctx) {
440
+ if (typeof document === 'undefined' || typeof window === 'undefined') return
441
+
442
+ // Claim the sidebar before touching the DOM. A second apply takes over
443
+ // instead of stacking: the run that already owns the sidebar is torn down
444
+ // first, so a hot reload swaps the bar in place and never leaves stale
445
+ // buttons or html attributes behind.
446
+ var owner = { alive: true, teardown: null }
447
+ try {
448
+ var current = window[OWNER_KEY]
449
+ if (current && current.alive) {
450
+ try {
451
+ if (typeof current.teardown === 'function') current.teardown()
452
+ } catch (error) {
453
+ void error
454
+ }
455
+ current.alive = false
456
+ }
457
+ window[OWNER_KEY] = owner
458
+ } catch (error) {
459
+ void error
460
+ }
461
+
462
+ var html = document.documentElement
463
+ var localeService = null
464
+ try {
465
+ localeService = ctx.locale
466
+ } catch (error) {
467
+ localeService = null
468
+ }
469
+ if (!localeService) localeService = safeGet(ctx, 'locale')
470
+ var t = createTranslator(localeService)
471
+
472
+ var config = { controller: null, values: assign({}, DEFAULTS), local: false }
473
+ var runtime = {
474
+ disposed: false,
475
+ // `mode` in the profile is the startup default. Picking a layout from
476
+ // the bar is session-only and must never rewrite the config form.
477
+ sessionMode: null,
478
+ configMode: null,
479
+ bar: null,
480
+ root: null,
481
+ nav: null,
482
+ regionArea: null,
483
+ popOpen: false,
484
+ rail: false,
485
+ railSaved: null,
486
+ hidden: { ext: false, ws: false },
487
+ observedRoot: null,
488
+ resizeObserver: null,
489
+ mutationObserver: null,
490
+ raf: 0,
491
+ }
492
+
493
+ var manualCleanups = []
494
+ var tempButton = null
495
+ var tempSlot = null // wrapper that keeps the root's top-level <button> count at one
496
+ var tempNewChatHost = null
497
+ var tempChatPolicy = 'hide' // TEMP_IN_CHAT_MODE in temp-session-config.txt
498
+ var tempChatGuard = null
499
+ var tempIds = [] // session ids the Host half will delete on quit
500
+ var tempBusy = false
501
+ var tempToastTimer = 0
502
+
503
+ // 0.2.4 — always-expanded session list. `expandAllKnown` gates the
504
+ // clicker until the Host half has answered (or clearly cannot answer),
505
+ // so EXPAND_ALL_SESSIONS=off is never briefly overridden by the default.
506
+ var expandAll = true
507
+ var expandAllKnown = false
508
+ var expandCooldown = Object.create(null)
509
+ var expandRounds = 0
510
+ var expandTimer = 0
511
+ var expandGateTimer = 0
512
+
513
+ // Register a resource and its disposal through the host context when it
514
+ // offers one; otherwise keep the disposer for the end of this run.
515
+ function effect(fn, label) {
516
+ var disposer = null
517
+ try {
518
+ if (typeof ctx.effect === 'function') {
519
+ ctx.effect(fn, label)
520
+ return
521
+ }
522
+ } catch (error) {
523
+ void error
524
+ }
525
+ try {
526
+ disposer = fn()
527
+ } catch (error) {
528
+ void error
529
+ }
530
+ if (typeof disposer === 'function') manualCleanups.push(disposer)
531
+ }
532
+
533
+ function setFlag(element, name, on) {
534
+ if (!element) return
535
+ if (on) element.setAttribute(name, 'true')
536
+ else element.removeAttribute(name)
537
+ }
538
+
539
+ // ---- markup -------------------------------------------------------
540
+
541
+ function makeButton(key, labelKey, hintKey, onClick) {
542
+ var button = document.createElement('button')
543
+ button.type = 'button'
544
+ button.setAttribute('data-dsbs-btn', key)
545
+ if (labelKey) button.setAttribute('data-dsbs-label', labelKey)
546
+ if (hintKey) button.setAttribute('data-dsbs-hint', hintKey)
547
+ if (labelKey) button.textContent = t(labelKey)
548
+ if (hintKey) button.title = t(hintKey)
549
+ button.addEventListener('click', function (event) {
550
+ event.preventDefault()
551
+ event.stopPropagation()
552
+ onClick(event)
553
+ })
554
+ return button
555
+ }
556
+
557
+ function makeSeparator() {
558
+ var separator = document.createElement('span')
559
+ separator.setAttribute('data-dsbs-sep', '')
560
+ separator.setAttribute('aria-hidden', 'true')
561
+ return separator
562
+ }
563
+
564
+ // ---- temporary sessions (0.2.0) -----------------------------------
565
+ // A temporary session IS a real DSH session. It is created through the
566
+ // host's own navigation service — the exact call the native "New Session"
567
+ // button makes — so it appears in the left sidebar, uses the native
568
+ // conversation window, keeps its own model selection and tool loop, and
569
+ // can be switched away from like any other task.
570
+ //
571
+ // Only deletion is ours. Verified in app.asar: DSH has no session-delete
572
+ // API (its own UI action is archive/hide), so the Host half records the
573
+ // ids and removes the session directories from disk at quit and again on
574
+ // the next launch. Nothing is kept "in memory only" any more.
575
+
576
+ function tempUrl(path) {
577
+ try { return new URL(path, document.baseURI).href } catch (error) { void error; return path }
578
+ }
579
+
580
+ // Surface the real failure: a 404 here means the Host half never mounted
581
+ // its /api routes (usually DSH was hot-reloaded, not restarted).
582
+ function tempCall(path, options) {
583
+ var init = { credentials: 'same-origin', cache: 'no-store' }
584
+ if (options && options.method) init.method = options.method
585
+ if (options && options.body !== undefined) {
586
+ init.body = options.body
587
+ init.headers = { 'content-type': 'application/json' }
588
+ }
589
+ return fetch(tempUrl(path), init)
590
+ .then(function (response) {
591
+ return response.text().then(function (body) {
592
+ var detail = (body || '').replace(/\s+/g, ' ').slice(0, 180)
593
+ if (!response.ok) throw new Error('HTTP ' + response.status + (detail ? ' · ' + detail : ''))
594
+ try { return JSON.parse(body) } catch (error) {
595
+ throw new Error('non-JSON response · ' + detail)
596
+ }
597
+ })
598
+ })
599
+ .catch(function (error) {
600
+ if (error instanceof TypeError) throw new Error('network error · ' + String(error.message))
601
+ throw error
602
+ })
603
+ }
604
+
605
+ function toast(text, isError) {
606
+ var node = null
607
+ try { node = document.querySelector('[data-dsbs-temp-toast]') } catch (error) { void error }
608
+ if (!node) {
609
+ node = document.createElement('div')
610
+ node.setAttribute('data-dsbs-temp-toast', '')
611
+ node.setAttribute('role', 'status')
612
+ document.body.appendChild(node)
613
+ }
614
+ node.setAttribute('data-dsbs-temp-toast-error', isError ? 'true' : 'false')
615
+ node.textContent = text
616
+ clearTimeout(tempToastTimer)
617
+ tempToastTimer = setTimeout(function () {
618
+ try { if (node.parentNode) node.parentNode.removeChild(node) } catch (error) { void error }
619
+ }, isError ? 10000 : 3500)
620
+ }
621
+
622
+ // `uiWorkspace` is the client service the sidebar itself uses for "New
623
+ // Session" (dsh-client-ui-workspace). Read through ctx.get() so a host
624
+ // build without it leaves the three sidebar layouts untouched.
625
+ function uiWorkspace() {
626
+ try {
627
+ if (typeof ctx.get !== 'function') return null
628
+ return ctx.get('uiWorkspace') || null
629
+ } catch (error) {
630
+ void error
631
+ return null
632
+ }
633
+ }
634
+
635
+ function listOf(store) {
636
+ try {
637
+ var snap = store && typeof store.getSnapshot === 'function' ? store.getSnapshot() : null
638
+ return snap || null
639
+ } catch (error) {
640
+ void error
641
+ return null
642
+ }
643
+ }
644
+
645
+ /** Same workspace the native button would pick: current, else the first. */
646
+ function newSessionWorkspace(ui) {
647
+ try {
648
+ var snap = listOf(ui.workspaces && ui.workspaces.list)
649
+ var items = (snap && snap.items) || []
650
+ var current = null
651
+ try { current = ui.mainReference && ui.mainReference.sessionId } catch (error) { void error }
652
+ if (current) {
653
+ for (var i = 0; i < items.length; i++) {
654
+ if ((items[i].sessionIds || []).indexOf(current) >= 0) return items[i].workspaceId
655
+ }
656
+ }
657
+ if (items.length > 0) return items[0].workspaceId
658
+ } catch (error) {
659
+ void error
660
+ }
661
+ return undefined
662
+ }
663
+
664
+ function knownSessionIds(ui) {
665
+ try {
666
+ var snap = listOf(ui.sessions && ui.sessions.list)
667
+ return (snap && snap.ids ? snap.ids.slice() : [])
668
+ } catch (error) {
669
+ void error
670
+ return []
671
+ }
672
+ }
673
+
674
+ /** Fallback for builds without createNamed: watch the list for a new id. */
675
+ function waitForNewSession(ui, before) {
676
+ return new Promise(function (resolve) {
677
+ var tries = 0
678
+ var tick = function () {
679
+ var ids = knownSessionIds(ui)
680
+ for (var i = 0; i < ids.length; i++) {
681
+ if (before.indexOf(ids[i]) < 0) { resolve(ids[i]); return }
682
+ }
683
+ tries += 1
684
+ if (tries > 50) { resolve(''); return }
685
+ setTimeout(tick, 100)
686
+ }
687
+ tick()
688
+ })
689
+ }
690
+
691
+ function createTempSession() {
692
+ if (tempBusy) return
693
+ var ui = uiWorkspace()
694
+ if (!ui) {
695
+ toast(t('temp.noNative') + ' ' + t('temp.restartHint'), true)
696
+ return
697
+ }
698
+ tempBusy = true
699
+ if (tempButton) {
700
+ tempButton.disabled = true
701
+ tempButton.textContent = t('temp.creating')
702
+ }
703
+ var workspaceId = newSessionWorkspace(ui)
704
+ var before = knownSessionIds(ui)
705
+ Promise.resolve()
706
+ .then(function () {
707
+ // No workspace yet (fresh install, nothing opened): let the host's
708
+ // own button logic decide, then read the id it settles on.
709
+ if (typeof ui.createNamed === 'function' && workspaceId !== undefined) {
710
+ return ui.createNamed({ workspaceId: workspaceId })
711
+ }
712
+ if (typeof ui.startSession === 'function') {
713
+ ui.startSession(workspaceId)
714
+ return waitForNewSession(ui, before)
715
+ }
716
+ throw new Error('uiWorkspace.createNamed / startSession missing')
717
+ })
718
+ .then(function (id) {
719
+ if (!id) throw new Error('the host returned no session id')
720
+ try {
721
+ if (typeof ui.openSession === 'function') ui.openSession(id)
722
+ } catch (error) {
723
+ void error
724
+ }
725
+ if (tempIds.indexOf(id) < 0) tempIds.push(id)
726
+ applyTempRows()
727
+ return tempCall('api/dsh-temp-session.track?sessionId=' + encodeURIComponent(id), { method: 'POST' })
728
+ })
729
+ .then(function (data) {
730
+ if (!data || data.ok !== true) throw new Error((data && data.error) || 'tracking failed')
731
+ toast(t('temp.created'), false)
732
+ })
733
+ .catch(function (error) {
734
+ toast(t('temp.failed') + String((error && error.message) || error) + ' · ' + t('temp.restartHint') + ' ' + t('temp.feedbackHint'), true)
735
+ })
736
+ .then(function () {
737
+ tempBusy = false
738
+ if (tempButton) {
739
+ tempButton.disabled = false
740
+ tempButton.textContent = t('temp.label')
741
+ }
742
+ })
743
+ }
744
+
745
+ /** Tag every sidebar row whose session id is in the temporary set. */
746
+ function applyTempRows() {
747
+ var rows = null
748
+ try { rows = document.querySelectorAll('[data-row-key^="session:"]') } catch (error) {
749
+ void error
750
+ return
751
+ }
752
+ if (!rows) return
753
+ var badge = t('temp.badge') || 'TEMP'
754
+ for (var i = 0; i < rows.length; i++) {
755
+ var row = rows[i]
756
+ var key = row.getAttribute('data-row-key') || ''
757
+ var id = key.slice('session:'.length)
758
+ if (tempIds.indexOf(id) >= 0) {
759
+ if (row.getAttribute('data-dsbs-temp-row') !== '1') row.setAttribute('data-dsbs-temp-row', '1')
760
+ if (row.getAttribute('data-dsbs-temp-badge') !== badge) row.setAttribute('data-dsbs-temp-badge', badge)
761
+ } else if (row.hasAttribute('data-dsbs-temp-row')) {
762
+ row.removeAttribute('data-dsbs-temp-row')
763
+ row.removeAttribute('data-dsbs-temp-badge')
764
+ }
765
+ }
766
+ }
767
+
768
+ function refreshTempIds() {
769
+ return tempCall('api/dsh-temp-session.list')
770
+ .then(function (data) {
771
+ if (data && Array.isArray(data.ids)) {
772
+ tempIds = data.ids.slice()
773
+ applyTempRows()
774
+ }
775
+ })
776
+ .catch(function (error) { void error })
777
+ }
778
+
779
+ // ---- Temp button beside the host's "New Session" button (0.1.6) ----
780
+ // The button is a plain sibling of the host button, never a replacement
781
+ // or a moved node: React keeps owning its own DOM and we only add ours.
782
+ function makeTempButton() {
783
+ var button = document.createElement('button')
784
+ button.type = 'button'
785
+ button.setAttribute('data-dsbs-btn', 'temp')
786
+ button.textContent = t('temp.label')
787
+ button.title = t('temp.hint')
788
+ button.addEventListener('click', function (event) {
789
+ event.preventDefault()
790
+ event.stopPropagation()
791
+ createTempSession()
792
+ })
793
+ return button
794
+ }
795
+
796
+ function makeTempSlot() {
797
+ var slot = document.createElement('div')
798
+ slot.setAttribute('data-dsbs-temp-slot', '')
799
+ return slot
800
+ }
801
+
802
+ function hideTempButton() {
803
+ if (tempButton) tempButton.setAttribute('hidden', '')
804
+ if (tempSlot) tempSlot.setAttribute('hidden', '')
805
+ }
806
+
807
+ function placeTempButton() {
808
+ if (!tempButton) tempButton = makeTempButton()
809
+ if (!tempSlot) tempSlot = makeTempSlot()
810
+ ensureChatGuard()
811
+ var native = null
812
+ try { native = document.querySelector('button[class*="newSession"]') } catch (error) { void error }
813
+ if (!native || !native.parentNode) return false
814
+ var host = native.parentNode
815
+ // The host's "New Session" button is a direct child of the sidebar root,
816
+ // and dsh-deepseek-chat validates that root: it needs exactly one direct
817
+ // <button> child. Adding a second one there makes its sidebar adapter
818
+ // give up and show "当前侧栏不兼容:顶部新会话仍使用 HARNESS". Our button
819
+ // therefore lives inside [data-dsbs-temp-slot], which the adapter ignores.
820
+ if (tempButton.parentNode !== tempSlot) tempSlot.appendChild(tempButton)
821
+ if (tempSlot.parentNode !== host) host.insertBefore(tempSlot, native.nextSibling)
822
+ if (host !== tempNewChatHost) {
823
+ if (tempNewChatHost) tempNewChatHost.removeAttribute('data-dsbs-newchat')
824
+ tempNewChatHost = host
825
+ host.setAttribute('data-dsbs-newchat', '')
826
+ }
827
+ var width = 0
828
+ try { width = host.getBoundingClientRect().width } catch (error) { void error }
829
+ var fixed = false
830
+ try { fixed = !!(window.getComputedStyle && window.getComputedStyle(native).position === 'fixed') } catch (error) { void error }
831
+ // The collapsed rail has no horizontal room for a second button.
832
+ setFlag(html, 'data-dsbs-temp-solo', fixed || runtime.rail === true || (width > 0 && width < 170))
833
+ var label = t('temp.label')
834
+ if (tempButton.textContent !== label) tempButton.textContent = label
835
+ var hint = t('temp.hint')
836
+ if (tempButton.title !== hint) tempButton.title = hint
837
+ return true
838
+ }
839
+
840
+ var tempStatusLoaded = false
841
+ var tempStatusTries = 0
842
+ var tempStatusPending = false
843
+
844
+ /** Publish the CSS flag only when the setting is known and enabled. */
845
+ function applyExpandFlag() {
846
+ setFlag(html, 'data-dsbs-expand-all', expandAllKnown && expandAll)
847
+ }
848
+
849
+ /**
850
+ * The host keeps five session rows per workspace and renders one overflow
851
+ * button for the rest: <button data-row-key="overflow:<group>">.
852
+ * Pressing it raises that group's watermark by five (or to Infinity on
853
+ * the last batch) and flips aria-expanded to true once nothing is hidden.
854
+ * React owns that state, so the honest way to expand is to press the host
855
+ * button — never to rewrite or fake host DOM.
856
+ *
857
+ * A short cooldown per group prevents the same button from being pressed
858
+ * twice before React re-renders (the second press would collapse it).
859
+ */
860
+ function expandAllSessions() {
861
+ if (runtime.disposed || !expandAllKnown || !expandAll) return
862
+ var buttons = null
863
+ try {
864
+ buttons = document.querySelectorAll('button[data-row-key^="overflow:"]')
865
+ } catch (error) {
866
+ void error
867
+ return
868
+ }
869
+ if (!buttons || !buttons.length) {
870
+ expandRounds = 0
871
+ return
872
+ }
873
+ var now = Date.now()
874
+ var clicked = 0
875
+ for (var i = 0; i < buttons.length; i++) {
876
+ var button = buttons[i]
877
+ if (!button || button.getAttribute('aria-expanded') === 'true') continue
878
+ var key = button.getAttribute('data-row-key') || ('row-' + i)
879
+ if (now - (expandCooldown[key] || 0) < 120) continue
880
+ expandCooldown[key] = now
881
+ try {
882
+ button.click()
883
+ clicked += 1
884
+ } catch (error) {
885
+ void error
886
+ }
887
+ }
888
+ if (clicked === 0 || expandRounds >= 40) {
889
+ expandRounds = 0
890
+ return
891
+ }
892
+ expandRounds += 1
893
+ if (expandTimer) return
894
+ expandTimer = setTimeout(function () {
895
+ expandTimer = 0
896
+ expandAllSessions()
897
+ }, 150)
898
+ }
899
+
900
+ function loadTempStatus() {
901
+ if (tempStatusLoaded || tempStatusPending) return
902
+ if (tempStatusTries >= 3) {
903
+ // The Host half never answered. Fall back to the file's default
904
+ // rather than leaving the session list half-folded forever.
905
+ if (!expandAllKnown) {
906
+ expandAllKnown = true
907
+ applyExpandFlag()
908
+ scheduleSync()
909
+ }
910
+ return
911
+ }
912
+ tempStatusTries += 1
913
+ tempStatusPending = true
914
+ // A hanging request must not leave the session list folded forever.
915
+ // Two seconds is well below the first heartbeat after a cold start.
916
+ if (!expandGateTimer) {
917
+ expandGateTimer = setTimeout(function () {
918
+ expandGateTimer = 0
919
+ if (expandAllKnown) return
920
+ expandAllKnown = true
921
+ expandAll = true
922
+ applyExpandFlag()
923
+ scheduleSync()
924
+ }, 2000)
925
+ }
926
+ tempCall('api/dsh-temp-session.status').then(function (data) {
927
+ tempStatusPending = false
928
+ tempStatusLoaded = true
929
+ if (expandGateTimer) { clearTimeout(expandGateTimer); expandGateTimer = 0 }
930
+ if (data && data.showButton === false) hideTempButton()
931
+ if (data && data.chatModePolicy) tempChatPolicy = String(data.chatModePolicy)
932
+ if (data && data.buttonStyle) setFlag(html, 'data-dsbs-temp-style', String(data.buttonStyle) === 'own')
933
+ // Older Host halves (<= 0.2.3) do not send this key; default to on.
934
+ expandAll = !data || data.expandAllSessions !== false
935
+ expandAllKnown = true
936
+ applyExpandFlag()
937
+ syncChatMode()
938
+ refreshTempIds()
939
+ scheduleSync()
940
+ }).catch(function (error) {
941
+ void error
942
+ tempStatusPending = false
943
+ if (expandGateTimer) { clearTimeout(expandGateTimer); expandGateTimer = 0 }
944
+ if (!expandAllKnown) {
945
+ expandAllKnown = true
946
+ expandAll = true
947
+ applyExpandFlag()
948
+ scheduleSync()
949
+ }
950
+ })
951
+ }
952
+
953
+ // ---- coexistence with dsh-deepseek-chat (CHAT / HARNESS switcher) ----
954
+ // That plugin swaps the sidebar and the whole main area for DeepSeek's
955
+ // official website when the user is in CHAT mode. Its "new chat" button
956
+ // shares the same row as the host's "New Session" button, so a second
957
+ // button there (ours) both breaks its layout and offers a harness model
958
+ // call in a screen that is not a harness session. We detect its DOM and
959
+ // step aside; the flag lives on <html> so the CSS above can gate on it.
960
+ function chatModeActive() {
961
+ try {
962
+ if (document.querySelector('[data-dsh-chat-content],[data-dsh-chat-layout]')) return true
963
+ } catch (error) {
964
+ void error
965
+ }
966
+ return false
967
+ }
968
+
969
+ function syncChatMode() {
970
+ if (runtime.disposed) return
971
+ var active = tempChatPolicy !== 'show' && chatModeActive()
972
+ setFlag(html, 'data-dsbs-chat-mode', active)
973
+ }
974
+
975
+ function ensureChatGuard() {
976
+ if (tempChatGuard !== null || runtime.disposed) return
977
+ tempChatGuard = setInterval(syncChatMode, 1500)
978
+ }
979
+
980
+ // One row of plain text buttons, the shape the bar had in its first
981
+ // version: the filled button is the chosen layout, and everything after
982
+ // the divider names the action its click performs in that layout.
983
+ function buildBar() {
984
+ var bar = document.createElement('div')
985
+ bar.setAttribute('data-dsbs-bar', '')
986
+ bar.setAttribute('role', 'toolbar')
987
+
988
+ var i
989
+ for (i = 0; i < MODES.length; i++) {
990
+ var mode = MODES[i]
991
+ bar.appendChild(makeButton('mode-' + mode, 'mode.' + mode, 'mode.' + mode + '.hint', (function (value) {
992
+ return function () { setMode(value) }
993
+ })(mode)))
994
+ }
995
+
996
+ var separator = makeSeparator()
997
+ bar.appendChild(separator)
998
+
999
+ var foldExt = makeButton('fold-ext', 'fold.ext.collapse', 'fold.ext.hint', function () { setField('foldExt', !config.values.foldExt) })
1000
+ var foldWs = makeButton('fold-ws', 'fold.ws.collapse', 'fold.ws.hint', function () { setField('foldWs', !config.values.foldWs) })
1001
+ var tabSwap = makeButton('tab-swap', 'tab.to.ws', 'tab.to.ws.hint', function () {
1002
+ setField('tab', config.values.tab === 'ext' ? 'ws' : 'ext')
1003
+ })
1004
+ var pop = makeButton('pop', 'pop.open', 'pop.hint', function () {
1005
+ runtime.popOpen = !runtime.popOpen
1006
+ scheduleSync()
1007
+ })
1008
+ pop.setAttribute('aria-expanded', 'false')
1009
+ var quick = makeButton('quick', 'quick.label', 'quick.hint', function () {
1010
+ var index = MODES.indexOf(config.values.mode)
1011
+ setMode(MODES[(index + 1) % MODES.length])
1012
+ })
1013
+ bar.appendChild(foldExt)
1014
+ bar.appendChild(foldWs)
1015
+ bar.appendChild(tabSwap)
1016
+ bar.appendChild(pop)
1017
+ bar.appendChild(quick)
1018
+
1019
+ runtime.parts = { separator: separator, context: [foldExt, foldWs, tabSwap, pop] }
1020
+ return bar
1021
+ }
1022
+
1023
+ function ensureBar(fx) {
1024
+ var bar = runtime.bar
1025
+ if (bar && bar.parentNode !== fx.root) {
1026
+ try {
1027
+ bar.parentNode.removeChild(bar)
1028
+ } catch (error) {
1029
+ void error
1030
+ }
1031
+ bar = null
1032
+ }
1033
+ if (!bar) {
1034
+ bar = buildBar()
1035
+ runtime.bar = bar
1036
+ }
1037
+ var anchor = fx.nav || fx.regionArea
1038
+ if (anchor && anchor.parentNode === fx.root) {
1039
+ if (bar.nextSibling !== anchor) fx.root.insertBefore(bar, anchor)
1040
+ } else if (bar.parentNode !== fx.root) {
1041
+ fx.root.appendChild(bar)
1042
+ }
1043
+ return bar
1044
+ }
1045
+
1046
+ function barButton(key) {
1047
+ if (key === 'temp') return tempButton
1048
+ if (!runtime.bar) return null
1049
+ return runtime.bar.querySelector('[data-dsbs-btn="' + key + '"]')
1050
+ }
1051
+
1052
+ function setVisible(key, visible) {
1053
+ var button = barButton(key)
1054
+ if (!button) return
1055
+ if (visible) button.removeAttribute('hidden')
1056
+ else button.setAttribute('hidden', '')
1057
+ }
1058
+
1059
+ function relabel() {
1060
+ if (!runtime.bar) return
1061
+ var nodes = runtime.bar.querySelectorAll('[data-dsbs-label]')
1062
+ for (var i = 0; i < nodes.length; i++) {
1063
+ var node = nodes[i]
1064
+ var labelKey = node.getAttribute('data-dsbs-label')
1065
+ var hintKey = node.getAttribute('data-dsbs-hint')
1066
+ // Rewriting unchanged text on every sync would drop focus from a
1067
+ // button reached with the keyboard, so only touch what changed.
1068
+ if (labelKey) {
1069
+ var label = t(labelKey)
1070
+ if (node.textContent !== label) node.textContent = label
1071
+ }
1072
+ if (hintKey) {
1073
+ var hint = t(hintKey)
1074
+ if (node.title !== hint) node.title = hint
1075
+ }
1076
+ }
1077
+ var barLabel = t('bar.label')
1078
+ if (runtime.bar.getAttribute('aria-label') !== barLabel) {
1079
+ runtime.bar.setAttribute('aria-label', barLabel)
1080
+ }
1081
+ }
1082
+
1083
+ // ---- visible state ------------------------------------------------
1084
+
1085
+ function closeOpenOverlays(element) {
1086
+ try {
1087
+ var open = element.querySelector('dialog[open], [role="menu"][data-state="open"], [data-state="open"]')
1088
+ if (!open) return
1089
+ open.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
1090
+ } catch (error) {
1091
+ void error
1092
+ }
1093
+ }
1094
+
1095
+ function setGroupHidden(element, group, hidden) {
1096
+ if (!element) return
1097
+ var was = runtime.hidden[group] === true
1098
+ if (hidden) {
1099
+ if (!was) closeOpenOverlays(element)
1100
+ element.setAttribute('aria-hidden', 'true')
1101
+ element.setAttribute('inert', '')
1102
+ } else {
1103
+ element.removeAttribute('aria-hidden')
1104
+ element.removeAttribute('inert')
1105
+ }
1106
+ if (was === hidden) return
1107
+ runtime.hidden[group] = hidden
1108
+ if (!hidden) {
1109
+ // Panels and menus position themselves from measured geometry; they
1110
+ // were hidden while that was measured, so ask them to measure again.
1111
+ try {
1112
+ window.dispatchEvent(new Event('resize'))
1113
+ } catch (error) {
1114
+ void error
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ function positionPop(fx) {
1120
+ if (!fx || !fx.root || !runtime.bar) return
1121
+ // Only the overlay is positioned; skip the two rect reads otherwise so
1122
+ // the heartbeat stays cheap.
1123
+ if (config.values.mode !== 'c' || !runtime.popOpen) return
1124
+ try {
1125
+ var rootRect = fx.root.getBoundingClientRect()
1126
+ var barRect = runtime.bar.getBoundingClientRect()
1127
+ var top = Math.round(barRect.bottom - rootRect.top + 4)
1128
+ if (!isFinite(top) || top < 0) top = 0
1129
+ fx.root.style.setProperty('--dsbs-pop-top', top + 'px')
1130
+ } catch (error) {
1131
+ void error
1132
+ }
1133
+ }
1134
+
1135
+ function paint() {
1136
+ if (runtime.disposed) return
1137
+ var state = config.values
1138
+ html.setAttribute('data-dsbs-mode', state.mode)
1139
+ html.setAttribute('data-dsbs-tab', state.tab)
1140
+ html.setAttribute('data-dsbs-quick', state.showQuickSwitch ? 'on' : 'off')
1141
+ setFlag(html, 'data-dsbs-fold-ext', state.foldExt === true)
1142
+ setFlag(html, 'data-dsbs-fold-ws', state.foldWs === true)
1143
+ setFlag(html, 'data-dsbs-pop', runtime.popOpen && state.mode === 'c')
1144
+ setFlag(html, 'data-dsbs-rail', runtime.rail)
1145
+
1146
+ var fx = resolveSidebar()
1147
+ runtime.root = fx ? fx.root : null
1148
+ runtime.nav = fx ? fx.nav : null
1149
+ runtime.regionArea = fx ? fx.regionArea : null
1150
+ if (!fx) return
1151
+
1152
+ if (fx.nav && !fx.nav.hasAttribute('data-dsbs-ext')) fx.nav.setAttribute('data-dsbs-ext', '')
1153
+ ensureBar(fx)
1154
+ placeTempButton()
1155
+ loadTempStatus()
1156
+ // Bars left behind by an earlier client generation are dead weight and
1157
+ // read as a duplicated control; keep exactly one.
1158
+ var strayBars = document.querySelectorAll('[data-dsbs-bar]')
1159
+ for (var b = 0; b < strayBars.length; b++) {
1160
+ if (strayBars[b] === runtime.bar) continue
1161
+ if (strayBars[b].parentNode) strayBars[b].parentNode.removeChild(strayBars[b])
1162
+ }
1163
+
1164
+ // With no panels to show, the buttons that act on that group are
1165
+ // pointless, and hiding the workspaces would leave an empty column.
1166
+ var hasExt = !!fx.nav
1167
+ var showFold = state.mode === 'a'
1168
+ var showTabs = state.mode === 'b'
1169
+ var showPop = state.mode === 'c'
1170
+ setVisible('fold-ext', showFold && hasExt)
1171
+ setVisible('fold-ws', showFold)
1172
+ setVisible('tab-swap', showTabs && hasExt)
1173
+ setVisible('pop', showPop && hasExt)
1174
+ setVisible('quick', state.showQuickSwitch === true)
1175
+
1176
+ // The label states what the click will do, so "收起扩展" and "展开扩展"
1177
+ // never coexist with a stale state.
1178
+ var foldExtButton = barButton('fold-ext')
1179
+ if (foldExtButton) foldExtButton.setAttribute('data-dsbs-label', state.foldExt ? 'fold.ext.expand' : 'fold.ext.collapse')
1180
+ var foldWsButton = barButton('fold-ws')
1181
+ if (foldWsButton) foldWsButton.setAttribute('data-dsbs-label', state.foldWs ? 'fold.ws.expand' : 'fold.ws.collapse')
1182
+ var popButton = barButton('pop')
1183
+ if (popButton) popButton.setAttribute('data-dsbs-label', runtime.popOpen && showPop ? 'pop.close' : 'pop.open')
1184
+ var tabSwapButton = barButton('tab-swap')
1185
+ if (tabSwapButton) {
1186
+ tabSwapButton.setAttribute('data-dsbs-label', state.tab === 'ext' ? 'tab.to.ws' : 'tab.to.ext')
1187
+ tabSwapButton.setAttribute('data-dsbs-hint', state.tab === 'ext' ? 'tab.to.ws.hint' : 'tab.to.ext.hint')
1188
+ }
1189
+
1190
+ // The divider only makes sense while at least one action button is
1191
+ // visible; an empty layout (no panels) can leave nothing after it.
1192
+ var parts = runtime.parts
1193
+ if (parts) {
1194
+ var visibleContext = 0
1195
+ for (var c = 0; c < parts.context.length; c++) {
1196
+ if (!parts.context[c].hasAttribute('hidden')) visibleContext++
1197
+ }
1198
+ if (visibleContext > 0) parts.separator.removeAttribute('hidden')
1199
+ else parts.separator.setAttribute('hidden', '')
1200
+ }
1201
+
1202
+ relabel()
1203
+
1204
+ var i
1205
+ for (i = 0; i < MODES.length; i++) {
1206
+ var modeButton = barButton('mode-' + MODES[i])
1207
+ if (modeButton) modeButton.setAttribute('aria-pressed', state.mode === MODES[i] ? 'true' : 'false')
1208
+ }
1209
+ if (foldExtButton) foldExtButton.setAttribute('aria-expanded', state.foldExt ? 'false' : 'true')
1210
+ if (foldWsButton) foldWsButton.setAttribute('aria-expanded', state.foldWs ? 'false' : 'true')
1211
+ if (popButton) {
1212
+ popButton.setAttribute('aria-expanded', runtime.popOpen && showPop ? 'true' : 'false')
1213
+ }
1214
+ if (runtime.bar) {
1215
+ runtime.bar.title = config.local ? t('bar.local') : t('bar.label')
1216
+ runtime.bar.setAttribute('aria-label', t('bar.label'))
1217
+ }
1218
+
1219
+ var extHidden
1220
+ var wsHidden
1221
+ if (state.mode === 'a') {
1222
+ extHidden = state.foldExt === true
1223
+ wsHidden = state.foldWs === true
1224
+ } else if (state.mode === 'b') {
1225
+ extHidden = state.tab !== 'ext'
1226
+ // No panels: keep the workspaces visible rather than the empty half.
1227
+ wsHidden = hasExt && state.tab === 'ext'
1228
+ } else {
1229
+ extHidden = !runtime.popOpen
1230
+ wsHidden = false
1231
+ }
1232
+ setGroupHidden(fx.nav, 'ext', extHidden)
1233
+ setGroupHidden(fx.regionArea, 'ws', wsHidden)
1234
+
1235
+ observeRoot(fx.root)
1236
+ positionPop(fx)
1237
+ }
1238
+
1239
+ // ---- state changes ------------------------------------------------
1240
+
1241
+ function setMode(mode) {
1242
+ if (MODES.indexOf(mode) < 0) return
1243
+ if (mode !== 'c' && runtime.popOpen) runtime.popOpen = false
1244
+ runtime.sessionMode = mode
1245
+ setField('mode', mode)
1246
+ }
1247
+
1248
+ function setField(field, value) {
1249
+ var patch = {}
1250
+ patch[field] = value
1251
+ config.values = assign({}, config.values, patch)
1252
+
1253
+ // `mode` is the startup default: keep it out of both the config form
1254
+ // and localStorage, so an in-session pick lives only until reload.
1255
+ // The other fields (tab / folds) still persist as before.
1256
+ if (field !== 'mode') {
1257
+ if (config.controller && typeof config.controller.mutate === 'function') {
1258
+ var revision
1259
+ try {
1260
+ var snapshot = config.controller.getSnapshot()
1261
+ revision = snapshot && snapshot.revision
1262
+ } catch (error) {
1263
+ revision = undefined
1264
+ }
1265
+ var ops = [{ op: 'set', path: [field], value: value }]
1266
+ try {
1267
+ var result = config.controller.mutate(ops, revision)
1268
+ if (result && typeof result.catch === 'function') result.catch(function () {})
1269
+ } catch (error) {
1270
+ void error
1271
+ }
1272
+ } else {
1273
+ var stored = assign({}, DEFAULTS, readLocal())
1274
+ stored[field] = value
1275
+ writeLocal(stored)
1276
+ config.local = true
1277
+ }
1278
+ }
1279
+ scheduleSync()
1280
+ }
1281
+
1282
+ // ---- observation --------------------------------------------------
1283
+
1284
+ function measureRail(fx) {
1285
+ var root = (fx && fx.root) || runtime.root
1286
+ if (!root) return
1287
+ var width = 0
1288
+ try {
1289
+ width = root.getBoundingClientRect().width
1290
+ } catch (error) {
1291
+ width = 0
1292
+ }
1293
+ var rail = width > 0 && width < 100
1294
+ // Below this width the bar cannot hold a full row once the cycle icon
1295
+ // is included, and a wrapped icon on its own line reads as debris.
1296
+ var narrow = width > 0 && width < 300
1297
+ if (narrow !== runtime.narrow) {
1298
+ runtime.narrow = narrow
1299
+ setFlag(html, 'data-dsbs-narrow', narrow)
1300
+ }
1301
+ if (rail === runtime.rail) return
1302
+ runtime.rail = rail
1303
+ if (rail) {
1304
+ runtime.railSaved = {
1305
+ mode: config.values.mode,
1306
+ tab: config.values.tab,
1307
+ foldExt: config.values.foldExt,
1308
+ foldWs: config.values.foldWs,
1309
+ }
1310
+ } else if (runtime.railSaved) {
1311
+ // Leaving the rail restores what the user had before the column was
1312
+ // squeezed; the bar is not reachable while the rail is showing.
1313
+ config.values = assign({}, config.values, runtime.railSaved)
1314
+ runtime.railSaved = null
1315
+ }
1316
+ }
1317
+
1318
+ var pending = false
1319
+ function scheduleSync() {
1320
+ if (runtime.disposed || pending) return
1321
+ pending = true
1322
+ Promise.resolve().then(function () {
1323
+ pending = false
1324
+ if (!runtime.disposed) sync()
1325
+ })
1326
+ }
1327
+
1328
+ function sync() {
1329
+ if (runtime.disposed) return
1330
+ // Both are idempotent: the status call stops after one answer, and the
1331
+ // expand clicker skips groups that are already fully shown.
1332
+ loadTempStatus()
1333
+ expandAllSessions()
1334
+ var fx = resolveSidebar()
1335
+ measureRail(fx)
1336
+ paint()
1337
+ syncChatMode()
1338
+ applyTempRows()
1339
+ }
1340
+
1341
+ function observeRoot(root) {
1342
+ if (!root || runtime.observedRoot === root) return
1343
+ if (runtime.resizeObserver) {
1344
+ try {
1345
+ runtime.resizeObserver.disconnect()
1346
+ } catch (error) {
1347
+ void error
1348
+ }
1349
+ }
1350
+ if (runtime.mutationObserver) {
1351
+ try {
1352
+ runtime.mutationObserver.disconnect()
1353
+ } catch (error) {
1354
+ void error
1355
+ }
1356
+ }
1357
+ runtime.observedRoot = root
1358
+ if (typeof ResizeObserver === 'function') {
1359
+ if (!runtime.resizeObserver) {
1360
+ runtime.resizeObserver = new ResizeObserver(function () { scheduleSync() })
1361
+ }
1362
+ try {
1363
+ runtime.resizeObserver.observe(root)
1364
+ } catch (error) {
1365
+ void error
1366
+ }
1367
+ }
1368
+ if (typeof MutationObserver === 'function') {
1369
+ if (!runtime.mutationObserver) {
1370
+ runtime.mutationObserver = new MutationObserver(function () { scheduleSync() })
1371
+ }
1372
+ try {
1373
+ // Only the root's own child list: React rebuilds the groups there,
1374
+ // and watching anything wider would turn every keystroke into work.
1375
+ runtime.mutationObserver.observe(root, { childList: true })
1376
+ } catch (error) {
1377
+ void error
1378
+ }
1379
+ }
1380
+ }
1381
+
1382
+ // ---- configuration -------------------------------------------------
1383
+
1384
+ function syncFromController() {
1385
+ if (!config.controller) return
1386
+ var snapshot = null
1387
+ try {
1388
+ snapshot = config.controller.getSnapshot()
1389
+ } catch (error) {
1390
+ snapshot = null
1391
+ }
1392
+ if (!snapshot || typeof snapshot !== 'object') return
1393
+ var next = adoptValues(snapshot.value)
1394
+ // The config form owns the startup default. A push caused by another
1395
+ // field must not undo the mode picked in this session; but a genuine
1396
+ // external change (settings page or cordis.patch.yml) wins and becomes
1397
+ // the new default for the next launch.
1398
+ if (runtime.configMode === null) {
1399
+ runtime.configMode = next.mode
1400
+ } else if (next.mode !== runtime.configMode) {
1401
+ runtime.configMode = next.mode
1402
+ runtime.sessionMode = null
1403
+ } else if (runtime.sessionMode) {
1404
+ next.mode = runtime.sessionMode
1405
+ }
1406
+ config.values = next
1407
+ config.local = false
1408
+ }
1409
+
1410
+ function tryAdopt(service) {
1411
+ if (!service || typeof service.get !== 'function') return false
1412
+ if (config.controller) return true
1413
+ var controller = null
1414
+ try {
1415
+ controller = service.get(ENTRY)
1416
+ } catch (error) {
1417
+ controller = null
1418
+ }
1419
+ if (!controller) return false
1420
+ config.controller = controller
1421
+ config.local = false
1422
+ syncFromController()
1423
+ var off = null
1424
+ try {
1425
+ if (typeof controller.subscribe === 'function') {
1426
+ off = controller.subscribe(function () {
1427
+ syncFromController()
1428
+ scheduleSync()
1429
+ })
1430
+ }
1431
+ } catch (error) {
1432
+ void error
1433
+ }
1434
+ effect(function () {
1435
+ return function () {
1436
+ if (typeof off !== 'function') return
1437
+ try {
1438
+ off()
1439
+ } catch (error) {
1440
+ void error
1441
+ }
1442
+ }
1443
+ }, 'sidebar-suite: config subscription')
1444
+ scheduleSync()
1445
+ return true
1446
+ }
1447
+
1448
+ function fallbackToLocal() {
1449
+ if (config.controller) return
1450
+ config.local = true
1451
+ config.values = adoptValues(readLocal())
1452
+ scheduleSync()
1453
+ }
1454
+
1455
+ // ---- plugin detail page: graphical default-mode picker -------------
1456
+
1457
+ function configController() {
1458
+ if (config.controller) return config.controller
1459
+ try {
1460
+ var service = safeGet(ctx, 'configForms')
1461
+ if (service && typeof service.get === 'function') return service.get(ENTRY)
1462
+ } catch (error) {
1463
+ void error
1464
+ }
1465
+ return null
1466
+ }
1467
+
1468
+ function readDefaultMode() {
1469
+ var controller = configController()
1470
+ try {
1471
+ var snapshot = controller && controller.getSnapshot && controller.getSnapshot()
1472
+ var value = snapshot && snapshot.value
1473
+ return MODES.indexOf(value && value.mode) >= 0 ? value.mode : DEFAULTS.mode
1474
+ } catch (error) {
1475
+ return DEFAULTS.mode
1476
+ }
1477
+ }
1478
+
1479
+ function writeDefaultMode(mode) {
1480
+ if (MODES.indexOf(mode) < 0) return false
1481
+ var controller = configController()
1482
+ if (!controller || typeof controller.mutate !== 'function') return false
1483
+ try {
1484
+ var snapshot = controller.getSnapshot && controller.getSnapshot()
1485
+ var result = controller.mutate(
1486
+ [{ op: 'set', path: ['mode'], value: mode }],
1487
+ snapshot && snapshot.revision
1488
+ )
1489
+ if (result && typeof result.catch === 'function') result.catch(function () {})
1490
+ // The settings page and the sidebar share this client run, so adopt
1491
+ // the new default immediately instead of waiting for a config push.
1492
+ runtime.sessionMode = null
1493
+ runtime.configMode = mode
1494
+ config.values = assign({}, config.values, { mode: mode })
1495
+ if (runtime.popOpen && mode !== 'c') runtime.popOpen = false
1496
+ scheduleSync()
1497
+ return true
1498
+ } catch (error) {
1499
+ return false
1500
+ }
1501
+ }
1502
+
1503
+ // The Host serves images that sit beside a bundle's detail document.
1504
+ // This is the same URL the Plugin Manager's own markdown renderer uses,
1505
+ // so the small QR in the pinned settings card needs no base64 payload.
1506
+ function supportQrUrl() {
1507
+ try {
1508
+ if (typeof document === 'undefined' || !document.baseURI) return ''
1509
+ var query = 'bundle=' + encodeURIComponent(ENTRY) +
1510
+ '&document=' + encodeURIComponent('DETAIL.md') +
1511
+ '&path=' + encodeURIComponent('assets/wechat-qr.png')
1512
+ return new URL('api/plugin-manager/detail-file?' + query, document.baseURI).href
1513
+ } catch (error) {
1514
+ return ''
1515
+ }
1516
+ }
1517
+
1518
+ // `plugins.bundle.config` is the keyed slot the Plugin Manager declares
1519
+ // for a bundle's own configuration page. The component reads and writes
1520
+ // the same configForms controller as the sidebar, so a click here becomes
1521
+ // the new startup default and the sidebar follows it immediately.
1522
+ function makeModePicker(h, ReactLib) {
1523
+ var box = function (extra) {
1524
+ return h('span', { style: assign({
1525
+ display: 'block', borderRadius: '2px',
1526
+ background: 'var(--dsw-alias-label-tertiary,rgba(127,127,127,.5))'
1527
+ }, extra || {}) })
1528
+ }
1529
+ var frame = function (extra, first, second) {
1530
+ return h('span', { style: assign({
1531
+ display: 'flex', flexDirection: 'column', width: '36px', height: '24px',
1532
+ padding: '2px', gap: '2px', boxSizing: 'border-box', position: 'relative',
1533
+ borderRadius: '4px',
1534
+ border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))'
1535
+ }, extra || {}) }, first || null, second || null)
1536
+ }
1537
+ function mini(mode) {
1538
+ if (mode === 'a') {
1539
+ return frame(null, box({ width: '100%', height: '7px' }), box({ width: '100%', height: '7px' }))
1540
+ }
1541
+ if (mode === 'b') {
1542
+ return frame(null, box({ width: '60%', height: '4px' }), box({ width: '100%', height: '11px' }))
1543
+ }
1544
+ return frame(null,
1545
+ box({ width: '100%', height: '100%', opacity: '0.35' }),
1546
+ box({
1547
+ position: 'absolute', left: '9px', top: '5px', width: '18px', height: '13px',
1548
+ background: 'var(--dsw-alias-label-primary,currentColor)', opacity: '0.85',
1549
+ boxShadow: '0 2px 6px rgba(0,0,0,.28)'
1550
+ })
1551
+ )
1552
+ }
1553
+
1554
+ return function ModePicker() {
1555
+ var state = ReactLib.useState(readDefaultMode())
1556
+ var mode = state[0]
1557
+ var setMode = state[1]
1558
+ ReactLib.useEffect(function () {
1559
+ var controller = configController()
1560
+ if (!controller || typeof controller.subscribe !== 'function') return
1561
+ var off = null
1562
+ try {
1563
+ off = controller.subscribe(function () { setMode(readDefaultMode()) })
1564
+ } catch (error) {
1565
+ void error
1566
+ }
1567
+ return function () {
1568
+ try { if (typeof off === 'function') off() } catch (error) { void error }
1569
+ }
1570
+ }, [])
1571
+
1572
+ // ---- temporary-session cleanup policy -------------------------
1573
+ // The Host half owns this: it is the only side that can delete files,
1574
+ // and the values live in temp-session-config.txt rather than in the
1575
+ // plugin config the cards above write. So this section talks to
1576
+ // /api/dsh-temp-session.config and never touches configForms — one
1577
+ // source of truth, no "settings page and file disagree" state.
1578
+ var policyState = ReactLib.useState(null)
1579
+ var policy = policyState[0]
1580
+ var setPolicy = policyState[1]
1581
+ var saveState = ReactLib.useState('')
1582
+ var saveStatus = saveState[0]
1583
+ var setSaveStatus = saveState[1]
1584
+
1585
+ // ---- feedback -------------------------------------------------
1586
+ // A plugin bundle has no server to post to, and no mail client it can
1587
+ // trust. So this block builds a complete report out of facts the Host
1588
+ // half already knows (version, OS, layout, button mode, route count,
1589
+ // last error) and then offers three channels that need nothing:
1590
+ // the clipboard, a pre-filled GitHub issue, and the plugin market's
1591
+ // own comment thread. DSH itself has no plugin-feedback field in its
1592
+ // manifest (verified in app.asar: no bugs/feedback handling in the
1593
+ // plugin-manager client); dsh-market is the surface that defines one.
1594
+ // The desktop shell opens http(s) links in the system browser
1595
+ // (app.asar lib/main.js attaches setWindowOpenHandler to the main
1596
+ // window), so an <a>-style jump works; because that handler answers
1597
+ // "deny" after handing the URL to the OS, `window.open` returns null
1598
+ // even on success — so the return value must never be read as "failed".
1599
+ var noteState = ReactLib.useState(readDraft())
1600
+ var note = noteState[0]
1601
+ var setNote = noteState[1]
1602
+ var fbState = ReactLib.useState('')
1603
+ var fbStatus = fbState[0]
1604
+ var setFbStatus = fbState[1]
1605
+
1606
+ function readDraft() {
1607
+ try {
1608
+ return String(window.localStorage.getItem(FEEDBACK_DRAFT_KEY) || '')
1609
+ } catch (error) {
1610
+ void error
1611
+ return ''
1612
+ }
1613
+ }
1614
+
1615
+ function updateNote(value) {
1616
+ setNote(value)
1617
+ try {
1618
+ if (value) window.localStorage.setItem(FEEDBACK_DRAFT_KEY, value)
1619
+ else window.localStorage.removeItem(FEEDBACK_DRAFT_KEY)
1620
+ } catch (error) {
1621
+ void error
1622
+ }
1623
+ }
1624
+
1625
+ function legacyCopy(text) {
1626
+ var area = document.createElement('textarea')
1627
+ area.value = text
1628
+ area.setAttribute('readonly', '')
1629
+ area.style.position = 'fixed'
1630
+ area.style.top = '-1000px'
1631
+ area.style.opacity = '0'
1632
+ document.body.appendChild(area)
1633
+ try {
1634
+ area.select()
1635
+ var done = typeof document.execCommand === 'function' && document.execCommand('copy')
1636
+ if (!done) throw new Error('copy blocked')
1637
+ } finally {
1638
+ try { if (area.parentNode) area.parentNode.removeChild(area) } catch (error) { void error }
1639
+ }
1640
+ }
1641
+
1642
+ function copyText(text) {
1643
+ var clip = typeof navigator !== 'undefined' ? navigator.clipboard : null
1644
+ if (clip && typeof clip.writeText === 'function') {
1645
+ return clip.writeText(text).catch(function () { legacyCopy(text) })
1646
+ }
1647
+ try {
1648
+ legacyCopy(text)
1649
+ return Promise.resolve()
1650
+ } catch (error) {
1651
+ return Promise.reject(error)
1652
+ }
1653
+ }
1654
+
1655
+ // One status call is enough for a whole session of editing: version
1656
+ // and OS do not change while the page is open.
1657
+ var diagCache = null
1658
+ function loadDiagnostics() {
1659
+ if (diagCache) return Promise.resolve(diagCache)
1660
+ return tempCall('api/dsh-temp-session.status').then(function (data) {
1661
+ if (data && data.ok === true) diagCache = data
1662
+ return diagCache
1663
+ }).catch(function () {
1664
+ return null
1665
+ })
1666
+ }
1667
+
1668
+ function reportText(diag) {
1669
+ var env = (diag && diag.env) || {}
1670
+ var purge = diag && diag.lastPurge
1671
+ var lines = []
1672
+ lines.push('### What happened')
1673
+ lines.push((note || '').trim() || '(no description)')
1674
+ lines.push('')
1675
+ lines.push('### Environment')
1676
+ lines.push('- plugin: ' + (env.plugin || '?') + ' · default layout: ' + readDefaultMode())
1677
+ lines.push('- dsh desktop: ' + (env.dshClient || '?')
1678
+ + ' · node ' + (env.node || '?')
1679
+ + (env.electron ? ' · electron ' + env.electron : ''))
1680
+ lines.push('- system: ' + ([env.platform, env.os, env.arch].filter(Boolean).join(' ') || '?'))
1681
+ lines.push('- temp button: ' + ((diag && diag.buttonStyle) || '?')
1682
+ + ' · tracked: ' + (diag ? diag.tempCount : '?')
1683
+ + ' · routes: ' + ((diag && diag.routes && diag.routes.registered) || 0)
1684
+ + ' · quit-cleanup: ' + (diag && diag.deleteOnExit === false ? 'off' : 'on')
1685
+ + (diag && diag.scheduleDelete ? ' · daily ' + diag.scheduleTime : ''))
1686
+ if (purge) {
1687
+ lines.push('- last cleanup: ' + purge.reason + ' @ ' + purge.at
1688
+ + ' → removed ' + purge.removed + '/' + purge.tried)
1689
+ }
1690
+ if (diag && diag.lastError) lines.push('- last error: ' + diag.lastError)
1691
+ return lines.join('\n')
1692
+ }
1693
+
1694
+ function issueUrl(text) {
1695
+ var title = (note || '').trim().split('\n')[0].trim().slice(0, 80)
1696
+ if (!title) title = 'dsh-sidebar-suite feedback'
1697
+ var head = FEEDBACK_REPO + '/issues/new?title=' + encodeURIComponent(title) + '&body='
1698
+ // Long URLs are rejected by the OS handler and by GitHub. Trim the
1699
+ // description first — the environment block is what makes a report
1700
+ // actionable, and it is short.
1701
+ var limit = 6500
1702
+ var body = text
1703
+ var encoded = encodeURIComponent(body)
1704
+ while (body.length > 0 && head.length + encoded.length > limit) {
1705
+ body = text.slice(0, Math.max(0, body.length - 240)) + '\n…(truncated)'
1706
+ encoded = encodeURIComponent(body)
1707
+ }
1708
+ return head + encoded
1709
+ }
1710
+
1711
+ function flashFeedback(kind) {
1712
+ setFbStatus(kind)
1713
+ try {
1714
+ window.setTimeout(function () {
1715
+ setFbStatus(function (previous) { return previous === kind ? '' : previous })
1716
+ }, 3200)
1717
+ } catch (error) {
1718
+ void error
1719
+ }
1720
+ }
1721
+
1722
+ function copyReport() {
1723
+ setFbStatus('working')
1724
+ loadDiagnostics()
1725
+ .then(function (diag) { return copyText(reportText(diag)) })
1726
+ .then(function () { flashFeedback('copied') })
1727
+ .catch(function () { setFbStatus('failed') })
1728
+ }
1729
+
1730
+ function openIssue() {
1731
+ setFbStatus('working')
1732
+ loadDiagnostics()
1733
+ .then(function (diag) {
1734
+ try {
1735
+ window.open(issueUrl(reportText(diag)), '_blank')
1736
+ } catch (error) {
1737
+ void error
1738
+ }
1739
+ flashFeedback('issueOpened')
1740
+ })
1741
+ .catch(function () { setFbStatus('failed') })
1742
+ }
1743
+
1744
+ // The plugin market (dsh-market) defines its own feedback entry: one
1745
+ // GitHub Discussion per plugin, keyed on `plugin:<owner/repo>` in the
1746
+ // public catalog repository. No diagnostics are needed — the note is
1747
+ // typed there directly — so this is a plain link to that exact thread.
1748
+ function openMarket() {
1749
+ try {
1750
+ window.open(FEEDBACK_MARKET_URL, '_blank')
1751
+ } catch (error) {
1752
+ void error
1753
+ }
1754
+ flashFeedback('issueOpened')
1755
+ }
1756
+
1757
+ function adoptPolicy(data, fallbackTime) {
1758
+ setPolicy({
1759
+ deleteOnExit: data.deleteOnExit !== false,
1760
+ scheduleDelete: data.scheduleDelete === true,
1761
+ scheduleTime: typeof data.scheduleTime === 'string' && data.scheduleTime
1762
+ ? data.scheduleTime
1763
+ : (fallbackTime || '03:00'),
1764
+ })
1765
+ }
1766
+
1767
+ ReactLib.useEffect(function () {
1768
+ var stopped = false
1769
+ tempCall('api/dsh-temp-session.config')
1770
+ .then(function (data) {
1771
+ if (stopped) return
1772
+ if (!data || data.ok !== true) { setPolicy(false); return }
1773
+ adoptPolicy(data)
1774
+ })
1775
+ .catch(function () {
1776
+ if (!stopped) setPolicy(false)
1777
+ })
1778
+ return function () { stopped = true }
1779
+ }, [])
1780
+
1781
+ function persist(patch) {
1782
+ if (!policy || policy === false) return
1783
+ setPolicy(assign({}, policy, patch))
1784
+ setSaveStatus('saving')
1785
+ tempCall('api/dsh-temp-session.config', { method: 'POST', body: JSON.stringify(patch) })
1786
+ .then(function (data) {
1787
+ if (!data || data.ok !== true) throw new Error((data && data.error) || 'save failed')
1788
+ adoptPolicy(data, patch.scheduleTime)
1789
+ setSaveStatus('saved')
1790
+ // Fade the confirmation out; a later failure must not be wiped.
1791
+ try {
1792
+ window.setTimeout(function () {
1793
+ setSaveStatus(function (previous) { return previous === 'saved' ? '' : previous })
1794
+ }, 2400)
1795
+ } catch (error) {
1796
+ void error
1797
+ }
1798
+ })
1799
+ .catch(function (error) {
1800
+ setSaveStatus(t('policy.failed') + String((error && error.message) || error))
1801
+ })
1802
+ }
1803
+
1804
+ var available = !!configController()
1805
+ var policyReady = !!(policy && policy !== false)
1806
+ var dim = { fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary,inherit)' }
1807
+ var pillBase = {
1808
+ font: 'inherit', fontSize: '11px', lineHeight: '16px', padding: '2px 10px',
1809
+ minWidth: '46px', borderRadius: '999px', cursor: 'pointer',
1810
+ border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))',
1811
+ background: 'transparent',
1812
+ color: 'var(--dsw-alias-label-secondary,inherit)'
1813
+ }
1814
+ var pillOn = {
1815
+ borderColor: 'var(--dsw-alias-label-primary,currentColor)',
1816
+ background: 'var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.22))',
1817
+ color: 'var(--dsw-alias-label-primary,inherit)'
1818
+ }
1819
+ function policyRow(label, active, onToggle, extra) {
1820
+ return h('div', {
1821
+ key: label,
1822
+ style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }
1823
+ },
1824
+ h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary,inherit)' } }, label),
1825
+ h('button', {
1826
+ type: 'button',
1827
+ 'aria-pressed': active ? 'true' : 'false',
1828
+ disabled: !policyReady,
1829
+ onClick: function () { onToggle(!active) },
1830
+ style: assign({}, pillBase, active ? pillOn : {})
1831
+ }, active ? t('policy.on') : t('policy.off')),
1832
+ extra || null
1833
+ )
1834
+ }
1835
+ var cardBase = {
1836
+ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '5px',
1837
+ flex: '1 1 140px', minWidth: '140px', maxWidth: '230px', padding: '10px',
1838
+ borderRadius: '10px', font: 'inherit', textAlign: 'left', cursor: 'pointer',
1839
+ color: 'var(--dsw-alias-label-primary,inherit)',
1840
+ border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))',
1841
+ background: 'transparent'
1842
+ }
1843
+ return h('div', { 'data-dsbs-config': ENTRY, style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
1844
+ h('div', { 'data-dsbs-intro': ENTRY, style: {
1845
+ fontSize: '11px', lineHeight: '16px',
1846
+ color: 'var(--dsw-alias-label-secondary,inherit)'
1847
+ } }, t('settings.intro')),
1848
+ h('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('settings.title')),
1849
+ h('div', { style: { fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary,inherit)' } }, t('settings.help')),
1850
+ h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
1851
+ MODES.map(function (value) {
1852
+ var active = value === mode
1853
+ return h('button', {
1854
+ key: value, type: 'button',
1855
+ 'aria-pressed': active ? 'true' : 'false',
1856
+ title: t('mode.' + value + '.hint'),
1857
+ disabled: !available,
1858
+ style: assign({}, cardBase, active ? {
1859
+ borderColor: 'var(--dsw-alias-label-primary,currentColor)',
1860
+ background: 'var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.22))'
1861
+ } : {}),
1862
+ onClick: function () {
1863
+ if (writeDefaultMode(value)) setMode(value)
1864
+ }
1865
+ },
1866
+ mini(value),
1867
+ h('span', { style: { fontSize: '12px', fontWeight: active ? '600' : '400' } },
1868
+ t('mode.' + value) + (active ? ' · ' + t('settings.current') : '')),
1869
+ h('span', { style: { fontSize: '11px', lineHeight: '15px', color: 'var(--dsw-alias-label-secondary,inherit)' } },
1870
+ t('mode.' + value + '.hint'))
1871
+ )
1872
+ })
1873
+ ),
1874
+ available ? null : h('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary,inherit)' } }, t('settings.unavailable')),
1875
+ h('div', { 'data-dsbs-temp-policy': ENTRY, style: {
1876
+ display: 'flex', flexDirection: 'column', gap: '8px',
1877
+ paddingTop: '12px', marginTop: '2px',
1878
+ borderTop: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))'
1879
+ } },
1880
+ h('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('policy.title')),
1881
+ h('div', { style: dim }, t('policy.help')),
1882
+ policy === null
1883
+ ? h('div', { style: dim }, t('policy.loading'))
1884
+ : policy === false
1885
+ ? h('div', { style: dim }, t('policy.unavailable'))
1886
+ : h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
1887
+ policyRow(t('policy.onQuit'), policy.deleteOnExit === true, function (next) {
1888
+ persist({ deleteOnExit: next })
1889
+ }),
1890
+ policyRow(t('policy.schedule'), policy.scheduleDelete === true, function (next) {
1891
+ persist({ scheduleDelete: next })
1892
+ }, h('input', {
1893
+ type: 'time',
1894
+ value: policy.scheduleTime,
1895
+ step: 60,
1896
+ 'aria-label': t('policy.schedule'),
1897
+ onChange: function (event) {
1898
+ var value = event && event.target ? event.target.value : ''
1899
+ if (!value) return
1900
+ persist({ scheduleTime: value })
1901
+ },
1902
+ style: {
1903
+ font: 'inherit', fontSize: '12px', padding: '1px 6px',
1904
+ borderRadius: '6px', color: 'var(--dsw-alias-label-primary,inherit)',
1905
+ background: 'transparent',
1906
+ opacity: policy.scheduleDelete ? '1' : '0.55',
1907
+ border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))'
1908
+ }
1909
+ })),
1910
+ h('div', { style: dim }, t('policy.note')),
1911
+ saveStatus
1912
+ ? h('div', {
1913
+ style: {
1914
+ fontSize: '11px', lineHeight: '16px',
1915
+ color: saveStatus === 'saved'
1916
+ ? 'var(--dsw-alias-label-secondary,inherit)'
1917
+ : saveStatus === 'saving'
1918
+ ? 'var(--dsw-alias-label-tertiary,inherit)'
1919
+ : 'var(--dsw-alias-text-error,#e5484d)'
1920
+ }
1921
+ }, saveStatus === 'saving' ? t('policy.saving') : saveStatus === 'saved' ? t('policy.saved') : saveStatus)
1922
+ : null
1923
+ )
1924
+ ),
1925
+ h('div', { 'data-dsbs-feedback': ENTRY, style: {
1926
+ display: 'flex', flexDirection: 'column', gap: '8px',
1927
+ paddingTop: '12px', marginTop: '2px',
1928
+ borderTop: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))'
1929
+ } },
1930
+ h('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('feedback.title')),
1931
+ h('div', { style: dim }, t('feedback.help')),
1932
+ h('textarea', {
1933
+ 'data-dsbs-feedback-note': '',
1934
+ value: note,
1935
+ rows: 2,
1936
+ maxLength: 4000,
1937
+ placeholder: t('feedback.placeholder'),
1938
+ 'aria-label': t('feedback.title'),
1939
+ onChange: function (event) {
1940
+ updateNote(event && event.target ? String(event.target.value || '') : '')
1941
+ },
1942
+ style: {
1943
+ width: '100%', boxSizing: 'border-box', resize: 'vertical',
1944
+ minHeight: '44px', maxHeight: '160px',
1945
+ padding: '6px 8px', borderRadius: '8px',
1946
+ font: 'inherit', fontSize: '12px', lineHeight: '18px',
1947
+ color: 'var(--dsw-alias-label-primary,inherit)',
1948
+ background: 'transparent',
1949
+ border: '1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.3))'
1950
+ }
1951
+ }),
1952
+ h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' } },
1953
+ h('button', {
1954
+ type: 'button',
1955
+ 'data-dsbs-feedback-copy': '',
1956
+ disabled: fbStatus === 'working',
1957
+ onClick: copyReport,
1958
+ style: assign({}, pillBase, { minWidth: '0', padding: '4px 12px' })
1959
+ }, fbStatus === 'copied'
1960
+ ? t('feedback.copied')
1961
+ : fbStatus === 'working'
1962
+ ? t('feedback.working')
1963
+ : t('feedback.copy')),
1964
+ h('button', {
1965
+ type: 'button',
1966
+ 'data-dsbs-feedback-issue': '',
1967
+ disabled: fbStatus === 'working',
1968
+ onClick: openIssue,
1969
+ style: assign({}, pillBase, { minWidth: '0', padding: '4px 12px' })
1970
+ }, fbStatus === 'issueOpened' ? t('feedback.issueOpened') : t('feedback.issue')),
1971
+ h('button', {
1972
+ type: 'button',
1973
+ 'data-dsbs-feedback-market': '',
1974
+ onClick: openMarket,
1975
+ style: assign({}, pillBase, { minWidth: '0', padding: '4px 12px' })
1976
+ }, t('feedback.market')),
1977
+ fbStatus === 'failed'
1978
+ ? h('span', { style: { fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-text-error,#e5484d)' } }, t('feedback.failed'))
1979
+ : null
1980
+ ),
1981
+ h('div', { style: dim }, t('feedback.hint')),
1982
+ h('div', { style: dim }, t('feedback.where')),
1983
+ h('div', { style: dim }, t('feedback.marketHint'))
1984
+ ),
1985
+ h('div', { 'data-dsbs-support': ENTRY },
1986
+ supportQrUrl() ? h('img', {
1987
+ src: supportQrUrl(),
1988
+ alt: t('settings.qrAlt'),
1989
+ onError: function (event) {
1990
+ try { event.currentTarget.style.display = 'none' } catch (error) { void error }
1991
+ }
1992
+ }) : null,
1993
+ h('div', { 'data-dsbs-support-text': ENTRY },
1994
+ h('div', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('settings.support')),
1995
+ h('div', { style: { fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary,inherit)' } }, t('settings.supportHint'))
1996
+ )
1997
+ )
1998
+ )
1999
+ }
2000
+ }
2001
+
2002
+ // Register only when React and the slots service are both available.
2003
+ // Any failure here is contained: the sidebar control bar is unaffected.
2004
+ if (React && React.createElement && React.useState && React.useEffect && typeof ctx.inject === 'function') {
2005
+ try {
2006
+ ctx.inject(['slots'], function (scope) {
2007
+ var slots = scope && scope.slots
2008
+ if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') return
2009
+ try {
2010
+ slots.inject('plugins.bundle.config', function () {
2011
+ return slots.register({
2012
+ name: 'plugins.bundle.config',
2013
+ key: ENTRY,
2014
+ order: 0,
2015
+ }, makeModePicker(React.createElement, React))
2016
+ })
2017
+ } catch (error) {
2018
+ void error
2019
+ }
2020
+ })
2021
+ } catch (error) {
2022
+ void error
2023
+ }
2024
+ }
2025
+
2026
+ // ---- wiring -------------------------------------------------------
2027
+
2028
+ effect(function () {
2029
+ var existing = document.getElementById(STYLE_ID)
2030
+ if (existing && existing.parentNode) existing.parentNode.removeChild(existing)
2031
+ var style = document.createElement('style')
2032
+ style.id = STYLE_ID
2033
+ style.setAttribute('data-plugin', ENTRY)
2034
+ style.textContent = CSS
2035
+ ;(document.head || document.documentElement).appendChild(style)
2036
+ return function () {
2037
+ if (style.parentNode) style.parentNode.removeChild(style)
2038
+ }
2039
+ }, 'sidebar-suite: styles')
2040
+
2041
+ effect(function () {
2042
+ if (!localeService || typeof localeService.subscribe !== 'function') return
2043
+ var off = localeService.subscribe(function () {
2044
+ t.refresh()
2045
+ relabel()
2046
+ scheduleSync()
2047
+ })
2048
+ return function () {
2049
+ try {
2050
+ off()
2051
+ } catch (error) {
2052
+ void error
2053
+ }
2054
+ }
2055
+ }, 'sidebar-suite: locale')
2056
+
2057
+ effect(function () {
2058
+ var stopped = false
2059
+ var timer = null
2060
+
2061
+ function attempt() {
2062
+ if (stopped) return true
2063
+ if (tryAdopt(safeGet(ctx, 'configForms'))) return true
2064
+ return false
2065
+ }
2066
+
2067
+ try {
2068
+ if (typeof ctx.inject === 'function') {
2069
+ ctx.inject(['configForms'], function (scope) {
2070
+ if (stopped) return
2071
+ try {
2072
+ tryAdopt(scope && scope.configForms)
2073
+ } catch (error) {
2074
+ void error
2075
+ }
2076
+ })
2077
+ }
2078
+ } catch (error) {
2079
+ void error
2080
+ }
2081
+
2082
+ if (attempt()) return function () { stopped = true }
2083
+
2084
+ // The client's config service may come up after this plugin: retry for
2085
+ // ten seconds, then fall back to device-local storage.
2086
+ var tries = 0
2087
+ timer = setInterval(function () {
2088
+ tries += 1
2089
+ if (attempt() || tries > 40) {
2090
+ clearInterval(timer)
2091
+ timer = null
2092
+ if (!config.controller) fallbackToLocal()
2093
+ }
2094
+ }, 250)
2095
+ if (timer && typeof timer.unref === 'function') timer.unref()
2096
+
2097
+ return function () {
2098
+ stopped = true
2099
+ if (timer) {
2100
+ clearInterval(timer)
2101
+ timer = null
2102
+ }
2103
+ }
2104
+ }, 'sidebar-suite: configuration')
2105
+
2106
+ effect(function () {
2107
+ var onPointerDown = function (event) {
2108
+ if (config.values.mode !== 'c' || !runtime.popOpen) return
2109
+ var target = event.target
2110
+ if (!target) return
2111
+ if (runtime.bar && runtime.bar.contains(target)) return
2112
+ if (runtime.nav && runtime.nav.contains(target)) return
2113
+ runtime.popOpen = false
2114
+ scheduleSync()
2115
+ }
2116
+ var onKeyDown = function (event) {
2117
+ if (event.key !== 'Escape' || !runtime.popOpen) return
2118
+ runtime.popOpen = false
2119
+ // Esc must hand focus back to the trigger, or the next Tab starts
2120
+ // from wherever the pointer last landed.
2121
+ var trigger = barButton('pop')
2122
+ if (trigger && typeof trigger.focus === 'function') trigger.focus()
2123
+ scheduleSync()
2124
+ }
2125
+ var onResize = function () {
2126
+ scheduleSync()
2127
+ }
2128
+ document.addEventListener('pointerdown', onPointerDown, true)
2129
+ document.addEventListener('keydown', onKeyDown, true)
2130
+ window.addEventListener('resize', onResize)
2131
+ return function () {
2132
+ document.removeEventListener('pointerdown', onPointerDown, true)
2133
+ document.removeEventListener('keydown', onKeyDown, true)
2134
+ window.removeEventListener('resize', onResize)
2135
+ }
2136
+ }, 'sidebar-suite: dismissal')
2137
+
2138
+ effect(function () {
2139
+ // Safety net: the sidebar root is created and destroyed by the host, so
2140
+ // a root replacement is invisible to an observer that watches the old
2141
+ // root's children. A slow heartbeat re-resolves it (one query and one
2142
+ // rect read) and is skipped while the page is hidden.
2143
+ var timer = setInterval(function () {
2144
+ if (typeof document.hidden === 'boolean' && document.hidden) return
2145
+ sync()
2146
+ }, 1500)
2147
+ if (timer && typeof timer.unref === 'function') timer.unref()
2148
+ sync()
2149
+ return function () {
2150
+ clearInterval(timer)
2151
+ }
2152
+ }, 'sidebar-suite: sync')
2153
+
2154
+ var teardown = null
2155
+ effect(function () {
2156
+ teardown = function () {
2157
+ if (runtime.disposed) return
2158
+ runtime.disposed = true
2159
+ try {
2160
+ clearTimeout(tempToastTimer)
2161
+ var toastNode = document.querySelector('[data-dsbs-temp-toast]')
2162
+ if (toastNode && toastNode.parentNode) toastNode.parentNode.removeChild(toastNode)
2163
+ } catch (error) {
2164
+ void error
2165
+ }
2166
+ try {
2167
+ var tagged = document.querySelectorAll('[data-dsbs-temp-row]')
2168
+ for (var t = 0; t < tagged.length; t++) {
2169
+ tagged[t].removeAttribute('data-dsbs-temp-row')
2170
+ tagged[t].removeAttribute('data-dsbs-temp-badge')
2171
+ }
2172
+ } catch (error) {
2173
+ void error
2174
+ }
2175
+ try {
2176
+ if (tempSlot && tempSlot.parentNode) tempSlot.parentNode.removeChild(tempSlot)
2177
+ else if (tempButton && tempButton.parentNode) tempButton.parentNode.removeChild(tempButton)
2178
+ tempButton = null
2179
+ tempSlot = null
2180
+ } catch (error) {
2181
+ void error
2182
+ }
2183
+ try {
2184
+ if (tempNewChatHost) tempNewChatHost.removeAttribute('data-dsbs-newchat')
2185
+ tempNewChatHost = null
2186
+ } catch (error) {
2187
+ void error
2188
+ }
2189
+ try {
2190
+ if (tempChatGuard !== null) clearInterval(tempChatGuard)
2191
+ tempChatGuard = null
2192
+ } catch (error) {
2193
+ void error
2194
+ }
2195
+ try {
2196
+ setFlag(html, 'data-dsbs-chat-mode', false)
2197
+ } catch (error) {
2198
+ void error
2199
+ }
2200
+ try {
2201
+ clearTimeout(expandTimer)
2202
+ clearTimeout(expandGateTimer)
2203
+ expandTimer = 0
2204
+ expandGateTimer = 0
2205
+ expandAllKnown = false
2206
+ expandRounds = 0
2207
+ expandCooldown = Object.create(null)
2208
+ html.removeAttribute('data-dsbs-expand-all')
2209
+ } catch (error) {
2210
+ void error
2211
+ }
2212
+ try {
2213
+ if (runtime.bar && runtime.bar.parentNode) runtime.bar.parentNode.removeChild(runtime.bar)
2214
+ } catch (error) {
2215
+ void error
2216
+ }
2217
+ try {
2218
+ if (window[OWNER_KEY] === owner) delete window[OWNER_KEY]
2219
+ } catch (error) {
2220
+ void error
2221
+ }
2222
+ try {
2223
+ if (runtime.root) runtime.root.style.removeProperty('--dsbs-pop-top')
2224
+ } catch (error) {
2225
+ void error
2226
+ }
2227
+ try {
2228
+ if (runtime.nav) runtime.nav.removeAttribute('data-dsbs-ext')
2229
+ } catch (error) {
2230
+ void error
2231
+ }
2232
+ try {
2233
+ if (runtime.nav) {
2234
+ runtime.nav.removeAttribute('aria-hidden')
2235
+ runtime.nav.removeAttribute('inert')
2236
+ }
2237
+ if (runtime.regionArea) {
2238
+ runtime.regionArea.removeAttribute('aria-hidden')
2239
+ runtime.regionArea.removeAttribute('inert')
2240
+ }
2241
+ } catch (error) {
2242
+ void error
2243
+ }
2244
+ var attributes = [
2245
+ 'data-dsbs-mode',
2246
+ 'data-dsbs-tab',
2247
+ 'data-dsbs-fold-ext',
2248
+ 'data-dsbs-fold-ws',
2249
+ 'data-dsbs-pop',
2250
+ 'data-dsbs-quick',
2251
+ 'data-dsbs-rail',
2252
+ 'data-dsbs-narrow',
2253
+ 'data-dsbs-temp-solo',
2254
+ 'data-dsbs-temp-style',
2255
+ ]
2256
+ for (var i = 0; i < attributes.length; i++) {
2257
+ html.removeAttribute(attributes[i])
2258
+ }
2259
+ try {
2260
+ if (runtime.resizeObserver) runtime.resizeObserver.disconnect()
2261
+ if (runtime.mutationObserver) runtime.mutationObserver.disconnect()
2262
+ } catch (error) {
2263
+ void error
2264
+ }
2265
+ runtime.bar = null
2266
+ runtime.root = null
2267
+ runtime.observedRoot = null
2268
+ for (var j = 0; j < manualCleanups.length; j++) {
2269
+ try {
2270
+ manualCleanups[j]()
2271
+ } catch (error) {
2272
+ void error
2273
+ }
2274
+ }
2275
+ manualCleanups.length = 0
2276
+ }
2277
+ return teardown
2278
+ }, 'sidebar-suite: teardown')
2279
+ owner.teardown = teardown
2280
+ }
2281
+
2282
+ return { inject: ['locale'], apply: apply }
2283
+ },
2284
+ })
2285
+