dsh-update-status 0.1.4 → 0.1.6

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
@@ -42,9 +42,9 @@ window.__ModuleLoader__.load({
42
42
  * JSON-only contract shared by the Host and Web halves.
43
43
  *
44
44
  * The static package uses the authenticated Connection RPC channel because
45
- * `harness.handle` / `host.call` are dynamic-Cordis-only closure APIs in DSH
46
- * 0.1.5-rc.1. The endpoint vocabulary remains deliberately small and private
47
- * to this plugin channel.
45
+ * `harness.handle` / `host.call` are dynamic-Cordis-only closure APIs. The
46
+ * endpoint vocabulary remains deliberately small and private to this plugin
47
+ * channel.
48
48
  */
49
49
  const PLUGIN_ID$1 = "dsh-update-status";
50
50
  const PACKAGE_NAME = "@deepseek-ai/dsh";
@@ -88,6 +88,25 @@ window.__ModuleLoader__.load({
88
88
  return null;
89
89
  }
90
90
  //#endregion
91
+ //#region src/shared/visual-state.ts
92
+ /**
93
+ * Only a read the plugin could not complete is a chip-level problem.
94
+ *
95
+ * An advisory notice — a preview channel this bundle has not been verified against,
96
+ * say — leaves the chip surface alone and belongs in the panel. Repainting the brand
97
+ * row for a plugin-side bookkeeping fact is the surprise the breathing dot replaced:
98
+ * an operator who upgrades DSH ahead of this bundle must not watch the chip turn red.
99
+ *
100
+ * A Host older than `warningKind` sends no field at all; `undefined` then keeps the
101
+ * previous behaviour (any warning is a problem) rather than silently dropping a real
102
+ * failure on the floor.
103
+ */
104
+ function isChipProblem(status) {
105
+ if (status === null) return false;
106
+ if (status.warningKind === void 0) return status.warning !== null;
107
+ return status.warningKind === "failure";
108
+ }
109
+ //#endregion
91
110
  //#region src/client/i18n.ts
92
111
  const DICTIONARY = {
93
112
  "brand.status": {
@@ -418,7 +437,7 @@ window.__ModuleLoader__.load({
418
437
  }
419
438
  function visualState(status, loading, error) {
420
439
  if (loading) return "loading";
421
- if (error !== null || status?.warning !== null) return status?.hasUpdate === true ? "update" : "problem";
440
+ if (error !== null || isChipProblem(status)) return status?.hasUpdate === true ? "update" : "problem";
422
441
  return status?.hasUpdate === true ? "update" : "current";
423
442
  }
424
443
  function badgeLabel(status, loading, error) {
@@ -466,6 +485,7 @@ window.__ModuleLoader__.load({
466
485
  children: shortVersion(version)
467
486
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
468
487
  className: "dus-dot",
488
+ "data-state": state,
469
489
  "data-update": snapshot.status?.hasUpdate === true || void 0,
470
490
  "data-loading": snapshot.loading || void 0,
471
491
  "aria-hidden": "true"
@@ -494,6 +514,7 @@ window.__ModuleLoader__.load({
494
514
  children: "↟"
495
515
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
496
516
  className: "dus-dot dus-footer-dot",
517
+ "data-state": state,
497
518
  "data-update": snapshot.status?.hasUpdate === true || void 0,
498
519
  "data-loading": state === "loading" || void 0
499
520
  })]
@@ -941,6 +962,7 @@ window.__ModuleLoader__.load({
941
962
  const latestVersion = record.latestVersion === null ? null : stringOrNull(record.latestVersion);
942
963
  const checkedAt = record.checkedAt === null ? null : stringOrNull(record.checkedAt);
943
964
  const warning = record.warning === null ? null : stringOrNull(record.warning);
965
+ const warningKind = record.warningKind === "failure" || record.warningKind === "notice" ? record.warningKind : void 0;
944
966
  const publishedAt = record.publishedAt === null ? null : stringOrNull(record.publishedAt);
945
967
  if (record.latestVersion !== null && latestVersion === null || record.checkedAt !== null && checkedAt === null || record.warning !== null && warning === null || record.publishedAt !== null && publishedAt === null) return void 0;
946
968
  const channels = [];
@@ -964,6 +986,7 @@ window.__ModuleLoader__.load({
964
986
  cached: record.cached,
965
987
  checkedAt,
966
988
  warning,
989
+ warningKind,
967
990
  installKind: record.installKind,
968
991
  upgradeCommand: record.upgradeCommand,
969
992
  releaseUrl: record.releaseUrl,
@@ -1204,28 +1227,14 @@ window.__ModuleLoader__.load({
1204
1227
  for (const listener of this.listeners) listener();
1205
1228
  }
1206
1229
  };
1207
- /** The one namespace name used by Host registration and browser binding. */
1230
+ /**
1231
+ * The one settings namespace, which on DSH 0.1.7 IS the Loader entry id in
1232
+ * `cordis.patch.yml`: the Host half's volatile `Config` fields are projected as
1233
+ * that entry's form, and the browser half addresses the same id through
1234
+ * `ctx.configForms.get(...)`.
1235
+ */
1208
1236
  const SETTINGS_NAMESPACE = PLUGIN_ID$1;
1209
1237
  //#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
1238
  //#region src/client/settings/hostDirectScope.ts
1230
1239
  /** A refused namespace write; `code` keeps the Host's own diagnosis. */
1231
1240
  var SettingsWriteFailure = class extends Error {
@@ -1447,6 +1456,65 @@ window.__ModuleLoader__.load({
1447
1456
  };
1448
1457
  }
1449
1458
  //#endregion
1459
+ //#region src/client/settings/configFormScope.ts
1460
+ /**
1461
+ * Guard: read the `configForms` face off an injection payload.
1462
+ *
1463
+ * The literal service key is read inside a `try`, because an injected payload
1464
+ * proxy can refuse a member read (the failure this plugin already hit on the
1465
+ * dotted `remote.settings` parent). Absence and refusal both answer
1466
+ * `undefined`, which leaves the channel on its direct Host path.
1467
+ *
1468
+ * @param raw - the payload a `ctx.inject(['configForms'], …)` callback received.
1469
+ * @returns the binder face, or undefined when this page has no settings forms.
1470
+ */
1471
+ function configFormsOf(raw) {
1472
+ if (raw === null || typeof raw !== "object") return void 0;
1473
+ let face;
1474
+ try {
1475
+ face = raw["configForms"];
1476
+ } catch {
1477
+ return;
1478
+ }
1479
+ if (face === null || typeof face !== "object") return void 0;
1480
+ if (typeof face.get !== "function") return void 0;
1481
+ return face;
1482
+ }
1483
+ /**
1484
+ * Project one official config form onto the plugin's settings-scope contract.
1485
+ *
1486
+ * @param form - the shared form for the plugin's own Loader entry id.
1487
+ * @returns a scope the preferences store and the channel selection can use as-is.
1488
+ */
1489
+ function configFormScope(form) {
1490
+ return {
1491
+ getSnapshot() {
1492
+ const snapshot = form.getSnapshot();
1493
+ return {
1494
+ status: snapshot.status,
1495
+ value: snapshot.value,
1496
+ base: snapshot.base,
1497
+ user: snapshot.user,
1498
+ revision: snapshot.revision,
1499
+ writable: snapshot.writable,
1500
+ mode: snapshot.mode
1501
+ };
1502
+ },
1503
+ subscribe(listener) {
1504
+ return form.subscribe(listener);
1505
+ },
1506
+ async set(field, value) {
1507
+ let accepted;
1508
+ try {
1509
+ accepted = await form.set(field, value);
1510
+ } catch (error) {
1511
+ throw new SettingsWriteFailure("settings/unreachable", error instanceof Error ? error.message : String(error));
1512
+ }
1513
+ if (!accepted) throw new SettingsWriteFailure("settings/conflict");
1514
+ }
1515
+ };
1516
+ }
1517
+ //#endregion
1450
1518
  //#region src/client/settings/settingsChannel.ts
1451
1519
  /** Snapshot used while no channel has answered yet. */
1452
1520
  const LOADING_SNAPSHOT = Object.freeze({
@@ -1561,17 +1629,55 @@ window.__ModuleLoader__.load({
1561
1629
  const UPDATE_STATUS_CSS = `
1562
1630
  .dus-brand-name{align-items:center;gap:6px;height:24px;max-width:100%;min-width:0;display:inline-flex}
1563
1631
  .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}
1564
- .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}
1632
+ /* The chip is a NEUTRAL SECOND-LEVEL SURFACE, never a high-contrast pill. It used
1633
+ to be label-primary + label-primary-inverted, which is a near-white box with dark
1634
+ text on the dark shell — a white frame that swallows the amber halo and fights the
1635
+ warn colour. --dsw-alias-button-floating-hover is the one palette surface that is
1636
+ grey in BOTH themes (#f1f3f5 light / #353638 dark), matching the reference: a light
1637
+ grey chip on the light shell, a dark grey chip with white text on the dark one. The
1638
+ sibling button-floating-fill is pure white in light mode and changes nothing there.
1639
+ Text stays label-primary (#0f1115 light / #f9fafb dark), so it is never low-contrast.
1640
+ Every colour here is a theme token, so the chip follows whatever appearance DSH
1641
+ resolves — light, dark, or system-follows-the-OS — with no plugin-side detection,
1642
+ no media query and no per-theme branch to keep in sync. */
1643
+ .dus-badge{--dus-badge-surface:var(--dsw-alias-button-floating-hover,#f1f3f5);align-items:center;background:var(--dus-badge-surface);border:0;border-radius:4px;color:var(--dsw-alias-label-primary,#0f1115);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}
1565
1644
  .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}
1645
+ /* Hover has to read as "raised" in BOTH themes. brightness() only ever brightens, and on
1646
+ the light shell it turns the grey chip pure white — the chip all but disappears. Mixing
1647
+ a little of the theme's own label colour into the surface darkens it in light mode and
1648
+ lightens it in dark mode, so one declaration stays correct in both. The brightness
1649
+ filter remains the fallback: an engine without color-mix drops only the second
1650
+ declaration, never the whole hover rule. */
1566
1651
  .dus-badge:hover{filter:brightness(1.08)}
1567
- .dus-badge[data-update=true]{background:var(--dsw-alias-state-warn-primary,#d97706);color:#111}
1568
- .dus-badge[data-error=true]{background:var(--dsw-alias-state-error-primary,#dc2626);color:#fff}
1652
+ @supports (background:color-mix(in srgb,red 50%,transparent)){.dus-badge:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,var(--dus-badge-surface));filter:none}}
1653
+ /* The failure state is the one deliberate repaint, and it follows the shell's own
1654
+ badge pattern (brokenBadge): error fill with a bg-layer-3 label, which is white
1655
+ text on the dark red of the light theme and dark text on the light red of the dark
1656
+ theme — readable in both, unlike the fixed white text it replaces. */
1657
+ .dus-badge[data-error=true]{background:var(--dsw-alias-state-error-primary,#dc2626);color:var(--dsw-alias-bg-layer-3,#fff)}
1569
1658
  .dus-badge-version{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1570
1659
  .dus-dot{background:currentColor;border-radius:50%;display:inline-block;flex:0 0 auto;height:5px;opacity:.9;width:5px}
1571
- .dus-dot[data-update=true]{animation:dus-pulse 1.5s ease-in-out infinite;background:var(--dsw-alias-state-warn-primary)}
1660
+ /* An available update repaints NOTHING: the chip keeps its normal fill and only
1661
+ the amber dot breathes. The halo is animated with the dot so the glow grows
1662
+ and shrinks as one light, never as a separate ripple. The update state is
1663
+ declared AFTER the loading state, so a re-check on a known update keeps the
1664
+ amber glow instead of falling back to the neutral loading pulse.
1665
+ Each box-shadow is declared twice: a literal amber first for any engine
1666
+ without color-mix, then the halo mixed from the theme's own warn colour so the
1667
+ glow always matches the dot in both light and dark themes. */
1668
+ /* The dot carries the whole status. Three states, in precedence order:
1669
+ up to date = a plain green circle (success token, never the text colour, so a
1670
+ dark theme no longer paints a black dot that reads as "off"); checking = the
1671
+ neutral grey pulse; a pending update = the amber dot below, which is the only
1672
+ state allowed to breathe or glow.
1673
+ The up-to-date rule is declared BEFORE loading and update: equal specificity, so
1674
+ the later, louder states win if two ever land on the same dot. */
1675
+ .dus-dot[data-state=current]{background:var(--dsw-alias-state-success-primary,#22c55e);opacity:1}
1572
1676
  .dus-dot[data-loading=true]{animation:dus-pulse .9s ease-in-out infinite}
1677
+ .dus-dot[data-update=true]{--dus-update-color:var(--dsw-alias-state-warn-primary,#f59e0b);animation:dus-update-pulse 1.6s ease-in-out infinite;background:var(--dus-update-color);height:6px;opacity:1;width:6px}
1573
1678
  @keyframes dus-pulse{0%,100%{opacity:.45;transform:scale(.82)}50%{opacity:1;transform:scale(1.15)}}
1574
- @media (prefers-reduced-motion:reduce){.dus-dot[data-update=true],.dus-dot[data-loading=true]{animation:none}}
1679
+ @keyframes dus-update-pulse{0%,100%{box-shadow:0 0 2px 0 rgba(245,158,11,.4);box-shadow:0 0 2px 0 color-mix(in srgb,var(--dus-update-color) 45%,transparent);transform:scale(.8)}50%{box-shadow:0 0 7px 2px rgba(245,158,11,.72);box-shadow:0 0 7px 2px color-mix(in srgb,var(--dus-update-color) 72%,transparent);transform:scale(1.15)}}
1680
+ @media (prefers-reduced-motion:reduce){.dus-dot[data-update=true],.dus-dot[data-loading=true]{animation:none}.dus-dot[data-update=true]{box-shadow:0 0 5px 1px rgba(245,158,11,.6);box-shadow:0 0 5px 1px color-mix(in srgb,var(--dus-update-color) 62%,transparent)}}
1575
1681
  .dus-footer-button{align-items:center;background:transparent;border:0;border-radius:10px;color:var(--dsw-alias-label-secondary);cursor:pointer;display:flex;height:44px;justify-content:center;min-height:44px;min-width:44px;padding:0;position:relative;touch-action:manipulation;width:44px}
1576
1682
  .dus-footer-button:hover{background:var(--dsw-alias-button-floating-hover);color:var(--dsw-alias-label-primary)}
1577
1683
  .dus-footer-icon{font-size:18px;line-height:1}
@@ -1717,11 +1823,11 @@ window.__ModuleLoader__.load({
1717
1823
  };
1718
1824
  }, "dsh-update-status: styles, settings channel and first status read");
1719
1825
  try {
1720
- ctx.inject(["settingsScope"], (raw) => {
1826
+ ctx.inject(["configForms"], (raw) => {
1721
1827
  try {
1722
- const binder = binderOf(raw);
1723
- if (binder === void 0) return;
1724
- channel.setOfficial(scopeOf(binder.bind({ namespace: SETTINGS_NAMESPACE })));
1828
+ const forms = configFormsOf(raw);
1829
+ if (forms === void 0) return;
1830
+ channel.setOfficial(configFormScope(forms.get(SETTINGS_NAMESPACE)));
1725
1831
  } catch {}
1726
1832
  });
1727
1833
  } catch {}