dsh-deepseek-balance-widget 1.0.0

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 ADDED
@@ -0,0 +1,467 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-deepseek-balance-widget",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ /** Route the host half exposes (same origin). */
8
+ const ENDPOINT = "/deepseek-balance";
9
+ /** Real-time usage route (platform userToken backed; neutral placeholder when unset). */
10
+ const USAGE_ENDPOINT = "/deepseek-usage";
11
+ /** Auto-refresh cadence, milliseconds. */
12
+ const REFRESH_MS = 30000;
13
+
14
+ /** Inline icon (matches the shell's 16px nav-icon look). */
15
+ const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 4L8 8.5 11.5 4M8 8.5v4M5.5 9.5h5M5.5 11.5h5"/></svg>';
16
+
17
+ /**
18
+ * 注意:本插件不在客户端内置任何个人数据。
19
+ * 余额与用量全部由宿主端用「本机」的凭据实时查询;未配置
20
+ * DEEPSEEK_PLATFORM_TOKEN 时,用量区域显示中性提示而不是任何人的快照。
21
+ */
22
+
23
+ /** Inject the widget stylesheet once. */
24
+ function ensureStyle() {
25
+ const tagId = "dsh-deepseek-balance-widget-css";
26
+ if (document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") !== null) return;
27
+ const css = [
28
+ ".dshBalanceEntry{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:8px;align-items:center;gap:10px;padding:9px 12px;display:flex;transition:background .14s}",
29
+ ".dshBalanceEntry:hover{background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.06))}",
30
+ ".dshBalanceEntry[data-active=true]{background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.06))}",
31
+ ".dshBalanceEntry.compact{width:auto;gap:10px;padding:6px 8px;flex:none}",
32
+ ".dshBalanceIcon{color:var(--dsw-alias-label-secondary,#9aa4b2);flex:none;display:flex}",
33
+ ".dshBalanceEntry.compact .dshBalanceIcon{width:16px;height:16px;justify-content:center;align-items:center}",
34
+ ".dshBalanceLabel{color:var(--dsw-alias-label-primary,#e6e9ef);font-size:14px;font-weight:500;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
35
+ ".dshBalanceValue{color:var(--dsw-alias-label-tertiary,#7b8494);font-size:12px;font-variant-numeric:tabular-nums;flex:none}",
36
+ ".dshBalanceEntry.compact .dshBalanceValue{font-size:12px;font-weight:500}",
37
+ ".dshBalanceValue[data-state=ok]{color:#3fb950}",
38
+ ".dshBalanceValue[data-state=err]{color:#f85149}",
39
+ ".dshBalanceValue[data-state=loading],.dshBalanceValue[data-state=muted]{color:var(--dsw-alias-label-tertiary,#7b8494)}",
40
+ ".dshBalanceStat{display:flex;flex-direction:column;align-items:flex-start;gap:1px;min-width:0;line-height:1.2}",
41
+ ".dshBalanceStatLabel{font-size:9px;color:#7b8494;white-space:nowrap;letter-spacing:.02em}",
42
+ ".dshBalanceStat .dshBalanceValue,.dshBalanceStat .dshBalanceToday,.dshBalanceStat .dshBalanceTodayTok{font-size:12px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap}",
43
+ ".dshBalanceToday[data-state=ok]{color:#e6e9ef}",
44
+ ".dshBalanceTodayTok[data-state=ok]{color:#9aa4b2}",
45
+ ".dshBalanceToday[data-state=err],.dshBalanceTodayTok[data-state=err]{color:#f85149}",
46
+ ".dshBalanceToday[data-state=loading],.dshBalanceTodayTok[data-state=loading]{color:var(--dsw-alias-label-tertiary,#7b8494)}",
47
+ ".dshBalanceToday[data-state=muted],.dshBalanceTodayTok[data-state=muted]{color:var(--dsw-alias-label-tertiary,#7b8494)}",
48
+ ".dshBalancePop{position:fixed;z-index:9999;width:280px;background:#1b1f27;border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.5);padding:14px 16px;color:#e6e9ef;font-size:13px;line-height:1.5}",
49
+ ".dshBalancePop h4{margin:0 0 10px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px}",
50
+ ".dshBalanceRow{display:flex;justify-content:space-between;gap:12px;padding:3px 0}",
51
+ ".dshBalanceRow span:first-child{color:#9aa4b2}",
52
+ ".dshBalanceRow span:last-child{font-variant-numeric:tabular-nums}",
53
+ ".dshBalanceFoot{display:flex;justify-content:space-between;align-items:center;margin-top:12px;padding-top:10px;border-top:1px solid rgba(255,255,255,.08);color:#7b8494;font-size:11px}",
54
+ ".dshBalanceBtn{appearance:none;cursor:pointer;background:#2d333b;color:#e6e9ef;border:1px solid rgba(255,255,255,.12);border-radius:7px;padding:4px 10px;font:inherit;font-size:12px}",
55
+ ".dshBalanceBtn:hover{background:#363d47}",
56
+ ".dshBalanceDot{width:7px;height:7px;border-radius:50%;flex:none}",
57
+ ".dshBalanceDot[data-state=ok]{background:#3fb950}",
58
+ ".dshBalanceDot[data-state=err]{background:#f85149}",
59
+ ".dshBalanceDot[data-state=loading],.dshBalanceDot[data-state=muted]{background:#d29922}",
60
+ ".dshBalanceUsage{display:block;margin-top:12px;padding-top:11px;border-top:1px solid rgba(255,255,255,.08)}",
61
+ ".dshBalanceUsageTitle{font-size:11px;color:#7b8494;letter-spacing:.04em;margin:0 0 2px;display:flex;align-items:baseline;gap:6px}",
62
+ ".dshBalanceUsageTitle b{color:#9aa4b2;font-weight:600;text-transform:uppercase}",
63
+ ".dshBalanceHint{font-size:12px;color:#7b8494;line-height:1.6;margin:2px 0 0}"
64
+ ].join("");
65
+ const tag = document.createElement("style");
66
+ tag.dataset.plugin = "dsh-deepseek-balance-widget";
67
+ tag.dataset.pluginCss = tagId;
68
+ tag.textContent = css;
69
+ document.head.appendChild(tag);
70
+ }
71
+
72
+ /** Find the sidebar shell root element, or undefined while not yet mounted. */
73
+ function sidebarRoot() {
74
+ const column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]');
75
+ if (column === null) return void 0;
76
+ return column.querySelector('[class*="logoRow"]')?.parentElement ?? column.firstElementChild;
77
+ }
78
+
79
+ /** Try to locate the bottom toolbar that holds Settings/Download/Call icons. */
80
+ function bottomToolbar() {
81
+ const column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]');
82
+ if (column === null) return void 0;
83
+ // Official dsh sidebar slots: the Settings trigger lives under [data-slot='sidebar.settings'].
84
+ const settingsSlot = column.querySelector("[data-slot='sidebar.settings']");
85
+ if (settingsSlot !== null) {
86
+ // Maid-atelier and similar skins mark the footer via [data-maid-sidebar-footer].
87
+ let row = settingsSlot.closest("[data-maid-sidebar-footer]");
88
+ if (row !== null) return row;
89
+ // Otherwise the immediate parent is the horizontal actions row.
90
+ row = settingsSlot.parentElement;
91
+ if (row !== null && row !== column) return row;
92
+ }
93
+ // Fallback: any bottom row containing a settings-ish button.
94
+ const settingsBtn = column.querySelector('button[aria-label="设置"], button[aria-label="Settings"], button[title="设置"], button[title="Settings"]');
95
+ if (settingsBtn !== null) {
96
+ const row = settingsBtn.closest('[class*="row"], [class*="bar"], [class*="toolbar"], [class*="actions"], [class*="bottom"]');
97
+ if (row !== null && row.parentElement === column) return row;
98
+ return settingsBtn.parentElement;
99
+ }
100
+ // Heuristic: last few children of the sidebar that look like a horizontal button row.
101
+ const candidates = Array.from(column.children).filter((el) => {
102
+ if (el.tagName !== "DIV") return false;
103
+ const buttons = el.querySelectorAll("button");
104
+ return buttons.length >= 2 && el.clientHeight < 80;
105
+ });
106
+ return candidates[candidates.length - 1];
107
+ }
108
+
109
+ /** The New Session button: nested in the logo row on current shells. */
110
+ function newSessionButton(root) {
111
+ const nested = root.querySelector("button[class*=\"newSession\"]");
112
+ if (nested !== null) return nested;
113
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
114
+ }
115
+
116
+ /** One labeled stat column (label above value). */
117
+ function statHtml(label, valueHtml) {
118
+ return '<span class="dshBalanceStat"><span class="dshBalanceStatLabel">' + label + "</span>" + valueHtml + "</span>";
119
+ }
120
+
121
+ /** Build the entry row (a detached button). */
122
+ function createEntry(variant) {
123
+ const entry = document.createElement("button");
124
+ entry.type = "button";
125
+ entry.dataset.dshBalanceEntry = "";
126
+ entry.className = "dshBalanceEntry" + (variant === "compact" ? " compact" : "");
127
+ entry.setAttribute("aria-label", "DeepSeek 余额(余额 / 今日消费 / 今日Token)");
128
+ entry.innerHTML =
129
+ statHtml("余额", '<span class="dshBalanceValue" data-state="loading">…</span>') +
130
+ statHtml("今日消费", '<span class="dshBalanceToday" data-state="loading">…</span>') +
131
+ statHtml("今日Token", '<span class="dshBalanceTodayTok" data-state="loading">…</span>');
132
+ return entry;
133
+ }
134
+
135
+ /** Re-insert the entry after the New Session row (sidebar mode). */
136
+ function placeSidebar(root, entry) {
137
+ const button = newSessionButton(root);
138
+ if (button === void 0) return false;
139
+ if (entry.parentElement !== root) {
140
+ const row = button.closest('[class*="logoRow"]');
141
+ const base = row !== null && row.parentElement === root ? row : button;
142
+ const anchor = base.nextElementSibling ?? null;
143
+ root.insertBefore(entry, anchor);
144
+ }
145
+ return true;
146
+ }
147
+
148
+ /** Insert the compact entry before the Settings slot/button (bottom toolbar mode). */
149
+ function placeToolbar(toolbar, entry) {
150
+ if (toolbar === void 0 || toolbar === null) return false;
151
+ if (entry.parentElement === toolbar) return true;
152
+ const settingsSlot = toolbar.querySelector("[data-slot='sidebar.settings']");
153
+ if (settingsSlot !== null) {
154
+ toolbar.insertBefore(entry, settingsSlot);
155
+ return true;
156
+ }
157
+ const settingsBtn = toolbar.querySelector('button[aria-label="设置"], button[aria-label="Settings"], button[title="设置"], button[title="Settings"]');
158
+ if (settingsBtn !== null && settingsBtn.parentElement === toolbar) {
159
+ toolbar.insertBefore(entry, settingsBtn);
160
+ } else {
161
+ toolbar.appendChild(entry);
162
+ }
163
+ return true;
164
+ }
165
+
166
+ /** Map a currency code to a display symbol. */
167
+ function sym(currency) {
168
+ if (currency === "USD") return "$";
169
+ if (currency === "CNY") return "¥";
170
+ return currency || "";
171
+ }
172
+
173
+ /** Fetch the latest balance envelope from the host route. */
174
+ async function fetchBalance() {
175
+ const res = await fetch(ENDPOINT, { method: "GET", headers: { Accept: "application/json" } });
176
+ return await res.json();
177
+ }
178
+
179
+ /** Fetch the latest usage envelope from the host route (never throws). */
180
+ async function fetchUsage() {
181
+ try {
182
+ const res = await fetch(USAGE_ENDPOINT, { method: "GET", headers: { Accept: "application/json" } });
183
+ return await res.json();
184
+ } catch (e) {
185
+ return { ok: false, error: String(e?.message ?? e) };
186
+ }
187
+ }
188
+
189
+ /** Compact number for the entry (1.2万 style). */
190
+ function fmtCompact(n) {
191
+ if (n === void 0 || n === null || n === "") return "—";
192
+ const v = Number(String(n).replace(/,/g, ""));
193
+ if (!Number.isFinite(v)) return String(n);
194
+ if (v >= 1e8) return (v / 1e8).toFixed(1) + "亿";
195
+ if (v >= 1e4) return (v / 1e4).toFixed(1) + "万";
196
+ return v.toLocaleString();
197
+ }
198
+
199
+ /** 宿主端返回的今日消费是纯数字(如 "0.00"),弹窗里补上 ¥ 展示。 */
200
+ function fmtYuan(s) {
201
+ const v = String(s ?? "");
202
+ if (/^[\d.]+$/.test(v)) return "¥" + v;
203
+ return v;
204
+ }
205
+
206
+ /**
207
+ * 用量来源解析:宿主端实时数据优先;拿不到时返回 null(绝不回退到
208
+ * 任何人的硬编码快照,保证每个人看到的都是自己的数据)。
209
+ * @returns {{src:object|null, state:string, error?:string}}
210
+ */
211
+ function usageStateOf(usageEnvelope) {
212
+ if (usageEnvelope && usageEnvelope.ok && usageEnvelope.data) {
213
+ return { src: usageEnvelope.data, state: "ok" };
214
+ }
215
+ if (usageEnvelope && usageEnvelope.ok === false && usageEnvelope.error) {
216
+ return { src: null, state: "err", error: usageEnvelope.error };
217
+ }
218
+ return { src: null, state: "muted" };
219
+ }
220
+
221
+ /** Render balance + today stats into the entry spans. */
222
+ function renderValue(envelope, usageEnvelope) {
223
+ const valueEl = entryRef && entryRef.querySelector(".dshBalanceValue");
224
+ const todayEl = entryRef && entryRef.querySelector(".dshBalanceToday");
225
+ const tokEl = entryRef && entryRef.querySelector(".dshBalanceTodayTok");
226
+
227
+ // 余额(入口处不显示货币符号)
228
+ if (valueEl !== null) {
229
+ if (!envelope || envelope.ok === false) {
230
+ valueEl.dataset.state = "err";
231
+ valueEl.textContent = "失败";
232
+ } else {
233
+ const info = (envelope.data && envelope.data.balance_infos && envelope.data.balance_infos[0]) || null;
234
+ if (info === null) {
235
+ valueEl.dataset.state = "ok";
236
+ valueEl.textContent = "—";
237
+ } else {
238
+ const total = info.total_balance;
239
+ valueEl.dataset.state = "ok";
240
+ valueEl.textContent = total !== void 0 ? total : "—";
241
+ }
242
+ }
243
+ }
244
+
245
+ // 今日消费 / 今日Token(实时数据优先;未配置/不可用时显示中性占位)
246
+ const usage = usageStateOf(usageEnvelope);
247
+ if (todayEl !== null) {
248
+ todayEl.dataset.state = usage.state === "ok" ? "ok" : usage.state;
249
+ todayEl.textContent = usage.src && usage.src.todaySpend ? String(usage.src.todaySpend).replace(/[¥$]/g, "").replace(" CNY", "") : "—";
250
+ }
251
+ if (tokEl !== null) {
252
+ tokEl.dataset.state = usage.state === "ok" ? "ok" : usage.state;
253
+ tokEl.textContent = usage.src ? fmtCompact(usage.src.todayTokens) : "—";
254
+ }
255
+ }
256
+
257
+ /** Update the detail popover if it is open. */
258
+ function renderPopover(envelope, usageEnvelope) {
259
+ if (popRef === null) return;
260
+ popRef.innerHTML = popoverHtml(envelope, usageEnvelope);
261
+ const btn = popRef.querySelector(".dshBalanceBtn");
262
+ if (btn !== null) btn.addEventListener("click", () => refresh(true));
263
+ }
264
+
265
+ /** Build the usage block: live platform data first, neutral hint otherwise. */
266
+ function usageHtmlOf(usageEnvelope) {
267
+ const usage = usageStateOf(usageEnvelope);
268
+ if (!usage.src) {
269
+ const hint = usage.state === "err"
270
+ ? "用量接口不可用(" + String(usage.error || "").slice(0, 60) + ")"
271
+ : "未配置 DEEPSEEK_PLATFORM_TOKEN,仅显示余额。登录 platform.deepseek.com 后把 userToken 写入 ~/.dsh/.credentials.yaml 即可显示实时用量(可选)。";
272
+ return '<div class="dshBalanceUsage">' +
273
+ '<div class="dshBalanceUsageTitle"><b>本月消耗</b><span>未启用</span></div>' +
274
+ '<div class="dshBalanceHint">' + hint + "</div>" +
275
+ "</div>";
276
+ }
277
+ const src = usage.src;
278
+ return '<div class="dshBalanceUsage">' +
279
+ '<div class="dshBalanceUsageTitle"><b>' + (src.period || "本月") + "消耗</b><span>" + (src.apiKey || "全部") + "</span></div>" +
280
+ '<div class="dshBalanceRow"><span>本月消费</span><span>' + String(src.totalSpend ?? "—") + "</span></div>" +
281
+ '<div class="dshBalanceRow"><span>API 请求次数</span><span>' + String(src.requestCount ?? "—") + "</span></div>" +
282
+ '<div class="dshBalanceRow"><span>Tokens</span><span>' + String(src.tokens ?? "—") + "</span></div>" +
283
+ "</div>";
284
+ }
285
+
286
+ /** Build the popover HTML from an envelope. */
287
+ function popoverHtml(envelope, usageEnvelope) {
288
+ const info = envelope && envelope.data && envelope.data.balance_infos && envelope.data.balance_infos[0];
289
+ // 宿主已把 currency 清空(入口不显示货币符号);弹窗里仍展示 ¥。
290
+ const cur = info ? (sym(info.currency) || "¥") : "¥";
291
+
292
+ let rows = "";
293
+ if (envelope && envelope.error && !info) {
294
+ rows += '<div class="dshBalanceRow"><span>错误</span><span>' + String(envelope.error).slice(0, 80) + "</span></div>";
295
+ } else if (!info) {
296
+ rows += '<div class="dshBalanceRow"><span>余额</span><span>—</span></div>';
297
+ } else {
298
+ const total = info.total_balance;
299
+ rows += '<div class="dshBalanceRow"><span>余额</span><span>' + cur + (total !== void 0 ? total : "—") + "</span></div>";
300
+ }
301
+ // 累计消费(全部时间)——放在余额下面,不属于「本月消耗」区块
302
+ const usage = usageStateOf(usageEnvelope);
303
+ const usageSrc = usage.src;
304
+ if (usageSrc && usageSrc.cumulativeSpend) {
305
+ rows += '<div class="dshBalanceRow"><span>累计消费</span><span>' + String(usageSrc.cumulativeSpend) + "</span></div>";
306
+ }
307
+ if (usageSrc && usageSrc.todaySpend) {
308
+ rows += '<div class="dshBalanceRow"><span>今日消费</span><span>' + fmtYuan(usageSrc.todaySpend) + "</span></div>";
309
+ }
310
+ if (usageSrc && usageSrc.todayTokens) {
311
+ rows += '<div class="dshBalanceRow"><span>今日Token</span><span>' + String(usageSrc.todayTokens) + "</span></div>";
312
+ }
313
+
314
+ const updated = lastUpdated ? lastUpdated.toLocaleTimeString() : "—";
315
+
316
+ // 用量区块:实时平台数据优先,中性提示兜底
317
+ const usageHtml = usageHtmlOf(usageEnvelope);
318
+
319
+ return "<h4>DeepSeek 余额</h4>" + rows + usageHtml +
320
+ '<div class="dshBalanceFoot"><span>更新于 ' + updated + "</span>" +
321
+ '<button class="dshBalanceBtn" type="button">刷新</button></div>';
322
+ }
323
+
324
+ /** Close the popover and detach the outside-click listener. */
325
+ function closePopover() {
326
+ if (popRef === null) return;
327
+ popRef.remove();
328
+ popRef = null;
329
+ if (outsideHandler !== null) {
330
+ document.removeEventListener("mousedown", outsideHandler);
331
+ outsideHandler = null;
332
+ }
333
+ }
334
+
335
+ /** Toggle the detail popover relative to the entry. */
336
+ function togglePopover() {
337
+ if (popRef !== null) {
338
+ closePopover();
339
+ return;
340
+ }
341
+ const pop = document.createElement("div");
342
+ pop.className = "dshBalancePop";
343
+ document.body.appendChild(pop);
344
+ popRef = pop;
345
+ renderPopover(lastEnvelope, lastUsage);
346
+ const rect = entryRef.getBoundingClientRect();
347
+ const popHeight = pop.offsetHeight || 220;
348
+ const spaceBelow = window.innerHeight - rect.bottom;
349
+ let top;
350
+ if (spaceBelow < popHeight + 12 && rect.top > popHeight + 12) {
351
+ top = rect.top - popHeight - 8;
352
+ } else {
353
+ top = rect.bottom + 8;
354
+ }
355
+ pop.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - 296)) + "px";
356
+ pop.style.top = top + "px";
357
+
358
+ outsideHandler = (event) => {
359
+ const target = event.target;
360
+ if (!(target instanceof Node)) return;
361
+ if (entryRef && entryRef.contains(target)) return;
362
+ if (pop.contains(target)) return;
363
+ closePopover();
364
+ };
365
+ document.addEventListener("mousedown", outsideHandler);
366
+ }
367
+
368
+ let entryRef = null;
369
+ let popRef = null;
370
+ let outsideHandler = null;
371
+ let lastEnvelope = null;
372
+ let lastUsage = null;
373
+ let lastUpdated = null;
374
+ let timer = null;
375
+
376
+ /** One refresh cycle: fetch balance + usage, update entry stats and popover. */
377
+ async function refresh(manual) {
378
+ const valueEl = entryRef && entryRef.querySelector(".dshBalanceValue");
379
+ if (valueEl && !manual) valueEl.dataset.state = "loading";
380
+ try {
381
+ const [envelope, usage] = await Promise.all([fetchBalance(), fetchUsage()]);
382
+ lastEnvelope = envelope;
383
+ lastUsage = usage;
384
+ lastUpdated = new Date();
385
+ renderValue(envelope, usage);
386
+ renderPopover(envelope, usage);
387
+ } catch (e) {
388
+ if (entryRef) {
389
+ const v = entryRef.querySelector(".dshBalanceValue");
390
+ if (v) { v.dataset.state = "err"; v.textContent = "失败"; }
391
+ }
392
+ lastEnvelope = { ok: false, error: String(e?.message ?? e) };
393
+ renderPopover(lastEnvelope, lastUsage);
394
+ }
395
+ }
396
+
397
+ /** Apply the client half: mount the widget and start polling. */
398
+ function apply(ctx) {
399
+ ensureStyle();
400
+ if (typeof document !== "undefined" && document.querySelector("[data-dsh-balance-entry]") !== null) return () => {};
401
+ const effect = ctx.effect(() => {
402
+ const entry = createEntry("compact");
403
+ entryRef = entry;
404
+ entry.addEventListener("click", togglePopover);
405
+
406
+ let root;
407
+ let mode = "toolbar";
408
+ let placed = false;
409
+
410
+ const tryPlace = () => {
411
+ if (root !== void 0 && !root.isConnected) { rootObserver.disconnect(); root = void 0; placed = false; }
412
+ if (placed) {
413
+ if (document.body.contains(entry)) return;
414
+ rootObserver.disconnect(); root = void 0; placed = false;
415
+ }
416
+ // Prefer bottom toolbar (Settings row).
417
+ root = bottomToolbar();
418
+ if (root !== void 0) {
419
+ mode = "toolbar";
420
+ if (!entry.classList.contains("compact")) entry.classList.add("compact");
421
+ placed = placeToolbar(root, entry);
422
+ }
423
+ // Fall back to sidebar under New Session.
424
+ if (!placed) {
425
+ root = sidebarRoot();
426
+ if (root !== void 0) {
427
+ mode = "sidebar";
428
+ entry.classList.remove("compact");
429
+ placed = placeSidebar(root, entry);
430
+ }
431
+ }
432
+ if (placed) rootObserver.observe(root, { childList: true, subtree: true });
433
+ };
434
+
435
+ const waitObserver = new MutationObserver(() => tryPlace());
436
+ waitObserver.observe(document.body, { childList: true, subtree: true });
437
+ const rootObserver = new MutationObserver(() => {
438
+ if (root === void 0 || !root.isConnected) { placed = false; tryPlace(); return; }
439
+ if (!root.contains(entry)) placed = mode === "toolbar" ? placeToolbar(root, entry) : placeSidebar(root, entry);
440
+ });
441
+ tryPlace();
442
+
443
+ refresh(false);
444
+ timer = setInterval(() => refresh(false), REFRESH_MS);
445
+
446
+ return () => {
447
+ waitObserver.disconnect();
448
+ rootObserver.disconnect();
449
+ if (timer !== null) clearInterval(timer);
450
+ if (outsideHandler !== null) {
451
+ document.removeEventListener("mousedown", outsideHandler);
452
+ outsideHandler = null;
453
+ }
454
+ if (popRef !== null) popRef.remove();
455
+ entry.remove();
456
+ entryRef = null;
457
+ popRef = null;
458
+ };
459
+ }, "dsh-deepseek-balance-widget: mount");
460
+ return effect;
461
+ }
462
+
463
+ exports.apply = apply;
464
+ exports.inject = [];
465
+ return module.exports;
466
+ }
467
+ });