dsh-update-status 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -12,8 +12,16 @@ window.__ModuleLoader__.load({
12
12
  };
13
13
  /**
14
14
  * Reduce raw dist-tags to user-meaningful choices without losing Host facts.
15
- * The selected channel and stable return path are mandatory; other rows must
16
- * represent a version that differs from both the running DSH and earlier rows.
15
+ * The selected channel and stable return path are always present; every other
16
+ * row must carry a version no earlier row already shows.
17
+ *
18
+ * A channel is deliberately NOT hidden just because it currently points at the
19
+ * release that is running. Following a channel is a statement about future
20
+ * releases, and `currentVersion` still rides the input for the caller's own
21
+ * rendering, but filtering on it created a deadlock: while running the version
22
+ * published on `alpha`, the `alpha` row disappeared, so the one channel a user
23
+ * on that line would want to follow could never be selected — the only way to
24
+ * see it was to already follow it.
17
25
  */
18
26
  function visibleChannelReleases(status) {
19
27
  const ordered = [...status.channels].sort((left, right) => CHANNEL_ORDER[left.channel] - CHANNEL_ORDER[right.channel]);
@@ -21,7 +29,7 @@ window.__ModuleLoader__.load({
21
29
  const visible = ordered.filter((release) => mandatory.has(release.channel));
22
30
  const seenVersions = new Set(visible.map((release) => release.version).filter((version) => version !== null));
23
31
  for (const release of ordered) {
24
- if (mandatory.has(release.channel) || release.version === null || release.version === status.currentVersion) continue;
32
+ if (mandatory.has(release.channel) || release.version === null) continue;
25
33
  if (seenVersions.has(release.version)) continue;
26
34
  visible.push(release);
27
35
  seenVersions.add(release.version);
@@ -42,13 +50,6 @@ window.__ModuleLoader__.load({
42
50
  const PACKAGE_NAME = "@deepseek-ai/dsh";
43
51
  /** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */
44
52
  const UPDATE_STATUS_CHANNEL = "/api";
45
- /** Exact DSH release this bundle declares compatible in package.json. */
46
- const STATIC_COMPATIBLE_VERSION = "0.1.5-rc.1";
47
- const RELEASE_CHANNELS = [
48
- "latest",
49
- "next",
50
- "alpha"
51
- ];
52
53
  const MAX_CACHE_TTL_MINUTES = 1440;
53
54
  function isCacheTtlMinutes(value) {
54
55
  return typeof value === "number" && Number.isInteger(value) && value >= 30 && value <= 1440;
@@ -439,7 +440,7 @@ window.__ModuleLoader__.load({
439
440
  event.stopPropagation();
440
441
  const target = event.currentTarget;
441
442
  if (target instanceof HTMLElement) target.blur();
442
- if (ui.canManage) ui.panel.toggle("brand");
443
+ ui.panel.toggle("brand");
443
444
  };
444
445
  const onKeyDown = (event) => {
445
446
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -454,13 +455,12 @@ window.__ModuleLoader__.load({
454
455
  className: "dus-badge",
455
456
  "data-update": snapshot.status?.hasUpdate === true || void 0,
456
457
  "data-error": state === "problem" || void 0,
457
- role: ui.canManage ? "button" : void 0,
458
- tabIndex: ui.canManage ? 0 : void 0,
458
+ role: "button",
459
+ tabIndex: 0,
459
460
  "aria-label": badgeLabel(snapshot.status, snapshot.loading, snapshot.error),
460
- "aria-disabled": ui.canManage ? void 0 : true,
461
461
  title: badgeLabel(snapshot.status, snapshot.loading, snapshot.error),
462
- onClick: ui.canManage ? activate : void 0,
463
- onKeyDown: ui.canManage ? onKeyDown : void 0,
462
+ onClick: activate,
463
+ onKeyDown,
464
464
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
465
465
  className: "dus-badge-version",
466
466
  children: shortVersion(version)
@@ -485,7 +485,6 @@ window.__ModuleLoader__.load({
485
485
  type: "button",
486
486
  "aria-label": t("footer.status"),
487
487
  title: label,
488
- disabled: !ui.canManage,
489
488
  onClick: () => {
490
489
  ui.panel.toggle("rail");
491
490
  },
@@ -534,7 +533,7 @@ window.__ModuleLoader__.load({
534
533
  element.focus();
535
534
  } catch {}
536
535
  }
537
- /** Full detail is only opened from a loopback/local DSH UI surface. */
536
+ /** Full detail from whichever page the operator is on (the transport is authenticated). */
538
537
  function UpdatePanel({ ui }) {
539
538
  const panel = useObservable(ui.panel);
540
539
  const preferences = useObservable(ui.preferences);
@@ -542,7 +541,7 @@ window.__ModuleLoader__.load({
542
541
  const commandRef = React.useRef(null);
543
542
  const dialogRef = React.useRef(null);
544
543
  const [copyMessage, setCopyMessage] = React.useState(null);
545
- const visible = panel.open && preferences.sidebarEnabled && ui.canManage;
544
+ const visible = panel.open && preferences.sidebarEnabled;
546
545
  React.useLayoutEffect(() => {
547
546
  if (!visible) return void 0;
548
547
  const dialog = dialogRef.current;
@@ -861,7 +860,7 @@ window.__ModuleLoader__.load({
861
860
  className: "dus-settings-hint",
862
861
  children: t("preview.follow")
863
862
  }),
864
- ui.canManage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
865
864
  className: "dus-action",
866
865
  type: "button",
867
866
  "aria-expanded": showGuidance,
@@ -870,7 +869,7 @@ window.__ModuleLoader__.load({
870
869
  },
871
870
  children: t(showGuidance ? "preview.close" : "preview.open")
872
871
  }),
873
- ui.canManage && showGuidance && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
872
+ showGuidance && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
874
873
  "aria-label": t("preview.open"),
875
874
  children: [
876
875
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -986,40 +985,12 @@ window.__ModuleLoader__.load({
986
985
  };
987
986
  var StatusStore = class {
988
987
  connection;
989
- snapshot;
990
- allowRequests;
988
+ snapshot = INITIAL_STATUS;
991
989
  listeners = /* @__PURE__ */ new Set();
992
990
  inFlight;
993
991
  stopped = false;
994
- constructor(connection, staticVersion = null) {
992
+ constructor(connection) {
995
993
  this.connection = connection;
996
- this.allowRequests = staticVersion === null;
997
- this.snapshot = staticVersion === null ? INITIAL_STATUS : {
998
- status: {
999
- currentVersion: staticVersion,
1000
- latestVersion: null,
1001
- hasUpdate: false,
1002
- cached: true,
1003
- checkedAt: null,
1004
- warning: null,
1005
- installKind: "unknown",
1006
- upgradeCommand: "",
1007
- releaseUrl: "",
1008
- changelogUrl: "",
1009
- publishedAt: null,
1010
- packageName: "@deepseek-ai/dsh",
1011
- channel: "latest",
1012
- channels: RELEASE_CHANNELS.map((channel) => ({
1013
- channel,
1014
- version: null,
1015
- publishedAt: null,
1016
- compatibility: "unverified"
1017
- })),
1018
- canApplyInPlace: false
1019
- },
1020
- loading: false,
1021
- error: null
1022
- };
1023
994
  }
1024
995
  getSnapshot = () => this.snapshot;
1025
996
  subscribe = (listener) => {
@@ -1030,15 +1001,14 @@ window.__ModuleLoader__.load({
1030
1001
  };
1031
1002
  /** First mount reads the Host's cached status; it never starts a browser timer. */
1032
1003
  async load(cacheTtlMinutes = 360) {
1033
- if (this.allowRequests) await this.request(false, this.snapshot.status?.channel ?? "latest", cacheTtlMinutes);
1004
+ await this.request(false, this.snapshot.status?.channel ?? "latest", cacheTtlMinutes);
1034
1005
  }
1035
1006
  /** User gesture only: force the Host to bypass TTL (while retaining single-flight). */
1036
1007
  async refresh(channel = this.snapshot.status?.channel ?? "latest", cacheTtlMinutes = 360) {
1037
- if (this.allowRequests) await this.request(true, channel, cacheTtlMinutes);
1008
+ await this.request(true, channel, cacheTtlMinutes);
1038
1009
  }
1039
1010
  /** Channel selection re-projects the Host cache; it is not a forced refresh. */
1040
1011
  async selectChannel(channel, cacheTtlMinutes = 360) {
1041
- if (!this.allowRequests) return;
1042
1012
  for (let attempts = 0; attempts < 3 && !this.stopped && this.snapshot.status?.channel !== channel; attempts += 1) {
1043
1013
  const pending = this.inFlight;
1044
1014
  if (pending !== void 0 && !await pending) return;
@@ -1237,6 +1207,355 @@ window.__ModuleLoader__.load({
1237
1207
  /** The one namespace name used by Host registration and browser binding. */
1238
1208
  const SETTINGS_NAMESPACE = PLUGIN_ID$1;
1239
1209
  //#endregion
1210
+ //#region src/client/settings/scopeFaces.ts
1211
+ /** Guard: does an object look like a binder with bind()? */
1212
+ function binderOf(raw) {
1213
+ if (raw === null || typeof raw !== "object") return void 0;
1214
+ const binder = raw.settingsScope;
1215
+ if (binder === null || typeof binder !== "object") return void 0;
1216
+ if (typeof binder.bind !== "function") return void 0;
1217
+ return binder;
1218
+ }
1219
+ /** Guard: does an object look like a bound scope? */
1220
+ function scopeOf(value) {
1221
+ if (value === null || typeof value !== "object") return void 0;
1222
+ const scope = value;
1223
+ if (typeof scope.getSnapshot !== "function") return void 0;
1224
+ if (typeof scope.subscribe !== "function") return void 0;
1225
+ if (typeof scope.set !== "function") return void 0;
1226
+ return scope;
1227
+ }
1228
+ //#endregion
1229
+ //#region src/client/settings/hostDirectScope.ts
1230
+ /** A refused namespace write; `code` keeps the Host's own diagnosis. */
1231
+ var SettingsWriteFailure = class extends Error {
1232
+ code;
1233
+ constructor(code, message) {
1234
+ super(message === void 0 || message === "" ? code : message);
1235
+ this.name = "SettingsWriteFailure";
1236
+ this.code = code;
1237
+ }
1238
+ };
1239
+ function isRecord(value) {
1240
+ return typeof value === "object" && value !== null;
1241
+ }
1242
+ /**
1243
+ * Guard: does this value look like the `remote.settings` namespace face?
1244
+ * Applied to a bare service object (`ctx.get('remote.settings')` or a payload
1245
+ * member), and it never calls anything.
1246
+ */
1247
+ function settingsRemoteFace(value) {
1248
+ if (!isRecord(value)) return void 0;
1249
+ if (typeof value.describe !== "function" || typeof value.mutate !== "function") return void 0;
1250
+ return value;
1251
+ }
1252
+ /**
1253
+ * Guard: read the `remote.settings` face off an injection payload.
1254
+ *
1255
+ * The payload a `ctx.inject(['remote.settings'], …)` callback receives exposes
1256
+ * the LITERAL service key. Reading the dotted PARENT off that injected context
1257
+ * (`payload.remote`) throws `cannot get property "remote" without inject` — a
1258
+ * throw that used to abort a whole wiring callback on a LAN page. So the literal
1259
+ * key is read FIRST and every read is contained; the nested
1260
+ * (`payload.remote.settings`) shape is only a fallback for a plain object
1261
+ * payload. Both absence and refusal answer `undefined`.
1262
+ */
1263
+ function settingsRemoteOf(raw) {
1264
+ if (!isRecord(raw)) return void 0;
1265
+ try {
1266
+ const direct = settingsRemoteFace(raw["remote.settings"]);
1267
+ if (direct !== void 0) return direct;
1268
+ } catch {}
1269
+ try {
1270
+ const remote = raw.remote;
1271
+ if (isRecord(remote)) return settingsRemoteFace(remote.settings);
1272
+ } catch {}
1273
+ }
1274
+ /**
1275
+ * Guard: subscribe to the Host's settings-document invalidation on the Remote
1276
+ * service object (`remote.$on`). Returns undefined when the service exposes no
1277
+ * event seam — the preferences then refresh on reconnect only.
1278
+ */
1279
+ function settingsInvalidationsOf(service) {
1280
+ if (!isRecord(service)) return void 0;
1281
+ const on = service.$on;
1282
+ if (typeof on !== "function") return void 0;
1283
+ return (listener) => {
1284
+ const dispose = on.call(service, "settings/document-updated", listener);
1285
+ return typeof dispose === "function" ? dispose : () => {};
1286
+ };
1287
+ }
1288
+ function cloneOps(ops) {
1289
+ try {
1290
+ return structuredClone(ops);
1291
+ } catch {
1292
+ return ops.map((op) => op.op === "set" ? {
1293
+ op: "set",
1294
+ path: [...op.path],
1295
+ value: op.value
1296
+ } : {
1297
+ op: "unset",
1298
+ path: [...op.path]
1299
+ });
1300
+ }
1301
+ }
1302
+ function messageOf(error) {
1303
+ return error instanceof Error ? error.message : String(error);
1304
+ }
1305
+ function sameSnapshot$1(a, b) {
1306
+ 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;
1307
+ }
1308
+ function readySnapshot(row, writable) {
1309
+ return {
1310
+ status: "ready",
1311
+ value: row.value,
1312
+ base: row.base,
1313
+ user: row.user,
1314
+ revision: row.revision,
1315
+ writable,
1316
+ mode: "host"
1317
+ };
1318
+ }
1319
+ function unavailableSnapshot(writable) {
1320
+ return {
1321
+ status: "unavailable",
1322
+ value: void 0,
1323
+ base: void 0,
1324
+ user: void 0,
1325
+ revision: void 0,
1326
+ writable,
1327
+ mode: "host"
1328
+ };
1329
+ }
1330
+ /**
1331
+ * Build the direct Host settings scope for one namespace.
1332
+ *
1333
+ * Snapshot semantics mirror the official per-namespace derivation: a namespace
1334
+ * row answers `ready` with the resolved section, its `base`/`user` layers and its
1335
+ * revision fence, and the document's `writable` decides editability. A row the
1336
+ * Host does not serve answers `unavailable`; a refused read keeps the last
1337
+ * confirmed document and reports `unavailable` only while it has never held one.
1338
+ *
1339
+ * Writes are serialized in issue order, fenced by the latest known revision and
1340
+ * REJECT with {@link SettingsWriteFailure} on a Host refusal — that rejection is
1341
+ * what makes the settings page surface a conflict instead of pretending the edit
1342
+ * landed (the official scope resolves on `!ok` after its recovery read).
1343
+ *
1344
+ * @param remote - the `remote.settings` face of the page's Remote service.
1345
+ * @param namespace - the settings namespace this scope derives from.
1346
+ * @returns the scope plus its load/dispose seams.
1347
+ */
1348
+ function createHostDirectScope(remote, namespace) {
1349
+ let snapshot = {
1350
+ status: "loading",
1351
+ value: void 0,
1352
+ base: void 0,
1353
+ user: void 0,
1354
+ revision: void 0,
1355
+ writable: false,
1356
+ mode: "host"
1357
+ };
1358
+ let held;
1359
+ let error;
1360
+ let disposed = false;
1361
+ let tail = Promise.resolve();
1362
+ const listeners = /* @__PURE__ */ new Set();
1363
+ const publish = (next) => {
1364
+ if (disposed || sameSnapshot$1(next, snapshot)) return;
1365
+ snapshot = next;
1366
+ for (const listener of [...listeners]) try {
1367
+ listener();
1368
+ } catch {}
1369
+ };
1370
+ const adopt = (next) => {
1371
+ if (next.status === "ready") held = next;
1372
+ publish(next);
1373
+ };
1374
+ async function load() {
1375
+ if (disposed) return;
1376
+ let outcome;
1377
+ try {
1378
+ outcome = await remote.describe();
1379
+ } catch (caught) {
1380
+ outcome = {
1381
+ ok: false,
1382
+ error: { message: messageOf(caught) }
1383
+ };
1384
+ }
1385
+ if (disposed) return;
1386
+ if (!outcome.ok) {
1387
+ error = outcome.error?.message ?? outcome.error?.code ?? "settings/describe-failed";
1388
+ publish(held ?? unavailableSnapshot(snapshot.writable));
1389
+ return;
1390
+ }
1391
+ error = void 0;
1392
+ const view = outcome.value;
1393
+ const writable = view.writable === true;
1394
+ const row = Array.isArray(view.namespaces) ? view.namespaces.find((candidate) => candidate.ns === namespace) : void 0;
1395
+ if (row === void 0 || typeof row.revision !== "number") {
1396
+ publish(unavailableSnapshot(writable));
1397
+ return;
1398
+ }
1399
+ adopt(readySnapshot(row, writable));
1400
+ }
1401
+ /** Fold one write answer's namespace row in without a second wire read. */
1402
+ function fold(row, writable) {
1403
+ if (typeof row.revision !== "number") return;
1404
+ adopt(readySnapshot(row, writable));
1405
+ }
1406
+ function mutate(ops, expectedRevision) {
1407
+ const owned = cloneOps(ops);
1408
+ const run = tail.then(async () => {
1409
+ if (disposed) throw new SettingsWriteFailure("settings/unavailable");
1410
+ const revision = expectedRevision ?? snapshot.revision;
1411
+ let outcome;
1412
+ try {
1413
+ outcome = revision === void 0 ? await remote.mutate(namespace, owned) : await remote.mutate(namespace, owned, revision);
1414
+ } catch (caught) {
1415
+ await load();
1416
+ throw new SettingsWriteFailure("settings/unreachable", messageOf(caught));
1417
+ }
1418
+ if (outcome.ok) {
1419
+ fold(outcome.value, true);
1420
+ return;
1421
+ }
1422
+ await load();
1423
+ throw new SettingsWriteFailure(outcome.error?.code ?? "settings/unknown", outcome.error?.message);
1424
+ });
1425
+ tail = run.then(() => void 0, () => void 0);
1426
+ return run;
1427
+ }
1428
+ return {
1429
+ getSnapshot: () => snapshot,
1430
+ subscribe(listener) {
1431
+ listeners.add(listener);
1432
+ return () => {
1433
+ listeners.delete(listener);
1434
+ };
1435
+ },
1436
+ set: (field, value) => mutate([{
1437
+ op: "set",
1438
+ path: [field],
1439
+ value
1440
+ }]),
1441
+ load,
1442
+ dispose() {
1443
+ disposed = true;
1444
+ listeners.clear();
1445
+ },
1446
+ lastError: () => error
1447
+ };
1448
+ }
1449
+ //#endregion
1450
+ //#region src/client/settings/settingsChannel.ts
1451
+ /** Snapshot used while no channel has answered yet. */
1452
+ const LOADING_SNAPSHOT = Object.freeze({
1453
+ status: "loading",
1454
+ value: void 0,
1455
+ base: void 0,
1456
+ user: void 0,
1457
+ revision: void 0,
1458
+ writable: false,
1459
+ mode: "host"
1460
+ });
1461
+ function sameSnapshot(a, b) {
1462
+ 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;
1463
+ }
1464
+ /** Ask a channel for its first/next read; a refusal surfaces as a snapshot, never a throw. */
1465
+ function loadSource(source) {
1466
+ const loadable = source;
1467
+ if (loadable === void 0 || typeof loadable.load !== "function") return Promise.resolve();
1468
+ return Promise.resolve(loadable.load()).then(() => void 0, () => void 0);
1469
+ }
1470
+ function createSettingsChannel(options) {
1471
+ let official;
1472
+ let direct;
1473
+ let active;
1474
+ let snapshot = LOADING_SNAPSHOT;
1475
+ let disposed = false;
1476
+ const disposers = [];
1477
+ const listeners = /* @__PURE__ */ new Set();
1478
+ const notify = () => {
1479
+ for (const listener of [...listeners]) try {
1480
+ listener();
1481
+ } catch {}
1482
+ };
1483
+ const publish = (next) => {
1484
+ if (disposed || sameSnapshot(next, snapshot)) return;
1485
+ snapshot = next;
1486
+ notify();
1487
+ };
1488
+ const select = () => {
1489
+ if (official !== void 0 && official.getSnapshot().status !== "unavailable") return official;
1490
+ if (direct === void 0) {
1491
+ try {
1492
+ direct = options.openDirect();
1493
+ } catch {
1494
+ direct = void 0;
1495
+ }
1496
+ if (direct !== void 0) {
1497
+ try {
1498
+ disposers.push(direct.subscribe(() => {
1499
+ if (active === direct) publish(direct.getSnapshot());
1500
+ }));
1501
+ } catch {}
1502
+ loadSource(direct);
1503
+ }
1504
+ }
1505
+ return direct ?? official;
1506
+ };
1507
+ const reselect = () => {
1508
+ if (disposed) return;
1509
+ const next = select();
1510
+ if (next !== active) {
1511
+ active = next;
1512
+ publish(next?.getSnapshot() ?? LOADING_SNAPSHOT);
1513
+ return;
1514
+ }
1515
+ publish(next?.getSnapshot() ?? LOADING_SNAPSHOT);
1516
+ };
1517
+ return {
1518
+ getSnapshot: () => snapshot,
1519
+ subscribe(listener) {
1520
+ listeners.add(listener);
1521
+ return () => {
1522
+ listeners.delete(listener);
1523
+ };
1524
+ },
1525
+ set(field, value) {
1526
+ const source = active;
1527
+ if (source === void 0) return Promise.reject(/* @__PURE__ */ new Error("dsh-update-status: settings channel is unavailable"));
1528
+ return source.set(field, value);
1529
+ },
1530
+ setOfficial(scope) {
1531
+ if (disposed) return;
1532
+ official = scope;
1533
+ if (scope !== void 0) try {
1534
+ disposers.push(scope.subscribe(() => {
1535
+ reselect();
1536
+ }));
1537
+ } catch {}
1538
+ reselect();
1539
+ },
1540
+ refresh: reselect,
1541
+ reload() {
1542
+ if (active === void 0 || active === official) return;
1543
+ loadSource(active);
1544
+ },
1545
+ dispose() {
1546
+ disposed = true;
1547
+ listeners.clear();
1548
+ for (const dispose of disposers.splice(0)) try {
1549
+ dispose();
1550
+ } catch {}
1551
+ const disposable = active;
1552
+ if (disposable !== void 0 && typeof disposable.dispose === "function") try {
1553
+ disposable.dispose();
1554
+ } catch {}
1555
+ }
1556
+ };
1557
+ }
1558
+ //#endregion
1240
1559
  //#region src/client/styles.ts
1241
1560
  /** Plugin-owned CSS only; no shell DOM selection or official SVG manipulation. */
1242
1561
  const UPDATE_STATUS_CSS = `
@@ -1338,48 +1657,87 @@ window.__ModuleLoader__.load({
1338
1657
  }
1339
1658
  }
1340
1659
  function apply(ctx) {
1341
- const connection = connectionOf(ctx);
1342
- const canManage = connection.isLoopback === true;
1343
- const status = new StatusStore(connection, canManage ? null : STATIC_COMPATIBLE_VERSION);
1660
+ const status = new StatusStore(connectionOf(ctx));
1344
1661
  const preferences = new PreferencesStore();
1345
1662
  const ui = {
1346
1663
  status,
1347
1664
  preferences,
1348
- panel: new PanelStore(),
1349
- canManage
1665
+ panel: new PanelStore()
1350
1666
  };
1667
+ const disposers = [];
1668
+ let stopped = false;
1669
+ /** Own a subscription raised from an injection callback; release after teardown at once. */
1670
+ const own = (dispose) => {
1671
+ if (typeof dispose !== "function") return;
1672
+ if (stopped) {
1673
+ try {
1674
+ dispose();
1675
+ } catch {}
1676
+ return;
1677
+ }
1678
+ disposers.push(dispose);
1679
+ };
1680
+ let remoteSettings;
1681
+ /** Resolve the direct channel's Remote face; `ctx.get` covers a payload we could not read. */
1682
+ const directRemote = () => {
1683
+ if (remoteSettings !== void 0) return remoteSettings;
1684
+ try {
1685
+ remoteSettings = settingsRemoteFace(ctx.get("remote.settings"));
1686
+ } catch {}
1687
+ return remoteSettings;
1688
+ };
1689
+ const channel = createSettingsChannel({ openDirect: () => {
1690
+ const remote = directRemote();
1691
+ return remote === void 0 ? void 0 : createHostDirectScope(remote, SETTINGS_NAMESPACE);
1692
+ } });
1351
1693
  ctx.effect(() => {
1352
1694
  const disposeStyles = installStyles();
1353
- if (canManage) status.load(preferences.getSnapshot().cacheTtlMinutes);
1695
+ const detach = preferences.attach(channel);
1696
+ let selected = status.getSnapshot().status?.channel ?? "latest";
1697
+ const syncPreferences = () => {
1698
+ const next = preferences.getSnapshot();
1699
+ if (next.channel !== selected) {
1700
+ selected = next.channel;
1701
+ status.selectChannel(selected, next.cacheTtlMinutes);
1702
+ }
1703
+ };
1704
+ syncPreferences();
1705
+ const unsubscribe = preferences.subscribe(syncPreferences);
1706
+ status.load(preferences.getSnapshot().cacheTtlMinutes);
1354
1707
  return () => {
1708
+ stopped = true;
1709
+ unsubscribe();
1710
+ detach();
1711
+ for (const dispose of disposers.splice(0)) try {
1712
+ dispose();
1713
+ } catch {}
1714
+ channel.dispose();
1355
1715
  status.stop();
1356
1716
  disposeStyles();
1357
1717
  };
1358
- }, "dsh-update-status: style and first status read");
1359
- ctx.inject(["settingsScope"], (raw) => {
1360
- const binder = raw.settingsScope;
1361
- if (binder === null || typeof binder !== "object" || typeof binder.bind !== "function") return;
1362
- try {
1363
- const scope = binder.bind({ namespace: SETTINGS_NAMESPACE });
1364
- ctx.effect(() => {
1365
- const detach = preferences.attach(scope);
1366
- let selected = status.getSnapshot().status?.channel ?? "latest";
1367
- const syncPreferences = () => {
1368
- const next = preferences.getSnapshot();
1369
- if (next.channel !== selected) {
1370
- selected = next.channel;
1371
- status.selectChannel(selected, next.cacheTtlMinutes);
1372
- }
1373
- };
1374
- syncPreferences();
1375
- const unsubscribe = preferences.subscribe(syncPreferences);
1376
- return () => {
1377
- unsubscribe();
1378
- detach();
1379
- };
1380
- }, "dsh-update-status: sidebar and channel preferences");
1381
- } catch {}
1382
- });
1718
+ }, "dsh-update-status: styles, settings channel and first status read");
1719
+ try {
1720
+ ctx.inject(["settingsScope"], (raw) => {
1721
+ try {
1722
+ const binder = binderOf(raw);
1723
+ if (binder === void 0) return;
1724
+ channel.setOfficial(scopeOf(binder.bind({ namespace: SETTINGS_NAMESPACE })));
1725
+ } catch {}
1726
+ });
1727
+ } catch {}
1728
+ try {
1729
+ ctx.inject(["remote.settings"], (raw) => {
1730
+ try {
1731
+ remoteSettings = settingsRemoteOf(raw) ?? remoteSettings;
1732
+ channel.refresh();
1733
+ const invalidations = settingsInvalidationsOf(ctx.get("remote"));
1734
+ if (invalidations !== void 0) own(invalidations(() => channel.reload()));
1735
+ } catch {}
1736
+ });
1737
+ } catch {}
1738
+ if (typeof ctx.on === "function") try {
1739
+ own(ctx.on("connection/reset", () => channel.reload()));
1740
+ } catch {}
1383
1741
  ctx.slots.inject("sidebar.brand.name", () => ctx.slots.register({
1384
1742
  name: "sidebar.brand.name",
1385
1743
  priority: -10