dsh-update-status 0.1.0 → 0.1.3

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
@@ -35,26 +35,20 @@ window.__ModuleLoader__.load({
35
35
  *
36
36
  * The static package uses the authenticated Connection RPC channel because
37
37
  * `harness.handle` / `host.call` are dynamic-Cordis-only closure APIs in DSH
38
- * 0.1.2-rc.1. The endpoint vocabulary remains deliberately small and private
38
+ * 0.1.5-rc.1. The endpoint vocabulary remains deliberately small and private
39
39
  * to this plugin channel.
40
40
  */
41
41
  const PLUGIN_ID$1 = "dsh-update-status";
42
42
  const PACKAGE_NAME = "@deepseek-ai/dsh";
43
- const UPDATE_STATUS_CHANNEL = "/dsh-update-status";
44
- /** Exact DSH release this Phase-1 bundle declares compatible in package.json. */
45
- const STATIC_COMPATIBLE_VERSION = "0.1.2-rc.1";
46
- const RELEASE_CHANNELS = [
47
- "latest",
48
- "next",
49
- "alpha"
50
- ];
43
+ /** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */
44
+ const UPDATE_STATUS_CHANNEL = "/api";
51
45
  const MAX_CACHE_TTL_MINUTES = 1440;
52
46
  function isCacheTtlMinutes(value) {
53
47
  return typeof value === "number" && Number.isInteger(value) && value >= 30 && value <= 1440;
54
48
  }
55
49
  const UPDATE_ENDPOINTS = {
56
- getStatus: "get-status",
57
- checkUpdate: "check-update"
50
+ getStatus: "dsh-update-status.get-status",
51
+ checkUpdate: "dsh-update-status.check-update"
58
52
  };
59
53
  function isReleaseChannel(value) {
60
54
  return value === "latest" || value === "next" || value === "alpha";
@@ -432,13 +426,13 @@ window.__ModuleLoader__.load({
432
426
  const snapshot = useObservable(ui.status);
433
427
  if (!preferences.sidebarEnabled) return null;
434
428
  const state = visualState(snapshot.status, snapshot.loading, snapshot.error);
435
- const version = snapshot.status?.currentVersion ?? "…";
429
+ const version = snapshot.status?.currentVersion ?? "—";
436
430
  const activate = (event) => {
437
431
  event.preventDefault();
438
432
  event.stopPropagation();
439
433
  const target = event.currentTarget;
440
434
  if (target instanceof HTMLElement) target.blur();
441
- if (ui.canManage) ui.panel.toggle("brand");
435
+ ui.panel.toggle("brand");
442
436
  };
443
437
  const onKeyDown = (event) => {
444
438
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -453,13 +447,12 @@ window.__ModuleLoader__.load({
453
447
  className: "dus-badge",
454
448
  "data-update": snapshot.status?.hasUpdate === true || void 0,
455
449
  "data-error": state === "problem" || void 0,
456
- role: ui.canManage ? "button" : void 0,
457
- tabIndex: ui.canManage ? 0 : void 0,
450
+ role: "button",
451
+ tabIndex: 0,
458
452
  "aria-label": badgeLabel(snapshot.status, snapshot.loading, snapshot.error),
459
- "aria-disabled": ui.canManage ? void 0 : true,
460
453
  title: badgeLabel(snapshot.status, snapshot.loading, snapshot.error),
461
- onClick: ui.canManage ? activate : void 0,
462
- onKeyDown: ui.canManage ? onKeyDown : void 0,
454
+ onClick: activate,
455
+ onKeyDown,
463
456
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
464
457
  className: "dus-badge-version",
465
458
  children: shortVersion(version)
@@ -484,7 +477,6 @@ window.__ModuleLoader__.load({
484
477
  type: "button",
485
478
  "aria-label": t("footer.status"),
486
479
  title: label,
487
- disabled: !ui.canManage,
488
480
  onClick: () => {
489
481
  ui.panel.toggle("rail");
490
482
  },
@@ -533,7 +525,7 @@ window.__ModuleLoader__.load({
533
525
  element.focus();
534
526
  } catch {}
535
527
  }
536
- /** Full detail is only opened from a loopback/local DSH UI surface. */
528
+ /** Full detail from whichever page the operator is on (the transport is authenticated). */
537
529
  function UpdatePanel({ ui }) {
538
530
  const panel = useObservable(ui.panel);
539
531
  const preferences = useObservable(ui.preferences);
@@ -541,7 +533,7 @@ window.__ModuleLoader__.load({
541
533
  const commandRef = React.useRef(null);
542
534
  const dialogRef = React.useRef(null);
543
535
  const [copyMessage, setCopyMessage] = React.useState(null);
544
- const visible = panel.open && preferences.sidebarEnabled && ui.canManage;
536
+ const visible = panel.open && preferences.sidebarEnabled;
545
537
  React.useLayoutEffect(() => {
546
538
  if (!visible) return void 0;
547
539
  const dialog = dialogRef.current;
@@ -860,7 +852,7 @@ window.__ModuleLoader__.load({
860
852
  className: "dus-settings-hint",
861
853
  children: t("preview.follow")
862
854
  }),
863
- ui.canManage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
864
856
  className: "dus-action",
865
857
  type: "button",
866
858
  "aria-expanded": showGuidance,
@@ -869,7 +861,7 @@ window.__ModuleLoader__.load({
869
861
  },
870
862
  children: t(showGuidance ? "preview.close" : "preview.open")
871
863
  }),
872
- ui.canManage && showGuidance && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
864
+ showGuidance && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
873
865
  "aria-label": t("preview.open"),
874
866
  children: [
875
867
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -985,40 +977,12 @@ window.__ModuleLoader__.load({
985
977
  };
986
978
  var StatusStore = class {
987
979
  connection;
988
- snapshot;
989
- allowRequests;
980
+ snapshot = INITIAL_STATUS;
990
981
  listeners = /* @__PURE__ */ new Set();
991
982
  inFlight;
992
983
  stopped = false;
993
- constructor(connection, staticVersion = null) {
984
+ constructor(connection) {
994
985
  this.connection = connection;
995
- this.allowRequests = staticVersion === null;
996
- this.snapshot = staticVersion === null ? INITIAL_STATUS : {
997
- status: {
998
- currentVersion: staticVersion,
999
- latestVersion: null,
1000
- hasUpdate: false,
1001
- cached: true,
1002
- checkedAt: null,
1003
- warning: null,
1004
- installKind: "unknown",
1005
- upgradeCommand: "",
1006
- releaseUrl: "",
1007
- changelogUrl: "",
1008
- publishedAt: null,
1009
- packageName: "@deepseek-ai/dsh",
1010
- channel: "latest",
1011
- channels: RELEASE_CHANNELS.map((channel) => ({
1012
- channel,
1013
- version: null,
1014
- publishedAt: null,
1015
- compatibility: "unverified"
1016
- })),
1017
- canApplyInPlace: false
1018
- },
1019
- loading: false,
1020
- error: null
1021
- };
1022
986
  }
1023
987
  getSnapshot = () => this.snapshot;
1024
988
  subscribe = (listener) => {
@@ -1029,15 +993,14 @@ window.__ModuleLoader__.load({
1029
993
  };
1030
994
  /** First mount reads the Host's cached status; it never starts a browser timer. */
1031
995
  async load(cacheTtlMinutes = 360) {
1032
- if (this.allowRequests) await this.request(false, this.snapshot.status?.channel ?? "latest", cacheTtlMinutes);
996
+ await this.request(false, this.snapshot.status?.channel ?? "latest", cacheTtlMinutes);
1033
997
  }
1034
998
  /** User gesture only: force the Host to bypass TTL (while retaining single-flight). */
1035
999
  async refresh(channel = this.snapshot.status?.channel ?? "latest", cacheTtlMinutes = 360) {
1036
- if (this.allowRequests) await this.request(true, channel, cacheTtlMinutes);
1000
+ await this.request(true, channel, cacheTtlMinutes);
1037
1001
  }
1038
1002
  /** Channel selection re-projects the Host cache; it is not a forced refresh. */
1039
1003
  async selectChannel(channel, cacheTtlMinutes = 360) {
1040
- if (!this.allowRequests) return;
1041
1004
  for (let attempts = 0; attempts < 3 && !this.stopped && this.snapshot.status?.channel !== channel; attempts += 1) {
1042
1005
  const pending = this.inFlight;
1043
1006
  if (pending !== void 0 && !await pending) return;
@@ -1236,18 +1199,367 @@ window.__ModuleLoader__.load({
1236
1199
  /** The one namespace name used by Host registration and browser binding. */
1237
1200
  const SETTINGS_NAMESPACE = PLUGIN_ID$1;
1238
1201
  //#endregion
1202
+ //#region src/client/settings/scopeFaces.ts
1203
+ /** Guard: does an object look like a binder with bind()? */
1204
+ function binderOf(raw) {
1205
+ if (raw === null || typeof raw !== "object") return void 0;
1206
+ const binder = raw.settingsScope;
1207
+ if (binder === null || typeof binder !== "object") return void 0;
1208
+ if (typeof binder.bind !== "function") return void 0;
1209
+ return binder;
1210
+ }
1211
+ /** Guard: does an object look like a bound scope? */
1212
+ function scopeOf(value) {
1213
+ if (value === null || typeof value !== "object") return void 0;
1214
+ const scope = value;
1215
+ if (typeof scope.getSnapshot !== "function") return void 0;
1216
+ if (typeof scope.subscribe !== "function") return void 0;
1217
+ if (typeof scope.set !== "function") return void 0;
1218
+ return scope;
1219
+ }
1220
+ //#endregion
1221
+ //#region src/client/settings/hostDirectScope.ts
1222
+ /** A refused namespace write; `code` keeps the Host's own diagnosis. */
1223
+ var SettingsWriteFailure = class extends Error {
1224
+ code;
1225
+ constructor(code, message) {
1226
+ super(message === void 0 || message === "" ? code : message);
1227
+ this.name = "SettingsWriteFailure";
1228
+ this.code = code;
1229
+ }
1230
+ };
1231
+ function isRecord(value) {
1232
+ return typeof value === "object" && value !== null;
1233
+ }
1234
+ /**
1235
+ * Guard: does this value look like the `remote.settings` namespace face?
1236
+ * Applied to a bare service object (`ctx.get('remote.settings')` or a payload
1237
+ * member), and it never calls anything.
1238
+ */
1239
+ function settingsRemoteFace(value) {
1240
+ if (!isRecord(value)) return void 0;
1241
+ if (typeof value.describe !== "function" || typeof value.mutate !== "function") return void 0;
1242
+ return value;
1243
+ }
1244
+ /**
1245
+ * Guard: read the `remote.settings` face off an injection payload.
1246
+ *
1247
+ * The payload a `ctx.inject(['remote.settings'], …)` callback receives exposes
1248
+ * the LITERAL service key. Reading the dotted PARENT off that injected context
1249
+ * (`payload.remote`) throws `cannot get property "remote" without inject` — a
1250
+ * throw that used to abort a whole wiring callback on a LAN page. So the literal
1251
+ * key is read FIRST and every read is contained; the nested
1252
+ * (`payload.remote.settings`) shape is only a fallback for a plain object
1253
+ * payload. Both absence and refusal answer `undefined`.
1254
+ */
1255
+ function settingsRemoteOf(raw) {
1256
+ if (!isRecord(raw)) return void 0;
1257
+ try {
1258
+ const direct = settingsRemoteFace(raw["remote.settings"]);
1259
+ if (direct !== void 0) return direct;
1260
+ } catch {}
1261
+ try {
1262
+ const remote = raw.remote;
1263
+ if (isRecord(remote)) return settingsRemoteFace(remote.settings);
1264
+ } catch {}
1265
+ }
1266
+ /**
1267
+ * Guard: subscribe to the Host's settings-document invalidation on the Remote
1268
+ * service object (`remote.$on`). Returns undefined when the service exposes no
1269
+ * event seam — the preferences then refresh on reconnect only.
1270
+ */
1271
+ function settingsInvalidationsOf(service) {
1272
+ if (!isRecord(service)) return void 0;
1273
+ const on = service.$on;
1274
+ if (typeof on !== "function") return void 0;
1275
+ return (listener) => {
1276
+ const dispose = on.call(service, "settings/document-updated", listener);
1277
+ return typeof dispose === "function" ? dispose : () => {};
1278
+ };
1279
+ }
1280
+ function cloneOps(ops) {
1281
+ try {
1282
+ return structuredClone(ops);
1283
+ } catch {
1284
+ return ops.map((op) => op.op === "set" ? {
1285
+ op: "set",
1286
+ path: [...op.path],
1287
+ value: op.value
1288
+ } : {
1289
+ op: "unset",
1290
+ path: [...op.path]
1291
+ });
1292
+ }
1293
+ }
1294
+ function messageOf(error) {
1295
+ return error instanceof Error ? error.message : String(error);
1296
+ }
1297
+ function sameSnapshot$1(a, b) {
1298
+ return a.status === b.status && a.revision === b.revision && a.writable === b.writable && a.mode === b.mode && a.value === b.value && a.base === b.base && a.user === b.user;
1299
+ }
1300
+ function readySnapshot(row, writable) {
1301
+ return {
1302
+ status: "ready",
1303
+ value: row.value,
1304
+ base: row.base,
1305
+ user: row.user,
1306
+ revision: row.revision,
1307
+ writable,
1308
+ mode: "host"
1309
+ };
1310
+ }
1311
+ function unavailableSnapshot(writable) {
1312
+ return {
1313
+ status: "unavailable",
1314
+ value: void 0,
1315
+ base: void 0,
1316
+ user: void 0,
1317
+ revision: void 0,
1318
+ writable,
1319
+ mode: "host"
1320
+ };
1321
+ }
1322
+ /**
1323
+ * Build the direct Host settings scope for one namespace.
1324
+ *
1325
+ * Snapshot semantics mirror the official per-namespace derivation: a namespace
1326
+ * row answers `ready` with the resolved section, its `base`/`user` layers and its
1327
+ * revision fence, and the document's `writable` decides editability. A row the
1328
+ * Host does not serve answers `unavailable`; a refused read keeps the last
1329
+ * confirmed document and reports `unavailable` only while it has never held one.
1330
+ *
1331
+ * Writes are serialized in issue order, fenced by the latest known revision and
1332
+ * REJECT with {@link SettingsWriteFailure} on a Host refusal — that rejection is
1333
+ * what makes the settings page surface a conflict instead of pretending the edit
1334
+ * landed (the official scope resolves on `!ok` after its recovery read).
1335
+ *
1336
+ * @param remote - the `remote.settings` face of the page's Remote service.
1337
+ * @param namespace - the settings namespace this scope derives from.
1338
+ * @returns the scope plus its load/dispose seams.
1339
+ */
1340
+ function createHostDirectScope(remote, namespace) {
1341
+ let snapshot = {
1342
+ status: "loading",
1343
+ value: void 0,
1344
+ base: void 0,
1345
+ user: void 0,
1346
+ revision: void 0,
1347
+ writable: false,
1348
+ mode: "host"
1349
+ };
1350
+ let held;
1351
+ let error;
1352
+ let disposed = false;
1353
+ let tail = Promise.resolve();
1354
+ const listeners = /* @__PURE__ */ new Set();
1355
+ const publish = (next) => {
1356
+ if (disposed || sameSnapshot$1(next, snapshot)) return;
1357
+ snapshot = next;
1358
+ for (const listener of [...listeners]) try {
1359
+ listener();
1360
+ } catch {}
1361
+ };
1362
+ const adopt = (next) => {
1363
+ if (next.status === "ready") held = next;
1364
+ publish(next);
1365
+ };
1366
+ async function load() {
1367
+ if (disposed) return;
1368
+ let outcome;
1369
+ try {
1370
+ outcome = await remote.describe();
1371
+ } catch (caught) {
1372
+ outcome = {
1373
+ ok: false,
1374
+ error: { message: messageOf(caught) }
1375
+ };
1376
+ }
1377
+ if (disposed) return;
1378
+ if (!outcome.ok) {
1379
+ error = outcome.error?.message ?? outcome.error?.code ?? "settings/describe-failed";
1380
+ publish(held ?? unavailableSnapshot(snapshot.writable));
1381
+ return;
1382
+ }
1383
+ error = void 0;
1384
+ const view = outcome.value;
1385
+ const writable = view.writable === true;
1386
+ const row = Array.isArray(view.namespaces) ? view.namespaces.find((candidate) => candidate.ns === namespace) : void 0;
1387
+ if (row === void 0 || typeof row.revision !== "number") {
1388
+ publish(unavailableSnapshot(writable));
1389
+ return;
1390
+ }
1391
+ adopt(readySnapshot(row, writable));
1392
+ }
1393
+ /** Fold one write answer's namespace row in without a second wire read. */
1394
+ function fold(row, writable) {
1395
+ if (typeof row.revision !== "number") return;
1396
+ adopt(readySnapshot(row, writable));
1397
+ }
1398
+ function mutate(ops, expectedRevision) {
1399
+ const owned = cloneOps(ops);
1400
+ const run = tail.then(async () => {
1401
+ if (disposed) throw new SettingsWriteFailure("settings/unavailable");
1402
+ const revision = expectedRevision ?? snapshot.revision;
1403
+ let outcome;
1404
+ try {
1405
+ outcome = revision === void 0 ? await remote.mutate(namespace, owned) : await remote.mutate(namespace, owned, revision);
1406
+ } catch (caught) {
1407
+ await load();
1408
+ throw new SettingsWriteFailure("settings/unreachable", messageOf(caught));
1409
+ }
1410
+ if (outcome.ok) {
1411
+ fold(outcome.value, true);
1412
+ return;
1413
+ }
1414
+ await load();
1415
+ throw new SettingsWriteFailure(outcome.error?.code ?? "settings/unknown", outcome.error?.message);
1416
+ });
1417
+ tail = run.then(() => void 0, () => void 0);
1418
+ return run;
1419
+ }
1420
+ return {
1421
+ getSnapshot: () => snapshot,
1422
+ subscribe(listener) {
1423
+ listeners.add(listener);
1424
+ return () => {
1425
+ listeners.delete(listener);
1426
+ };
1427
+ },
1428
+ set: (field, value) => mutate([{
1429
+ op: "set",
1430
+ path: [field],
1431
+ value
1432
+ }]),
1433
+ load,
1434
+ dispose() {
1435
+ disposed = true;
1436
+ listeners.clear();
1437
+ },
1438
+ lastError: () => error
1439
+ };
1440
+ }
1441
+ //#endregion
1442
+ //#region src/client/settings/settingsChannel.ts
1443
+ /** Snapshot used while no channel has answered yet. */
1444
+ const LOADING_SNAPSHOT = Object.freeze({
1445
+ status: "loading",
1446
+ value: void 0,
1447
+ base: void 0,
1448
+ user: void 0,
1449
+ revision: void 0,
1450
+ writable: false,
1451
+ mode: "host"
1452
+ });
1453
+ function sameSnapshot(a, b) {
1454
+ return a.status === b.status && a.revision === b.revision && a.writable === b.writable && a.mode === b.mode && a.value === b.value && a.base === b.base && a.user === b.user;
1455
+ }
1456
+ /** Ask a channel for its first/next read; a refusal surfaces as a snapshot, never a throw. */
1457
+ function loadSource(source) {
1458
+ const loadable = source;
1459
+ if (loadable === void 0 || typeof loadable.load !== "function") return Promise.resolve();
1460
+ return Promise.resolve(loadable.load()).then(() => void 0, () => void 0);
1461
+ }
1462
+ function createSettingsChannel(options) {
1463
+ let official;
1464
+ let direct;
1465
+ let active;
1466
+ let snapshot = LOADING_SNAPSHOT;
1467
+ let disposed = false;
1468
+ const disposers = [];
1469
+ const listeners = /* @__PURE__ */ new Set();
1470
+ const notify = () => {
1471
+ for (const listener of [...listeners]) try {
1472
+ listener();
1473
+ } catch {}
1474
+ };
1475
+ const publish = (next) => {
1476
+ if (disposed || sameSnapshot(next, snapshot)) return;
1477
+ snapshot = next;
1478
+ notify();
1479
+ };
1480
+ const select = () => {
1481
+ if (official !== void 0 && official.getSnapshot().status !== "unavailable") return official;
1482
+ if (direct === void 0) {
1483
+ try {
1484
+ direct = options.openDirect();
1485
+ } catch {
1486
+ direct = void 0;
1487
+ }
1488
+ if (direct !== void 0) {
1489
+ try {
1490
+ disposers.push(direct.subscribe(() => {
1491
+ if (active === direct) publish(direct.getSnapshot());
1492
+ }));
1493
+ } catch {}
1494
+ loadSource(direct);
1495
+ }
1496
+ }
1497
+ return direct ?? official;
1498
+ };
1499
+ const reselect = () => {
1500
+ if (disposed) return;
1501
+ const next = select();
1502
+ if (next !== active) {
1503
+ active = next;
1504
+ publish(next?.getSnapshot() ?? LOADING_SNAPSHOT);
1505
+ return;
1506
+ }
1507
+ publish(next?.getSnapshot() ?? LOADING_SNAPSHOT);
1508
+ };
1509
+ return {
1510
+ getSnapshot: () => snapshot,
1511
+ subscribe(listener) {
1512
+ listeners.add(listener);
1513
+ return () => {
1514
+ listeners.delete(listener);
1515
+ };
1516
+ },
1517
+ set(field, value) {
1518
+ const source = active;
1519
+ if (source === void 0) return Promise.reject(/* @__PURE__ */ new Error("dsh-update-status: settings channel is unavailable"));
1520
+ return source.set(field, value);
1521
+ },
1522
+ setOfficial(scope) {
1523
+ if (disposed) return;
1524
+ official = scope;
1525
+ if (scope !== void 0) try {
1526
+ disposers.push(scope.subscribe(() => {
1527
+ reselect();
1528
+ }));
1529
+ } catch {}
1530
+ reselect();
1531
+ },
1532
+ refresh: reselect,
1533
+ reload() {
1534
+ if (active === void 0 || active === official) return;
1535
+ loadSource(active);
1536
+ },
1537
+ dispose() {
1538
+ disposed = true;
1539
+ listeners.clear();
1540
+ for (const dispose of disposers.splice(0)) try {
1541
+ dispose();
1542
+ } catch {}
1543
+ const disposable = active;
1544
+ if (disposable !== void 0 && typeof disposable.dispose === "function") try {
1545
+ disposable.dispose();
1546
+ } catch {}
1547
+ }
1548
+ };
1549
+ }
1550
+ //#endregion
1239
1551
  //#region src/client/styles.ts
1240
1552
  /** Plugin-owned CSS only; no shell DOM selection or official SVG manipulation. */
1241
1553
  const UPDATE_STATUS_CSS = `
1242
- .dus-brand-name{align-items:center;gap:8px;min-width:0;width:100%;display:flex}
1243
- .dus-brand-deepseek{font-size:15px;font-weight:650;letter-spacing:-.015em;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1244
- .dus-badge{align-items:center;gap:5px;background:var(--dsw-alias-button-floating-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-secondary);cursor:pointer;display:inline-flex;flex:0 0 auto;font-size:11px;font-variant-numeric:tabular-nums;line-height:20px;max-width:132px;outline:none;padding:0 8px;touch-action:manipulation;user-select:none}
1554
+ .dus-brand-name{align-items:center;gap:6px;height:24px;max-width:100%;min-width:0;display:inline-flex}
1555
+ .dus-brand-deepseek{font-size:14px;font-weight:650;letter-spacing:-.015em;line-height:24px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1556
+ .dus-badge{align-items:center;background:var(--dsw-alias-label-primary,#111);border:0;border-radius:4px;color:var(--dsw-alias-label-primary-inverted,#fff);cursor:pointer;display:inline-flex;flex:none;font-family:var(--ds-font-family-code,var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace));font-size:10px;font-variant-numeric:tabular-nums;font-weight:600;gap:4px;height:16px;line-height:16px;max-width:140px;outline:none;padding:0 6px;touch-action:manipulation;user-select:none;white-space:nowrap}
1245
1557
  .dus-badge:focus-visible,.dus-footer-button:focus-visible,.dus-action:focus-visible,.dus-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
1246
- .dus-badge:hover{background:var(--dsw-alias-button-floating-hover)}
1247
- .dus-badge[data-update=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}
1248
- .dus-badge[data-error=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
1558
+ .dus-badge:hover{filter:brightness(1.08)}
1559
+ .dus-badge[data-update=true]{background:var(--dsw-alias-state-warn-primary,#d97706);color:#111}
1560
+ .dus-badge[data-error=true]{background:var(--dsw-alias-state-error-primary,#dc2626);color:#fff}
1249
1561
  .dus-badge-version{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1250
- .dus-dot{background:currentColor;border-radius:50%;display:inline-block;flex:0 0 auto;height:6px;width:6px}
1562
+ .dus-dot{background:currentColor;border-radius:50%;display:inline-block;flex:0 0 auto;height:5px;opacity:.9;width:5px}
1251
1563
  .dus-dot[data-update=true]{animation:dus-pulse 1.5s ease-in-out infinite;background:var(--dsw-alias-state-warn-primary)}
1252
1564
  .dus-dot[data-loading=true]{animation:dus-pulse .9s ease-in-out infinite}
1253
1565
  @keyframes dus-pulse{0%,100%{opacity:.45;transform:scale(.82)}50%{opacity:1;transform:scale(1.15)}}
@@ -1309,7 +1621,7 @@ window.__ModuleLoader__.load({
1309
1621
  .dus-channel-compat[data-compatibility=verified]{color:var(--dsw-alias-state-success-primary)}
1310
1622
  .dus-channel-compat[data-compatibility=incompatible]{color:var(--dsw-alias-state-error-primary)}
1311
1623
  .dus-settings-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45;margin:8px 0 0}
1312
- @media (max-width:640px),(hover:none) and (pointer:coarse){.dus-panel[data-origin]{border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0;bottom:0;left:0;right:0;max-height:min(78dvh,calc(100dvh - env(safe-area-inset-top)));padding:16px max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));position:fixed;top:auto;width:100vw}.dus-footer-button{height:48px;min-height:48px;min-width:48px;width:48px}.dus-badge{line-height:24px;min-height:28px}.dus-action{min-height:40px;line-height:38px}.dus-close{height:40px;min-height:40px;min-width:40px;width:40px}}
1624
+ @media (max-width:640px),(hover:none) and (pointer:coarse){.dus-panel[data-origin]{border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0;bottom:0;left:0;right:0;max-height:min(78dvh,calc(100dvh - env(safe-area-inset-top)));padding:16px max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));position:fixed;top:auto;width:100vw}.dus-footer-button{height:48px;min-height:48px;min-width:48px;width:48px}.dus-action{min-height:40px;line-height:38px}.dus-close{height:40px;min-height:40px;min-width:40px;width:40px}}
1313
1625
  `;
1314
1626
  //#endregion
1315
1627
  //#region src/client/index.ts
@@ -1337,48 +1649,87 @@ window.__ModuleLoader__.load({
1337
1649
  }
1338
1650
  }
1339
1651
  function apply(ctx) {
1340
- const connection = connectionOf(ctx);
1341
- const canManage = connection.isLoopback === true;
1342
- const status = new StatusStore(connection, canManage ? null : STATIC_COMPATIBLE_VERSION);
1652
+ const status = new StatusStore(connectionOf(ctx));
1343
1653
  const preferences = new PreferencesStore();
1344
1654
  const ui = {
1345
1655
  status,
1346
1656
  preferences,
1347
- panel: new PanelStore(),
1348
- canManage
1657
+ panel: new PanelStore()
1349
1658
  };
1659
+ const disposers = [];
1660
+ let stopped = false;
1661
+ /** Own a subscription raised from an injection callback; release after teardown at once. */
1662
+ const own = (dispose) => {
1663
+ if (typeof dispose !== "function") return;
1664
+ if (stopped) {
1665
+ try {
1666
+ dispose();
1667
+ } catch {}
1668
+ return;
1669
+ }
1670
+ disposers.push(dispose);
1671
+ };
1672
+ let remoteSettings;
1673
+ /** Resolve the direct channel's Remote face; `ctx.get` covers a payload we could not read. */
1674
+ const directRemote = () => {
1675
+ if (remoteSettings !== void 0) return remoteSettings;
1676
+ try {
1677
+ remoteSettings = settingsRemoteFace(ctx.get("remote.settings"));
1678
+ } catch {}
1679
+ return remoteSettings;
1680
+ };
1681
+ const channel = createSettingsChannel({ openDirect: () => {
1682
+ const remote = directRemote();
1683
+ return remote === void 0 ? void 0 : createHostDirectScope(remote, SETTINGS_NAMESPACE);
1684
+ } });
1350
1685
  ctx.effect(() => {
1351
1686
  const disposeStyles = installStyles();
1352
- if (canManage) status.load(preferences.getSnapshot().cacheTtlMinutes);
1687
+ const detach = preferences.attach(channel);
1688
+ let selected = status.getSnapshot().status?.channel ?? "latest";
1689
+ const syncPreferences = () => {
1690
+ const next = preferences.getSnapshot();
1691
+ if (next.channel !== selected) {
1692
+ selected = next.channel;
1693
+ status.selectChannel(selected, next.cacheTtlMinutes);
1694
+ }
1695
+ };
1696
+ syncPreferences();
1697
+ const unsubscribe = preferences.subscribe(syncPreferences);
1698
+ status.load(preferences.getSnapshot().cacheTtlMinutes);
1353
1699
  return () => {
1700
+ stopped = true;
1701
+ unsubscribe();
1702
+ detach();
1703
+ for (const dispose of disposers.splice(0)) try {
1704
+ dispose();
1705
+ } catch {}
1706
+ channel.dispose();
1354
1707
  status.stop();
1355
1708
  disposeStyles();
1356
1709
  };
1357
- }, "dsh-update-status: style and first status read");
1358
- ctx.inject(["settingsScope"], (raw) => {
1359
- const binder = raw.settingsScope;
1360
- if (binder === null || typeof binder !== "object" || typeof binder.bind !== "function") return;
1361
- try {
1362
- const scope = binder.bind({ namespace: SETTINGS_NAMESPACE });
1363
- ctx.effect(() => {
1364
- const detach = preferences.attach(scope);
1365
- let selected = status.getSnapshot().status?.channel ?? "latest";
1366
- const syncPreferences = () => {
1367
- const next = preferences.getSnapshot();
1368
- if (next.channel !== selected) {
1369
- selected = next.channel;
1370
- status.selectChannel(selected, next.cacheTtlMinutes);
1371
- }
1372
- };
1373
- syncPreferences();
1374
- const unsubscribe = preferences.subscribe(syncPreferences);
1375
- return () => {
1376
- unsubscribe();
1377
- detach();
1378
- };
1379
- }, "dsh-update-status: sidebar and channel preferences");
1380
- } catch {}
1381
- });
1710
+ }, "dsh-update-status: styles, settings channel and first status read");
1711
+ try {
1712
+ ctx.inject(["settingsScope"], (raw) => {
1713
+ try {
1714
+ const binder = binderOf(raw);
1715
+ if (binder === void 0) return;
1716
+ channel.setOfficial(scopeOf(binder.bind({ namespace: SETTINGS_NAMESPACE })));
1717
+ } catch {}
1718
+ });
1719
+ } catch {}
1720
+ try {
1721
+ ctx.inject(["remote.settings"], (raw) => {
1722
+ try {
1723
+ remoteSettings = settingsRemoteOf(raw) ?? remoteSettings;
1724
+ channel.refresh();
1725
+ const invalidations = settingsInvalidationsOf(ctx.get("remote"));
1726
+ if (invalidations !== void 0) own(invalidations(() => channel.reload()));
1727
+ } catch {}
1728
+ });
1729
+ } catch {}
1730
+ if (typeof ctx.on === "function") try {
1731
+ own(ctx.on("connection/reset", () => channel.reload()));
1732
+ } catch {}
1382
1733
  ctx.slots.inject("sidebar.brand.name", () => ctx.slots.register({
1383
1734
  name: "sidebar.brand.name",
1384
1735
  priority: -10