dsh-sidebar-suite 0.2.6 → 0.2.8

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