dsh-remote-control 1.1.0 → 2.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/cordis.patch.yml CHANGED
@@ -19,9 +19,7 @@
19
19
  enabled: true
20
20
  idleReleaseSec: 300
21
21
  keepDisplay: false
22
- # 右栏自动弹码(原独立条目 dsh-remote-control-presentation,2026-10-03 折进来)。
23
- # imageFile 故意不写在这里:默认值取 homedir(见 src/shell/config.ts),
24
- # 写进 YAML 就把某个人的家目录焊进分发包了。
25
- sidebarQr:
22
+ # 状态栏那颗 pill:配对的**唯一**入口(`/drc pair` 与右栏自动弹码都在 2026-10-03 删了)。
23
+ # 关掉它这台主机就没有配对入口了——所以关掉会在 status.json 的 problems 里留一条 warn:pill。
24
+ pill:
26
25
  enabled: true
27
- refreshMs: 2000
@@ -31,112 +31,521 @@ __export(main_exports, {
31
31
  name: () => name
32
32
  });
33
33
  module.exports = __toCommonJS(main_exports);
34
- var ROUTE = "/plugins/dsh-remote-control/pairing";
34
+
35
+ // client/src/pill.ts
36
+ var STATUS_ROUTE = "/plugins/dsh-remote-control/status";
37
+ var NEW_ROUTE = "/plugins/dsh-remote-control/pairing/new";
38
+ var IMAGE_ROUTE = "/plugins/dsh-remote-control/pairing.png";
39
+ var UNPAIR_ROUTE = "/plugins/dsh-remote-control/unpair";
40
+ var SLOT_NAME = "conversation.composer.dock";
41
+ var PILL_ID = "dsh-remote-control";
42
+ var PILL_ORDER = 20;
43
+ var STYLE_ID = "dsh-remote-control/pill.css";
35
44
  var POLL_MS = 2e3;
