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/CHANGELOG.md +34 -0
- package/README.md +33 -6
- package/README.zh.md +33 -6
- package/docs/RELEASING.md +50 -16
- package/lib/client.js +446 -95
- package/lib/client.js.map +1 -1
- package/lib/index.js +57 -17
- package/package.json +14 -16
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.
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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:
|
|
457
|
-
tabIndex:
|
|
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:
|
|
462
|
-
onKeyDown
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1243
|
-
.dus-brand-deepseek{font-size:
|
|
1244
|
-
.dus-badge{align-items:center;
|
|
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{
|
|
1247
|
-
.dus-badge[data-update=true]{
|
|
1248
|
-
.dus-badge[data-error=true]{
|
|
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:
|
|
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-
|
|
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
|
|
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
|
-
|
|
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:
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|