36
- var timer;
37
- var stopped = true;
38
- var inFlight = false;
39
- var lastEpoch = null;
40
- var lastSessionId = null;
41
- var reported = /* @__PURE__ */ new Set();
42
- function report(message, error) {
43
- if (reported.has(message)) return;
44
- reported.add(message);
45
- try {
46
- const detail = error instanceof Error ? error.stack ?? error.message : error === void 0 ? "" : String(error);
47
- console.error(`[dsh-remote-control sidebar] ${message} ${detail}`);
48
- } catch {
45
+ function panelViewFor(answer, httpStatus) {
46
+ if (!answer || typeof answer !== "object") return { kind: "failed", detail: `HTTP ${httpStatus}` };
47
+ if (answer.state === "ready") {
48
+ const token = typeof answer.token === "string" ? answer.token : "";
49
+ const epoch = typeof answer.epoch === "string" ? answer.epoch : "";
50
+ const expiresInMs = typeof answer.expiresInMs === "number" ? answer.expiresInMs : 0;
51
+ if (token === "" || expiresInMs <= 0) return { kind: "failed", detail: "\u53D1\u7801\u56DE\u7B54\u91CC\u7684\u7801\u4E0D\u5B8C\u6574" };
52
+ return { kind: "qr", token, expiresInMs, imageSrc: `${IMAGE_ROUTE}?e=${encodeURIComponent(epoch)}` };
49
53
  }
54
+ if (answer.state === "unavailable") {
55
+ return {
56
+ kind: "unavailable",
57
+ reason: answer.reason === "relay-offline" ? "\u4E2D\u7EE7\u8FD8\u6CA1\u8FDE\u4E0A\uFF0C\u6682\u65F6\u65E0\u6CD5\u914D\u5BF9\u3002" : "\u73B0\u5728\u53D1\u4E0D\u51FA\u914D\u5BF9\u7801\u3002"
58
+ };
59
+ }
60
+ return {
61
+ kind: "failed",
62
+ // 把宿主说的那道守卫一起印出来:这条路由在浏览器里点,屏幕上的这句话就是唯一的现场。
63
+ detail: typeof answer.error === "string" ? typeof answer.guard === "string" ? `${answer.error}\uFF08${answer.guard}\uFF09` : answer.error : `HTTP ${httpStatus}`
64
+ };
65
+ }
66
+ function pillLabel(status) {
67
+ const relay = status?.relay;
68
+ if (relay === "idle") return { text: "\u8FDC\u7A0B\u672A\u542F\u52A8", tone: "off" };
69
+ if (relay === "offline") return { text: "\u5DF2\u65AD\u5F00\u8FDE\u63A5", tone: "error" };
70
+ if (relay === "connecting") return { text: "\u8FDE\u63A5\u4E2D", tone: "wait" };
71
+ if (relay !== "online") return { text: "\u8FDC\u7A0B\u63A7\u5236", tone: "off" };
72
+ const paired = typeof status?.paired === "number" ? status.paired : 0;
73
+ if (paired > 0) {
74
+ const waiting = waitingCount(status);
75
+ if (waiting > 0) return { text: `\u7B49 ${waiting} \u4EF6\u4E8B`, tone: "wait" };
76
+ return { text: "\u5DF2\u914D\u5BF9", tone: "on" };
77
+ }
78
+ return { text: "\u672A\u914D\u5BF9", tone: "off" };
79
+ }
80
+ function waitingCount(status) {
81
+ const count = status?.waiting;
82
+ return typeof count === "number" && count > 0 ? Math.min(Math.floor(count), 99) : 0;
83
+ }
84
+ function humanDuration(sec) {
85
+ const total = typeof sec === "number" && sec > 0 ? Math.floor(sec) : 0;
86
+ if (total < 60) return `${total} \u79D2`;
87
+ const minutes = Math.floor(total / 60);
88
+ return minutes >= 10 ? `${minutes} \u5206` : `${minutes} \u5206 ${total % 60} \u79D2`;
89
+ }
90
+ function waitingRow(status) {
91
+ const count = waitingCount(status);
92
+ if (count === 0) return "";
93
+ const oldest = typeof status?.waitingOldestSec === "number" ? status.waitingOldestSec : 0;
94
+ return `${count} \u4EF6${oldest > 0 ? ` \xB7 \u6700\u4E45 ${humanDuration(oldest)}` : ""}`.slice(0, 40);
50
95
  }
51
- function safeSessionId(held) {
52
- const id = held && typeof held === "object" ? held.sessionId : void 0;
53
- return typeof id === "string" && id !== "" ? id : null;
96
+ function relayAddress(url) {
97
+ if (typeof url !== "string" || url.trim() === "") return "";
98
+ const withoutScheme = url.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
99
+ const authority = /^[^/?]*/.exec(withoutScheme)?.[0] ?? "";
100
+ return authority.slice(0, 40);
101
+ }
102
+ function relayRow(status) {
103
+ return relayAddress(status?.serverUrl);
104
+ }
105
+ function relayStateRow(status) {
106
+ const relay = status?.relay;
107
+ if (relay === "online") return "\u5DF2\u8FDE\u63A5";
108
+ if (relay === "connecting") return "\u8FDE\u63A5\u4E2D";
109
+ if (relay === "offline") return "\u5DF2\u65AD\u5F00";
110
+ if (relay === "idle") return "\u672A\u542F\u52A8";
111
+ return "";
112
+ }
113
+ function versionRow(status) {
114
+ const version = status?.version;
115
+ return typeof version === "string" && version.trim() !== "" ? version.trim().slice(0, 24) : "";
116
+ }
117
+ function secondsText(ms) {
118
+ const whole = Math.max(0, Math.round(ms / 1e3));
119
+ if (whole < 60) return `${whole} \u79D2`;
120
+ return `${Math.floor(whole / 60)} \u5206 ${whole % 60} \u79D2`;
121
+ }
122
+ var CSS = `
123
+ .drc-pill { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 2px 8px;
124
+ border: 0; border-radius: 999px; background: transparent; color: inherit; font: inherit;
125
+ font-size: 12px; line-height: 18px; cursor: pointer;
126
+ /* \u5BBF\u4E3B\u90A3\u4E00\u6392\u6324\u4E0D\u4E0B\u65F6\u4F1A\u81EA\u5DF1\u622A\u65AD\uFF08\u300C6 \u8F6E \u2026\u300D\u90A3\u79CD\uFF09\uFF0C\u6211\u4EEC\u4E5F\u8DDF\u8FD9\u4E2A\u5F62\u72B6\u8D70\uFF1A
127
+ \u4E00\u884C\u5199\u5B8C\uFF0C\u5199\u4E0D\u4E0B\u5C31\u7701\u7565\u53F7\u3002**\u6CA1\u6709\u8FD9\u4E24\u6761**\u7684\u8BDD\uFF0Cflex \u9879\u4F1A\u88AB\u538B\u5230\u300C\u4E2D\u6587\u6700\u5C0F\u5185\u5BB9\u5BBD\u5EA6
128
+ = \u4E00\u4E2A\u5B57\u300D\uFF0C\u4E8E\u662F\u300C\u5DF2\u8FDE 1 \u53F0\u300D\u7AD6\u7740\u6392\u6210\u56DB\u884C\uFF082026-10-03 \u771F\u5C4F\u5E55\u622A\u56FE\uFF09\u3002 */
129
+ flex: 0 1 auto; white-space: nowrap; }
130
+ .drc-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
131
+ .drc-pill:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04)); }
132
+ .drc-dot { width: 6px; height: 6px; flex: 0 0 auto; border-radius: 50%; background: var(--dsw-alias-label-dimmed, #9aa0a6); }
133
+ .drc-dot[data-tone="wait"] { background: var(--dsw-alias-state-warn-primary, #faad14); }
134
+ .drc-dot[data-tone="on"] { background: var(--dsw-alias-state-success-primary, #52c41a); }
135
+ .drc-dot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #ff4d4f); }
136
+ .drc-panel { position: absolute; bottom: calc(100% + 8px); right: 0; z-index: 30; width: 240px;
137
+ padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1)); border-radius: 12px;
138
+ background: var(--dsw-specific-tip, var(--dsw-alias-bg-layer-1, #ffffff));
139
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.06);
140
+ color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; }
141
+ /* \u62AC\u5934\u4E00\u884C\uFF1A\u5DE6\u4FA7\u72B6\u6001\uFF0C\u53F3\u4FA7\u52A8\u4F5C\u3002\u4E24\u9897\u6309\u94AE\u90FD\u6536\u5728\u8FD9\u4E2A\u53F3\u4E0A\u89D2\uFF082026-10-03 \u91CD\u8BBE\u8BA1\u7684\u8981\u6C42\uFF09\u3002 */
142
+ .drc-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
143
+ .drc-head-label { font-weight: 600; }
144
+ .drc-head-label[data-tone="off"] { color: var(--dsw-alias-label-secondary, #6b7280); }
145
+ .drc-head-label[data-tone="wait"] { color: var(--dsw-alias-state-warn-primary, #faad14); }
146
+ .drc-head-label[data-tone="on"] { color: var(--dsw-alias-state-success-primary, #52c41a); }
147
+ .drc-head-label[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #ff4d4f); }
148
+ .drc-actions { display: flex; gap: 6px; flex: 0 0 auto; }
149
+ .drc-btn { padding: 2px 10px; border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
150
+ border-radius: 8px; background: transparent; color: inherit; font: inherit; font-size: 12px; cursor: pointer;
151
+ white-space: nowrap; }
152
+ .drc-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04)); }
153
+ /* \u6B63\u6587\u90A3\u4E09\u884C\uFF08\u4E2D\u7EE7 / \u72B6\u6001 / \u7248\u672C\uFF09\uFF1A\u952E\u5DE6\u503C\u53F3\uFF08space-between\uFF09\uFF0C\u503C\u88AB\u622A\u65AD\u65F6\u7701\u7565\u53F7\u2014\u2014
154
+ \u548C\u90A3\u9897 pill \u540C\u4E00\u5957"\u6324\u4E0D\u4E0B\u5C31\u622A\u6587\u5B57\u4E0D\u6491\u5BB9\u5668"\u3002\u7B2C\u4E00\u884C\u5E26\u90A3\u6761\u5206\u9694\u7EBF\uFF0C\u540E\u4E24\u884C\u53EA\u7559\u884C\u8DDD\u3002 */
155
+ .drc-info { display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
156
+ white-space: nowrap; margin-top: 8px; padding-top: 6px;
157
+ border-top: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08)); }
158
+ .drc-info + .drc-info { margin-top: 2px; padding-top: 0; border-top: 0; }
159
+ .drc-key { flex: 0 0 auto; color: var(--dsw-alias-label-tertiary); }
160
+ .drc-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
161
+ .drc-qr { display: block; width: 200px; height: 200px; margin: 8px auto 0; image-rendering: pixelated; }
162
+ .drc-code { margin: 8px 0 0; text-align: center; font-weight: 600; font-size: 14px; letter-spacing: 2px; }
163
+ .drc-note { margin: 4px 0 0; text-align: center; color: var(--dsw-alias-label-tertiary); }
164
+ .drc-note[data-kind="failed"] { color: var(--dsw-alias-state-error-primary, #ff4d4f); }
165
+ `;
166
+ function isSlots(value) {
167
+ if (!value || typeof value !== "object") return false;
168
+ const record = value;
169
+ return typeof record.inject === "function" && typeof record.register === "function";
170
+ }
171
+ function pickSlots(source) {
172
+ if (isSlots(source)) return source;
173
+ const asGettable = source;
174
+ if (asGettable && typeof asGettable.get === "function") {
175
+ try {
176
+ const got = asGettable.get("slots", true);
177
+ if (isSlots(got)) return got;
178
+ } catch {
179
+ }
180
+ }
181
+ return void 0;
54
182
  }
55
- async function poll(ctx) {
56
- if (stopped || inFlight) return;
183
+ function findCreateElement() {
57
184
  try {
58
- if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
185
+ if (typeof require !== "function") return void 0;
186
+ const moduleId = "react";
187
+ const react = require(moduleId);
188
+ const create = react?.createElement ?? react?.default?.createElement;
189
+ return typeof create === "function" ? create : void 0;
59
190
  } catch {
191
+ return void 0;
60
192
  }
61
- let sessionId;
193
+ }
194
+ function warn(message, error) {
62
195
  try {
63
- const held = ctx.sidebarRight.require();
64
- const id = safeSessionId(held);
65
- if (id === null) return;
66
- sessionId = id;
196
+ const detail = error instanceof Error ? error.message : error === void 0 ? "" : String(error);
197
+ console.warn(`[dsh-remote-control pill] ${message} ${detail}`);
67
198
  } catch {
68
- return;
69
199
  }
70
- if (sessionId !== lastSessionId) {
71
- lastSessionId = sessionId;
72
- lastEpoch = null;
73
- }
74
- inFlight = true;
200
+ }
201
+ function ensureStyle(doc) {
75
202
  try {
76
- const response = await fetch(`${ROUTE}?session=${encodeURIComponent(sessionId)}`, {
77
- headers: { accept: "application/json" },
78
- credentials: "same-origin"
203
+ const existing = doc.querySelector(`style[data-plugin-css="${STYLE_ID}"]`);
204
+ if (existing) {
205
+ if (existing.textContent !== CSS) existing.textContent = CSS;
206
+ return;
207
+ }
208
+ const element = doc.createElement("style");
209
+ element.setAttribute("data-plugin-css", STYLE_ID);
210
+ element.textContent = CSS;
211
+ doc.head.appendChild(element);
212
+ } catch (error) {
213
+ warn("\u6837\u5F0F\u6CE8\u5165\u5931\u8D25\uFF08pill \u4ECD\u53EF\u7528\uFF0C\u53EA\u662F\u6CA1\u914D\u8272\uFF09", error);
214
+ }
215
+ }
216
+ function buildPill(root, deps) {
217
+ const doc = root.ownerDocument;
218
+ ensureStyle(doc);
219
+ const button = doc.createElement("button");
220
+ button.type = "button";
221
+ button.className = "drc-pill";
222
+ button.setAttribute("aria-haspopup", "dialog");
223
+ button.setAttribute("aria-expanded", "false");
224
+ const dot = doc.createElement("span");
225
+ dot.className = "drc-dot";
226
+ dot.setAttribute("aria-hidden", "true");
227
+ const label = doc.createElement("span");
228
+ label.className = "drc-label";
229
+ button.appendChild(dot);
230
+ button.appendChild(label);
231
+ root.appendChild(button);
232
+ let panel;
233
+ let statusTimer;
234
+ let countTimer;
235
+ let view = { kind: "info" };
236
+ let expiresInMs = 0;
237
+ let lastStatus;
238
+ let disposed = false;
239
+ const pairedNow = () => typeof lastStatus?.paired === "number" ? lastStatus.paired : 0;
240
+ const write = () => {
241
+ const next = pillLabel(lastStatus);
242
+ label.textContent = next.text;
243
+ dot.setAttribute("data-tone", next.tone);
244
+ button.setAttribute("title", `dsh-remote-control\uFF1A${next.text}`);
245
+ button.setAttribute("aria-label", next.text);
246
+ };
247
+ const text = (tag, className, content) => {
248
+ const node = doc.createElement(tag);
249
+ node.className = className;
250
+ node.textContent = content;
251
+ return node;
252
+ };
253
+ const buttonOf = (content, onClick) => {
254
+ const node = doc.createElement("button");
255
+ node.type = "button";
256
+ node.className = "drc-btn";
257
+ node.textContent = content;
258
+ node.addEventListener("click", onClick);
259
+ return node;
260
+ };
261
+ const qrNote = () => expiresInMs > 0 ? `\u626B\u7801\u914D\u5BF9 \xB7 ${secondsText(expiresInMs)}\u540E\u8FC7\u671F` : "\u914D\u5BF9\u7801\u5DF2\u8FC7\u671F\uFF0C\u70B9\u53F3\u4E0A\u89D2\u300C\u5237\u65B0\u300D\u91CD\u65B0\u751F\u6210";
262
+ const header = () => {
263
+ const head = doc.createElement("div");
264
+ head.className = "drc-head";
265
+ const state = pillLabel(lastStatus);
266
+ const stateText = text("span", "drc-head-label", state.text);
267
+ stateText.setAttribute("data-tone", state.tone);
268
+ head.appendChild(stateText);
269
+ const actions = doc.createElement("div");
270
+ actions.className = "drc-actions";
271
+ const action = pairedNow() > 0 ? { label: "\u9000\u51FA\u914D\u5BF9", run: requestUnpair } : { label: "\u5237\u65B0", run: requestPairing };
272
+ actions.appendChild(buttonOf(action.label, () => void action.run()));
273
+ head.appendChild(actions);
274
+ return head;
275
+ };
276
+ const infoRows = () => {
277
+ if (!lastStatus) return [];
278
+ const rows = [
279
+ // 排第一:它是唯一一条"看完要做点什么"的,其余三条都是"知道就行"。
280
+ ["\u5F85\u5904\u7406", waitingRow(lastStatus)],
281
+ ["\u4E2D\u7EE7", relayRow(lastStatus)],
282
+ ["\u72B6\u6001", relayStateRow(lastStatus)],
283
+ ["\u7248\u672C", versionRow(lastStatus)]
284
+ ];
285
+ return rows.filter(([, value]) => value !== "").map(([key, value]) => {
286
+ const row = doc.createElement("div");
287
+ row.className = "drc-info";
288
+ row.appendChild(text("span", "drc-key", key));
289
+ row.appendChild(text("span", "drc-value", value));
290
+ return row;
79
291
  });
80
- if (!response.ok) {
81
- if (response.status === 403) report("route refused the request (403): \u73AF\u56DE\u5B88\u536B\u5224\u5B9A\u975E\u540C\u6E90");
82
- else if (response.status !== 404) report(`route answered ${response.status}`);
292
+ };
293
+ const paint = () => {
294
+ if (!panel) return;
295
+ panel.replaceChildren();
296
+ panel.appendChild(header());
297
+ if (view.kind === "loading") {
298
+ panel.appendChild(text("p", "drc-note", "\u6B63\u5728\u751F\u6210\u914D\u5BF9\u7801\u2026"));
299
+ } else if (view.kind === "qr") {
300
+ const image = doc.createElement("img");
301
+ image.className = "drc-qr";
302
+ image.setAttribute("alt", "\u914D\u5BF9\u4E8C\u7EF4\u7801");
303
+ image.src = view.imageSrc;
304
+ panel.appendChild(image);
305
+ panel.appendChild(text("p", "drc-code", view.token));
306
+ panel.appendChild(text("p", "drc-note", qrNote()));
307
+ } else if (view.kind === "unavailable" || view.kind === "failed") {
308
+ const note = view.kind === "unavailable" ? text("p", "drc-note", view.reason) : (() => {
309
+ const node = text("p", "drc-note", `\u914D\u5BF9\u8BF7\u6C42\u6CA1\u6210\u529F\uFF1A${view.detail}`);
310
+ node.setAttribute("data-kind", "failed");
311
+ return node;
312
+ })();
313
+ panel.appendChild(note);
314
+ }
315
+ for (const row of infoRows()) panel.appendChild(row);
316
+ };
317
+ const closePanel = () => {
318
+ if (countTimer !== void 0) {
319
+ clearInterval(countTimer);
320
+ countTimer = void 0;
321
+ }
322
+ if (panel) {
323
+ panel.remove();
324
+ panel = void 0;
325
+ }
326
+ button.setAttribute("aria-expanded", "false");
327
+ };
328
+ async function requestPairing() {
329
+ if (disposed || !panel) return;
330
+ view = { kind: "loading" };
331
+ paint();
332
+ let answer;
333
+ let httpStatus = 0;
334
+ try {
335
+ const response = await deps.fetchImpl(NEW_ROUTE, {
336
+ method: "POST",
337
+ headers: { "x-drc-pair": "1" },
338
+ credentials: "same-origin"
339
+ });
340
+ httpStatus = typeof response?.status === "number" ? response.status : 0;
341
+ answer = await response?.json?.();
342
+ } catch (error) {
343
+ view = { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
344
+ paint();
83
345
  return;
84
346
  }
85
- const body = await response.json();
86
- if (body.state !== "ready") return;
87
- const epoch = typeof body.epoch === "string" ? body.epoch : "";
88
- const address = typeof body.address === "string" ? body.address : "";
89
- const expiresAt = typeof body.expiresAt === "number" ? body.expiresAt : 0;
90
- if (epoch === "" || address === "" || epoch === lastEpoch) return;
91
- if (expiresAt <= Date.now()) return;
347
+ if (disposed || !panel) return;
348
+ view = panelViewFor(answer, httpStatus);
349
+ if (view.kind === "qr") {
350
+ expiresInMs = view.expiresInMs;
351
+ if (countTimer === void 0) {
352
+ countTimer = setInterval(() => {
353
+ expiresInMs -= 1e3;
354
+ if (expiresInMs <= 0) {
355
+ clearInterval(countTimer);
356
+ countTimer = void 0;
357
+ }
358
+ paint();
359
+ }, 1e3);
360
+ }
361
+ }
362
+ paint();
363
+ }
364
+ async function requestUnpair() {
365
+ if (disposed || !panel) return;
366
+ try {
367
+ await deps.fetchImpl(UNPAIR_ROUTE, {
368
+ method: "POST",
369
+ headers: { "x-drc-pair": "1" },
370
+ credentials: "same-origin"
371
+ });
372
+ } catch (error) {
373
+ warn("\u9000\u51FA\u914D\u5BF9\u8BF7\u6C42\u5931\u8D25\uFF08\u9762\u677F\u4F1A\u9760\u4E0B\u4E00\u6B21\u8F6E\u8BE2\u5BF9\u8D26\uFF09", error);
374
+ }
375
+ if (disposed || !panel) return;
376
+ if (lastStatus) lastStatus = { ...lastStatus, paired: 0 };
377
+ write();
378
+ view = { kind: "loading" };
379
+ paint();
380
+ void requestPairing();
381
+ }
382
+ const openPanel = () => {
383
+ if (panel || disposed) return;
384
+ panel = doc.createElement("div");
385
+ panel.className = "drc-panel";
386
+ panel.setAttribute("role", "dialog");
387
+ panel.setAttribute("aria-label", "dsh-remote-control \u914D\u5BF9");
388
+ panel.tabIndex = -1;
389
+ root.appendChild(panel);
390
+ button.setAttribute("aria-expanded", "true");
391
+ if (pairedNow() > 0) {
392
+ view = { kind: "info" };
393
+ paint();
394
+ } else {
395
+ void requestPairing();
396
+ }
92
397
  try {
93
- ctx.sidebarRight.openResource(address);
94
- lastEpoch = epoch;
398
+ panel.focus();
399
+ } catch {
400
+ }
401
+ };
402
+ const inside = (candidate) => {
403
+ if (!candidate || typeof candidate !== "object") return false;
404
+ try {
405
+ return root.contains(candidate) || (panel?.contains(candidate) ?? false);
406
+ } catch {
407
+ return false;
408
+ }
409
+ };
410
+ const onPointerDown = (event) => {
411
+ if (!panel) return;
412
+ if (inside(event?.target)) return;
413
+ closePanel();
414
+ };
415
+ const onKeyDown = (event) => {
416
+ if (event.key === "Escape") closePanel();
417
+ };
418
+ async function pollStatus() {
419
+ if (disposed) return;
420
+ try {
421
+ if (doc.visibilityState === "hidden") return;
422
+ } catch {
423
+ }
424
+ try {
425
+ const response = await deps.fetchImpl(`${STATUS_ROUTE}?t=${Date.now()}`, {
426
+ headers: { accept: "application/json" },
427
+ credentials: "same-origin"
428
+ });
429
+ if (!response || response.status !== 200) return;
430
+ const body = await response.json?.();
431
+ if (!body || typeof body !== "object") return;
432
+ lastStatus = body;
433
+ if (view.kind === "qr" && pairedNow() > 0) {
434
+ view = { kind: "info" };
435
+ if (countTimer !== void 0) {
436
+ clearInterval(countTimer);
437
+ countTimer = void 0;
438
+ }
439
+ if (panel) paint();
440
+ }
441
+ write();
95
442
  } catch (error) {
96
- report("openResource failed", error);
443
+ deps.warn?.("status poll failed");
444
+ warn("status poll failed\uFF08pill \u4F1A\u505C\u5728\u9ED8\u8BA4\u6587\u6848\u4E0A\uFF09", error);
97
445
  }
98
- } catch (error) {
99
- report("poll failed", error);
100
- } finally {
101
- inFlight = false;
102
446
  }
447
+ button.addEventListener("click", () => {
448
+ if (panel) closePanel();
449
+ else openPanel();
450
+ });
451
+ doc.addEventListener("pointerdown", onPointerDown, true);
452
+ doc.addEventListener("keydown", onKeyDown);
453
+ write();
454
+ void pollStatus();
455
+ statusTimer = setInterval(() => void pollStatus(), POLL_MS);
456
+ return () => {
457
+ if (disposed) return;
458
+ disposed = true;
459
+ if (statusTimer !== void 0) clearInterval(statusTimer);
460
+ closePanel();
461
+ doc.removeEventListener("pointerdown", onPointerDown, true);
462
+ doc.removeEventListener("keydown", onKeyDown);
463
+ button.remove();
464
+ };
103
465
  }
104
- function stop() {
105
- stopped = true;
106
- if (timer !== void 0) {
107
- clearInterval(timer);
108
- timer = void 0;
466
+ function mountPill(ctx, createElement) {
467
+ if (!createElement) {
468
+ warn("\u88C5\u8F7D\u5668\u7ED9\u4E0D\u51FA react\uFF1A\u914D\u5BF9\u90A3\u9897 pill \u4E0D\u4F1A\u51FA\u73B0");
469
+ return false;
109
470
  }
471
+ let mounted = false;
472
+ const mount = (slots) => {
473
+ if (mounted) return;
474
+ let cleanup;
475
+ const attach = (node) => {
476
+ const element = node;
477
+ if (element && typeof element.appendChild === "function" && element.ownerDocument) {
478
+ if (cleanup) return;
479
+ element.style.position = "relative";
480
+ cleanup = buildPill(element, {
481
+ fetchImpl: (input, init) => globalThis.fetch(input, init)
482
+ });
483
+ } else if (cleanup) {
484
+ cleanup();
485
+ cleanup = void 0;
486
+ }
487
+ };
488
+ const component = () => createElement("span", { ref: attach });
489
+ mounted = true;
490
+ try {
491
+ slots.inject(SLOT_NAME, () => {
492
+ try {
493
+ slots.register({ name: SLOT_NAME, id: PILL_ID, order: PILL_ORDER, inject: () => ({}) }, component);
494
+ } catch (error) {
495
+ warn("\u69FD\u4F4D\u6CE8\u518C\u5931\u8D25\uFF08\u90A3\u9897 pill \u4E0D\u51FA\u73B0\uFF09", error);
496
+ }
497
+ });
498
+ } catch (error) {
499
+ mounted = false;
500
+ warn("\u69FD\u4F4D\u6CE8\u5165\u5931\u8D25\uFF08\u90A3\u9897 pill \u4E0D\u51FA\u73B0\uFF09", error);
501
+ return;
502
+ }
503
+ try {
504
+ ctx.effect?.(() => () => {
505
+ if (cleanup) {
506
+ cleanup();
507
+ cleanup = void 0;
508
+ }
509
+ });
510
+ } catch {
511
+ }
512
+ };
513
+ let early;
110
514
  try {
111
- if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibility);
515
+ early = pickSlots(ctx.slots) ?? pickSlots(ctx.get?.("slots", true));
112
516
  } catch {
113
517
  }
518
+ if (early) mount(early);
519
+ try {
520
+ ctx.inject?.(["slots"], ((...args) => {
521
+ for (const candidate of [...args, ctx]) {
522
+ const found = pickSlots(candidate);
523
+ if (found) {
524
+ mount(found);
525
+ return;
526
+ }
527
+ }
528
+ }));
529
+ } catch (error) {
530
+ warn("ctx.inject \u53E3\u4E0D\u53EF\u7528\uFF1Aslots \u518D\u665A\u5230\u4E5F\u6CA1\u4EBA\u8865\u6302", error);
531
+ }
532
+ if (!mounted) {
533
+ warn("apply \u65F6\u6CA1\u63A2\u5230 slots\uFF08\u5DF2\u6302\u6CE8\u5165\u56DE\u8C03\u7B49\u5B83\u665A\u5230\uFF09\uFF1Bpill \u4E00\u76F4\u4E0D\u51FA\u73B0\u5C31\u662F\u8FD9\u4EE3\u5BBF\u4E3B\u6CA1\u8FD9\u4E2A\u670D\u52A1");
534
+ }
535
+ return mounted;
114
536
  }
115
- var current;
116
- function onVisibility() {
117
- if (current && !stopped) void poll(current);
118
- }
537
+
538
+ // client/src/main.ts
119
539
  var name = "dsh-remote-control";
120
- var inject = ["sidebarRight"];
540
+ var inject = ["slots"];
121
541
  function apply(ctx) {
122
542
  try {
123
- current = ctx;
124
- stopped = false;
125
- reported.clear();
543
+ mountPill(ctx, findCreateElement());
544
+ } catch (error) {
126
545
  try {
127
- if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibility);
546
+ console.error("[dsh-remote-control] pill \u6302\u8F7D\u629B\u9519\uFF08\u5DF2\u541E\u6389\uFF09\uFF1A", error);
128
547
  } catch {
129
548
  }
130
- timer = setInterval(() => void poll(ctx), POLL_MS);
131
- void poll(ctx);
132
- try {
133
- ctx.effect?.(() => stop);
134
- } catch (error) {
135
- report("ctx.effect unavailable; poll timer will outlive this fiber", error);
136
- }
137
- } catch (error) {
138
- report("apply failed", error);
139
- stop();
140
549
  }
141
550
  }
142
551