@huaqiu/dsh-tool-plugin-update 0.6.4 → 0.6.5
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 +106 -2
- package/lib/client.js.map +1 -1
- package/package.json +3 -3
- package/src/client/api.ts +9 -2
- package/src/client/version.ts +151 -0
package/lib/client.js
CHANGED
|
@@ -244,6 +244,110 @@ window.__ModuleLoader__.load({
|
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
//#endregion
|
|
247
|
+
//#region src/client/version.ts
|
|
248
|
+
/** Strict SemVer 2.0.0: MAJOR.MINOR.PATCH[-prerelease][+build]. */
|
|
249
|
+
const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
250
|
+
/** Parse a strict SemVer string, or null when it is not one. */
|
|
251
|
+
function parseSemver(version) {
|
|
252
|
+
const match = SEMVER_RE.exec(version.trim());
|
|
253
|
+
if (match === null) return null;
|
|
254
|
+
return {
|
|
255
|
+
major: Number(match[1]),
|
|
256
|
+
minor: Number(match[2]),
|
|
257
|
+
patch: Number(match[3]),
|
|
258
|
+
prerelease: match[4] === void 0 ? [] : match[4].split(".")
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** True when one prerelease identifier is purely numeric. */
|
|
262
|
+
function isNumericIdentifier(id) {
|
|
263
|
+
return /^\d+$/.test(id);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Compare two prerelease identifier lists per SemVer §11:
|
|
267
|
+
* numeric identifiers compare numerically and rank below alphanumeric ones;
|
|
268
|
+
* a larger set of identifiers ranks higher when all shared ones are equal.
|
|
269
|
+
*/
|
|
270
|
+
function comparePrerelease(a, b) {
|
|
271
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
272
|
+
if (a.length === 0) return 1;
|
|
273
|
+
if (b.length === 0) return -1;
|
|
274
|
+
const shared = Math.min(a.length, b.length);
|
|
275
|
+
for (let index = 0; index < shared; index += 1) {
|
|
276
|
+
const left = a[index];
|
|
277
|
+
const right = b[index];
|
|
278
|
+
if (left === void 0 || right === void 0) continue;
|
|
279
|
+
if (left === right) continue;
|
|
280
|
+
const leftNumeric = isNumericIdentifier(left);
|
|
281
|
+
const rightNumeric = isNumericIdentifier(right);
|
|
282
|
+
if (leftNumeric && rightNumeric) return Number(left) < Number(right) ? -1 : 1;
|
|
283
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
284
|
+
return left < right ? -1 : 1;
|
|
285
|
+
}
|
|
286
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Compare two strict SemVer strings: negative when `a < b`, positive when
|
|
290
|
+
* `a > b`, 0 when equal, and null when either side is not SemVer (build
|
|
291
|
+
* metadata is ignored in precedence, per spec).
|
|
292
|
+
*/
|
|
293
|
+
function compareSemver(a, b) {
|
|
294
|
+
const left = parseSemver(a);
|
|
295
|
+
const right = parseSemver(b);
|
|
296
|
+
if (left === null || right === null) return null;
|
|
297
|
+
if (left.major !== right.major) return left.major < right.major ? -1 : 1;
|
|
298
|
+
if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
|
|
299
|
+
if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
|
|
300
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Whether the offered release is an actionable upgrade over the installed
|
|
304
|
+
* version: an absent plugin (nothing installed) is always installable, and
|
|
305
|
+
* an installed one only when the offered SemVer is strictly greater. An
|
|
306
|
+
* unprovable direction (non-SemVer on either side) is NOT an upgrade.
|
|
307
|
+
*/
|
|
308
|
+
function isVersionUpgrade(currentVersion, availableVersion) {
|
|
309
|
+
if (currentVersion === null) return true;
|
|
310
|
+
const order = compareSemver(availableVersion, currentVersion);
|
|
311
|
+
return order !== null && order > 0;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Re-evaluate one report item against the direction rule.
|
|
315
|
+
*
|
|
316
|
+
* A server-reported `update` that is not a strict SemVer upgrade is demoted
|
|
317
|
+
* to `current`: the plugin is at (or ahead of) the offered release, there is
|
|
318
|
+
* nothing to install, and no install control may be offered for it. Every
|
|
319
|
+
* other state passes through untouched — `install` (plugin absent locally)
|
|
320
|
+
* has no local version to compare, and `blocked`/`ignored`/`current` are
|
|
321
|
+
* already non-actionable. Unknown server fields are preserved verbatim: the
|
|
322
|
+
* client stays a thin reader of the hq-edge report shape.
|
|
323
|
+
*/
|
|
324
|
+
function normalizePluginUpdateItem(item) {
|
|
325
|
+
if (item.state !== "update") return item;
|
|
326
|
+
if (isVersionUpgrade(item.currentVersion, item.availableVersion)) return item;
|
|
327
|
+
return {
|
|
328
|
+
...item,
|
|
329
|
+
state: "current"
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/** States that carry an install action (the badge/actionable definition). */
|
|
333
|
+
function isActionableState(item) {
|
|
334
|
+
return item.state === "update" || item.state === "install";
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Apply {@link normalizePluginUpdateItem} to a whole report and recompute
|
|
338
|
+
* `actionableCount` from the corrected states, so the server's count can
|
|
339
|
+
* never disagree with what the panel and the sidebar badge render.
|
|
340
|
+
*/
|
|
341
|
+
function normalizePluginUpdateReport(report) {
|
|
342
|
+
if (!Array.isArray(report.items)) return report;
|
|
343
|
+
const items = report.items.map(normalizePluginUpdateItem);
|
|
344
|
+
return {
|
|
345
|
+
...report,
|
|
346
|
+
items,
|
|
347
|
+
actionableCount: items.filter(isActionableState).length
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
247
351
|
//#region src/client/api.ts
|
|
248
352
|
/**
|
|
249
353
|
* Plugin-update API client (browser half).
|
|
@@ -290,11 +394,11 @@ window.__ModuleLoader__.load({
|
|
|
290
394
|
}
|
|
291
395
|
/** GET the cached (or empty-shell) plugin-update report. */
|
|
292
396
|
function fetchReport() {
|
|
293
|
-
return request("/report");
|
|
397
|
+
return request("/report").then(normalizePluginUpdateReport);
|
|
294
398
|
}
|
|
295
399
|
/** POST an explicit refresh; returns the freshly computed report. */
|
|
296
400
|
function checkUpdates() {
|
|
297
|
-
return request("/check", { method: "POST" });
|
|
401
|
+
return request("/check", { method: "POST" }).then(normalizePluginUpdateReport);
|
|
298
402
|
}
|
|
299
403
|
/** POST the install transaction for the confirmed set version. */
|
|
300
404
|
function installSet(setVersion) {
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../dsh-sidebar-entry/lib/index.mjs","../src/client/api.ts","../src/client/copy.ts","../src/client/icons.ts","../src/client/update-view.ts","../src/client/index.ts"],"sourcesContent":["//#region src/index.ts\n/** Find the sidebar shell root element, or undefined while not yet mounted. */\nfunction sidebarRoot() {\n\tconst column = document.querySelector(\"[data-pane=\\\"sidebar\\\"], [class*=\\\"sidebarCol\\\"]\");\n\tif (column === null) return void 0;\n\treturn column.querySelector(\"[class*=\\\"logoRow\\\"]\")?.parentElement ?? column.firstElementChild;\n}\n/** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */\nfunction newSessionButton(root) {\n\tconst nested = root.querySelector(\"button[class*=\\\"newSession\\\"]\");\n\tif (nested !== null) return nested;\n\tfor (const child of root.children) if (child.tagName === \"BUTTON\") return child;\n}\n/** Build the entry row (a detached button; insert once the shell is up). */\nfunction createEntry(options) {\n\tconst entry = document.createElement(\"button\");\n\tentry.type = \"button\";\n\tentry.setAttribute(options.rowAttribute, \"\");\n\tif (options.plugin !== void 0) {\n\t\tentry.setAttribute(\"data-dsh-plugin\", options.plugin);\n\t\tentry.setAttribute(\"data-dsh-part\", \"sidebar-entry\");\n\t}\n\tentry.className = options.css[\"entry\"] ?? \"\";\n\tconst labelSpan = document.createElement(\"span\");\n\tlabelSpan.className = options.css[\"entryLabel\"] ?? \"\";\n\tconst iconSpan = document.createElement(\"span\");\n\ticonSpan.className = options.css[\"entryIcon\"] ?? \"\";\n\ticonSpan.innerHTML = options.icon;\n\tentry.append(iconSpan, labelSpan);\n\tlet badgeSpan = null;\n\tconst applyBadge = () => {\n\t\tif (options.badge === void 0) return;\n\t\tconst count = options.badge.count();\n\t\tif (count <= 0) {\n\t\t\tbadgeSpan?.remove();\n\t\t\tbadgeSpan = null;\n\t\t\treturn;\n\t\t}\n\t\tif (badgeSpan === null) {\n\t\t\tbadgeSpan = document.createElement(\"span\");\n\t\t\tbadgeSpan.className = options.css[\"entryBadge\"] ?? \"\";\n\t\t\tentry.append(badgeSpan);\n\t\t}\n\t\tbadgeSpan.textContent = String(count);\n\t\tentry.setAttribute(\"data-has-badge\", \"\");\n\t};\n\tconst applyLabel = () => {\n\t\tentry.setAttribute(\"aria-label\", options.label());\n\t\tif (options.tooltip !== void 0) entry.setAttribute(\"title\", options.tooltip());\n\t\tlabelSpan.textContent = options.label();\n\t};\n\tapplyLabel();\n\tapplyBadge();\n\tentry.addEventListener(\"click\", options.onToggle);\n\treturn {\n\t\tentry,\n\t\tapplyLabel,\n\t\tapplyBadge\n\t};\n}\n/**\n* Re-insert the entry after the New Session row, in the family's intended\n* order.\n*\n* The `familySelectors` array order is the single source of truth for visual\n* order, so the rendered order is stable regardless of which plugin mounts\n* first or of observer callback order — a re-render can never swap two rows.\n* (Earlier builds used the `position: 'before' | 'after'` convention, which\n* clustered rows by mount order and let a late-mounting `before` row steal the\n* lead. The array order supersedes it; `position` remains only as a fallback\n* for a row whose selector is absent from the family list.)\n*/\nfunction placeEntry(root, entry, options) {\n\tconst button = newSessionButton(root);\n\tif (button === void 0) return false;\n\tif (entry.parentElement !== root) {\n\t\tconst row = button.closest(\"[class*=\\\"logoRow\\\"]\");\n\t\tconst base = row !== null && row.parentElement === root ? row : button;\n\t\tconst order = options.familySelectors;\n\t\tconst present = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(order.join(\", \"))).map((el) => ({\n\t\t\tel,\n\t\t\tidx: order.findIndex((sel) => el.matches(sel))\n\t\t}));\n\t\tconst selfIndex = order.indexOf(options.rowSelector);\n\t\tlet anchor;\n\t\tif (selfIndex === -1) anchor = options.position === \"before\" ? present.length > 0 ? present[0].el : base.nextElementSibling : present.length > 0 ? present[present.length - 1].el.nextElementSibling : base.nextElementSibling;\n\t\telse {\n\t\t\tconst later = present.find((member) => member.idx > selfIndex);\n\t\t\tif (later !== void 0) anchor = later.el;\n\t\t\telse {\n\t\t\t\tconst earlier = [...present].reverse().find((member) => member.idx < selfIndex);\n\t\t\t\tanchor = earlier !== void 0 ? earlier.el.nextElementSibling : base.nextElementSibling;\n\t\t\t}\n\t\t}\n\t\troot.insertBefore(entry, anchor ?? null);\n\t}\n\treturn true;\n}\n/**\n* Mount the sidebar entry, waiting for the shell to render and self-healing\n* on later React re-renders.\n* @param options - the row's attribute/icon/copy/action/ordering configuration.\n* @returns disposer removing the entry and its observers.\n*/\nfunction mountSidebarEntry(options) {\n\tif (typeof document !== \"undefined\" && document.querySelector(options.rowSelector) !== null) return () => {};\n\tconst { entry, applyLabel, applyBadge } = createEntry(options);\n\tlet root;\n\tlet placed = false;\n\tlet unsubscribeRefresh;\n\tif (options.refresh !== void 0) try {\n\t\tunsubscribeRefresh = options.refresh.subscribe(applyLabel);\n\t} catch {}\n\tlet unsubscribeBadge;\n\tif (options.badge !== void 0) try {\n\t\tunsubscribeBadge = options.badge.subscribe(applyBadge);\n\t} catch {}\n\tconst tryPlace = () => {\n\t\tif (root !== void 0 && !root.isConnected) {\n\t\t\trootObserver.disconnect();\n\t\t\troot = void 0;\n\t\t\tplaced = false;\n\t\t}\n\t\tif (placed) {\n\t\t\tif (document.body.contains(entry)) return;\n\t\t\trootObserver.disconnect();\n\t\t\troot = void 0;\n\t\t\tplaced = false;\n\t\t}\n\t\troot ??= sidebarRoot();\n\t\tif (root === void 0) return;\n\t\tplaced = placeEntry(root, entry, options);\n\t\tif (placed) rootObserver.observe(root, {\n\t\t\tchildList: true,\n\t\t\tsubtree: true\n\t\t});\n\t};\n\tconst waitObserver = new MutationObserver(() => {\n\t\ttryPlace();\n\t});\n\twaitObserver.observe(document.body, {\n\t\tchildList: true,\n\t\tsubtree: true\n\t});\n\tconst rootObserver = new MutationObserver(() => {\n\t\tif (root === void 0 || !root.isConnected) {\n\t\t\tplaced = false;\n\t\t\ttryPlace();\n\t\t\treturn;\n\t\t}\n\t\tif (!root.contains(entry)) placed = placeEntry(root, entry, options);\n\t});\n\toptions.active === void 0 || (() => {\n\t\tconst syncActive = () => {\n\t\t\tif (options.active.isOpen()) entry.dataset.active = \"true\";\n\t\t\telse delete entry.dataset.active;\n\t\t};\n\t\tconst unsubscribe = options.active.subscribe(syncActive);\n\t\tsyncActive();\n\t\treturn unsubscribe;\n\t})();\n\ttryPlace();\n\treturn () => {\n\t\twaitObserver.disconnect();\n\t\trootObserver.disconnect();\n\t\tunsubscribeRefresh?.();\n\t\tunsubscribeBadge?.();\n\t\tentry.remove();\n\t};\n}\n/**\n* Every row selector in the Huaqiu sidebar family, in intended visual order.\n*\n* Family ordering is only stable when every participating plugin passes the\n* SAME list to {@link mountSidebarEntry} — the core positions a row relative to\n* the family block, never relative to transient shell geometry. Adding a new\n* family row means adding its selector here (and to no plugin-local list).\n*/\nconst HUAQIU_SIDEBAR_ENTRY_FAMILY = [\n\t\"[data-hq-plugin-update-entry]\",\n\t\"[data-hqcg-footprint-entry]\",\n\t\"[data-hqcg-symbol-entry]\",\n\t\"[data-hqcg-schematic-entry]\",\n\t\"[data-hq-dummy-entry]\"\n];\n/**\n* Read the host UI language from `<html lang>`.\n*\n* `dsh-client-locale` rewrites the attribute on every language change (dsh\n* web) and HQ Edge mirrors it, so this is the one source every family plugin\n* can read without importing a `locale` service.\n* @returns 'zh' / 'en', or undefined when the tag is absent or unsupported\n* (callers fall back to their own default).\n*/\nfunction detectHostLocale() {\n\tif (typeof document === \"undefined\") return void 0;\n\tconst tag = document.documentElement?.getAttribute(\"lang\");\n\tif (tag === null || tag === void 0 || tag === \"\") return void 0;\n\tconst primary = tag.toLowerCase().split(\"-\")[0];\n\treturn primary === \"zh\" || primary === \"en\" ? primary : void 0;\n}\n/**\n* Watch `<html lang>` and report every resolved change.\n*\n* Idempotent-safe: repeated calls share one observer and each caller gets its\n* own disposer; the observer disconnects when the last listener leaves.\n* @param listener - called with the newly resolved language.\n* @returns disposer removing this listener.\n*/\n/** Shared observer/listener set backing {@link observeHostLocale}. */\nconst localeListeners = /* @__PURE__ */ new Set();\nlet localeObserver = null;\nfunction observeHostLocale(listener) {\n\tif (typeof document === \"undefined\" || typeof MutationObserver === \"undefined\" || document.documentElement === null) return () => {};\n\tlet current = detectHostLocale();\n\tconst listeners = localeListeners;\n\tlisteners.add(listener);\n\tif (localeObserver === null) {\n\t\tlocaleObserver = new MutationObserver(() => {\n\t\t\tconst next = detectHostLocale();\n\t\t\tif (next === current) return;\n\t\t\tcurrent = next;\n\t\t\tfor (const fn of [...listeners]) try {\n\t\t\t\tfn(next);\n\t\t\t} catch {}\n\t\t});\n\t\tlocaleObserver.observe(document.documentElement, {\n\t\t\tattributes: true,\n\t\t\tattributeFilter: [\"lang\"]\n\t\t});\n\t}\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t\tif (listeners.size === 0 && localeObserver !== null) {\n\t\t\tlocaleObserver.disconnect();\n\t\t\tlocaleObserver = null;\n\t\t}\n\t};\n}\n//#endregion\nexport { HUAQIU_SIDEBAR_ENTRY_FAMILY, detectHostLocale, mountSidebarEntry, observeHostLocale };\n","/**\n * Plugin-update API client (browser half).\n *\n * Every call is a same-origin fetch against the DSH webServer prefix route\n * (`/api/v1/huaqiu/plugin-update/…`) which the node half forwards to hq-edge.\n * Response shapes mirror hq-edge `apps/server/src/routes/plugin-updates.ts`.\n *\n * @module\n */\n\nexport const UPDATE_BASE = '/api/v1/huaqiu/plugin-update'\n\nexport interface PluginUpdateItem {\n name: string\n /** Version installed in the active bundle; null when the plugin is absent. */\n currentVersion: string | null\n availableVersion: string\n state: 'current' | 'update' | 'install' | 'blocked' | 'ignored'\n /** The block reason, or null when the item is not blocked. The server sends a\n * single code (`\"runtime-unknown\"`), so it is normalized before use. */\n blockedBy?: string | string[] | null\n ignored?: boolean\n reasons?: string[]\n notes?: string[]\n}\n\n/**\n * One downloadable HQ Edge runtime distribution, as hq-edge reports it\n * (`RuntimeUpdateEntry` in `apps/server/src/dsh/PluginUpdateService.ts`).\n *\n * Note the shape: `runtime.latest` is this ENTRY OBJECT, not a bare version\n * string. Reading it as a string is what once rendered \"[object Object]\".\n */\nexport interface RuntimeUpdateEntry {\n /** Clean SemVer of the available distribution. */\n version: string\n /** SHA-256 of the release archive, lowercase hex (64 chars). */\n sha256: string\n /** Platform the archive targets: win | darwin | linux | all. */\n platform: string\n /** Optional CPU architecture: x64 | arm64 (absent = any). */\n arch?: string\n /** Exact absolute HTTPS URL of the release archive. */\n url: string\n}\n\nexport interface RuntimeUpdateInfo {\n configured: boolean\n currentVersion?: string | null\n /** The latest compatible distribution, or null when the manifest had none. */\n latest?: RuntimeUpdateEntry | null\n updateAvailable: boolean\n checkedAt?: string | null\n lastError?: string | null\n /**\n * Where the runtime-update manifest is read from. Optional: an older hq-edge\n * does not send it, and the panel must degrade to a read-only source row.\n */\n source?: RuntimeUpdateSource\n}\n\n/**\n * The runtime-update manifest source, editable from this panel.\n *\n * `origin` says which URL is in effect: `override` is a URL the user set here\n * (persisted by hq-edge per host), `config` is the packaged/env default, and\n * `unset` means runtime updates are not configured at all.\n */\nexport interface RuntimeUpdateSource {\n /** The manifest URL currently in effect; empty when none is known. */\n url: string\n origin: 'override' | 'config' | 'unset'\n /** The URL configuration supplies, independent of any user override. */\n configuredUrl: string\n /** True when a user override is shadowing the configured URL. */\n override: boolean\n}\n\n/**\n * The plugin release-manifest source (the gitee\n * `plugins-release-manifest.json` by default) — same shape and same edit flow\n * as {@link RuntimeUpdateSource}, because hq-edge serves both from one shared\n * type.\n */\nexport type PluginUpdateSource = RuntimeUpdateSource\n\n/**\n * The latest runtime version to display, or null when there is none.\n *\n * hq-edge sends the full release entry; an older or future server could send\n * a bare string. Both are accepted here so the facts row can never again fall\n * back to `String(object)` — i.e. \"[object Object]\" — in the DOM.\n */\nexport function runtimeLatestVersion(\n runtime: RuntimeUpdateInfo | null | undefined,\n): string | null {\n const latest: unknown = runtime?.latest\n if (typeof latest === 'string') return latest.length > 0 ? latest : null\n if (latest !== null && typeof latest === 'object' && 'version' in latest) {\n const version = (latest as { version?: unknown }).version\n if (typeof version === 'string' && version.length > 0) return version\n }\n return null\n}\n\nexport interface PluginUpdateReport {\n /** The evaluated release set; null while no manifest has been read. */\n manifestVersion: string | null\n pluginSetVersion?: string\n items: PluginUpdateItem[]\n actionableCount: number\n ignoredSet: boolean\n fetchedAt?: string\n lastError?: string | null\n runtime: RuntimeUpdateInfo\n /**\n * Where the plugin release manifest is read from. Optional: an older\n * hq-edge does not send it, and the panel must degrade to no source row.\n */\n pluginSource?: PluginUpdateSource\n}\n\nasync function request<T>(path: string, init?: RequestInit): Promise<T> {\n const res = await fetch(`${UPDATE_BASE}${path}`, {\n cache: 'no-store',\n ...init,\n headers: { 'content-type': 'application/json; charset=utf-8', ...(init?.headers ?? {}) },\n })\n const body = (await res.json().catch(() => null)) as T | null\n if (!res.ok) {\n const err = (body as { error?: string } | null)?.error ?? `HTTP ${res.status}`\n throw new Error(err)\n }\n if (body === null) throw new Error('empty response')\n return body\n}\n\n/** GET the cached (or empty-shell) plugin-update report. */\nexport function fetchReport(): Promise<PluginUpdateReport> {\n return request<PluginUpdateReport>('/report')\n}\n\n/** POST an explicit refresh; returns the freshly computed report. */\nexport function checkUpdates(): Promise<PluginUpdateReport> {\n return request<PluginUpdateReport>('/check', { method: 'POST' })\n}\n\nexport interface InstallResult {\n ok: boolean\n installedSetVersion?: string\n installed?: number\n skipped?: number\n restarting?: boolean\n error?: string\n}\n\n/** POST the install transaction for the confirmed set version. */\nexport function installSet(setVersion: string): Promise<InstallResult> {\n return request<InstallResult>('/install', {\n method: 'POST',\n body: JSON.stringify({ setVersion }),\n })\n}\n\n/** POST ignore for the current offer (set-scoped, persists across restarts). */\nexport function ignoreOffer(setVersion: string): Promise<unknown> {\n return request<unknown>('/ignore', { method: 'POST', body: JSON.stringify({ setVersion }) })\n}\n\n/**\n * The outcome of a user-confirmed HQ Edge runtime update.\n *\n * hq-edge answers `202 { requestId, targetVersion, state: 'accepted' }` and\n * then shuts itself down for the EDA host to swap the runtime, so a success\n * here means \"accepted\", not \"installed\". Failures arrive as 409 (already\n * updating → `busy`), 422 (preflight/manifest/download refused) or 503.\n */\nexport interface RuntimeUpdateRequestResult {\n requestId?: string\n targetVersion?: string\n state?: string\n ok?: boolean\n error?: string\n busy?: boolean\n}\n\n/** POST the user-confirmed HQ Edge runtime update for one target version. */\nexport function requestRuntimeUpdate(\n targetVersion: string,\n): Promise<RuntimeUpdateRequestResult> {\n return request<RuntimeUpdateRequestResult>('/runtime-request', {\n method: 'POST',\n body: JSON.stringify({ targetVersion }),\n })\n}\n\n/** POST unignore for the set version (reverses an ignore). */\nexport function unignoreOffer(setVersion: string): Promise<unknown> {\n return request<unknown>('/unignore', { method: 'POST', body: JSON.stringify({ setVersion }) })\n}\n\n/**\n * GET the runtime-update manifest source: the URL in effect, its origin\n * (`override` | `config` | `unset`) and the configured URL behind it.\n *\n * Read-only upstream, so this is open like `/report` — only the mutation\n * below is loopback-guarded (and reaches it same-origin anyway).\n */\nexport function getRuntimeUpdateSource(): Promise<RuntimeUpdateSource> {\n return request<RuntimeUpdateSource>('/runtime-source')\n}\n\n/**\n * POST a new runtime-update manifest URL, or `\"\"` to clear a user override\n * and fall back to the configured URL.\n *\n * hq-edge validates the URL itself (`https` required except for loopback, no\n * embedded credentials) and answers 422 with a message meant for a person —\n * this client surfaces it verbatim rather than pre-validating, so the rule\n * lives in exactly one place. A successful answer is the NEW source; the\n * caller must then re-run discovery, because the cached report still names\n * the old manifest's offer.\n */\nexport function setRuntimeUpdateSource(url: string): Promise<RuntimeUpdateSource> {\n return request<RuntimeUpdateSource>('/runtime-source', {\n method: 'POST',\n body: JSON.stringify({ url }),\n })\n}\n\n/**\n * GET the plugin release-manifest source: the URL discovery reads, its origin\n * (`override` | `config` | `unset`) and the configured URL behind it. Same\n * open/read-only contract as the runtime source.\n */\nexport function getPluginUpdateSource(): Promise<PluginUpdateSource> {\n return request<PluginUpdateSource>('/plugin-source')\n}\n\n/**\n * POST a new plugin release-manifest URL, or `\"\"` to clear a user override.\n *\n * hq-edge persists the override per host AND re-runs discovery in the\n * background, so the next report (and the sidebar badge) already reflect the\n * new manifest. This client still re-checks explicitly after a save — the\n * background pass is a safety net for other consumers, not a substitute.\n */\nexport function setPluginUpdateSource(url: string): Promise<PluginUpdateSource> {\n return request<PluginUpdateSource>('/plugin-source', {\n method: 'POST',\n body: JSON.stringify({ url }),\n })\n}\n","/**\n * Localized copy for the plugin-update UI (browser half).\n *\n * zh is the fallback, matching the rest of the Huaqiu plugin family\n * (`<html lang>` drives the resolution via the shared sidebar-entry locale\n * helpers). The wording follows the shell's own plugin manager\n * (`ui-plugin-manager/src/client/locales.ts`) so both read as one product.\n *\n * @module\n */\n\nconst zh = {\n entryLabel: '插件更新',\n entryTooltip: '查看可用的插件更新',\n viewTitle: '插件更新',\n viewIntro: '检查并安装可用的插件更新',\n viewInfo: '在这里查看插件集的更新。标注为「被阻止」的插件因主机或 HQ Edge 版本不兼容而无法安装;已是最新的插件无需操作。',\n refresh: '刷新',\n close: '关闭',\n loading: '正在读取插件更新…',\n empty: '没有可用的插件更新',\n checkFailed: '检查更新失败,请重试',\n installAll: (count: number) => `安装 ${count} 项更新`,\n installOne: '安装',\n installing: '正在安装…(DSH 将自动重启)',\n nothingToInstall: '暂无可安装的更新',\n ignore: '忽略此版本',\n unignore: '恢复提醒',\n ignoredOffer: '已忽略当前插件集更新',\n groupCount: (count: number) => `${count} 个插件`,\n group: {\n actionable: '待处理',\n blocked: '无法安装',\n ignored: '已忽略',\n current: '已是最新',\n },\n state: {\n current: '当前',\n update: '可更新',\n install: '待安装',\n ignored: '已忽略',\n blocked: '被阻止',\n },\n blockedReason: {\n host: '不适用于当前 EDA 主机',\n 'hq-edge-version': '当前 HQ Edge 版本不兼容',\n 'runtime-unknown': 'HQ Edge 版本未知,无法校验',\n },\n runtime: {\n title: 'HQ Edge 运行时',\n current: '当前版本',\n latest: '最新版本',\n set: '发布插件集',\n checkedAt: '上次检查',\n source: '更新源',\n unknown: '版本未知',\n notConfigured: '未配置更新源',\n note: '运行时更新由 EDA 安装器处理',\n update: '安装运行时更新',\n updateConfirm: (version: string) =>\n `安装 HQ Edge 运行时 ${version}?HQ Edge 会先关闭并完成替换,然后重新启动。`,\n updating: '正在下载并校验运行时包,可能需要一分钟…',\n updateAccepted: '运行时更新已受理,HQ Edge 正在重启…',\n // ── Update-source editor ──\n sourceHint: '运行时更新清单的下载地址,保存后立即生效',\n sourceEdit: '修改',\n sourceSave: '保存',\n sourceReset: '恢复默认',\n saved: '更新源已保存,正在重新检查更新…',\n saveFailed: '保存更新源失败',\n sourceInputLabel: '运行时更新清单地址',\n sourcePlaceholder: 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json',\n },\n // ── Plugin release-manifest source editor ──\n pluginSource: {\n title: '插件更新源',\n hint: '插件更新清单的下载地址,保存后立即生效',\n edit: '修改',\n save: '保存',\n reset: '恢复默认',\n saved: '更新源已保存,正在重新检查更新…',\n saveFailed: '保存更新源失败',\n inputLabel: '插件更新清单地址',\n placeholder: 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json',\n },\n // Origin tags, shared by both source editors (hq-edge serves one type).\n originOverride: '自定义',\n originConfig: '默认配置',\n originUnset: '未配置',\n lastError: '上次检查失败',\n installError: '安装失败',\n installSuccess: '更新已安装,正在重启 DSH…',\n confirmInstall: '确认安装',\n cancel: '取消',\n}\n\nexport type UpdateCopy = typeof zh\n\nconst en: UpdateCopy = {\n entryLabel: 'Plugin Updates',\n entryTooltip: 'Review available plugin updates',\n viewTitle: 'Plugin Updates',\n viewIntro: 'Check for and install available plugin updates',\n viewInfo: 'Review the plugin set updates here. A plugin marked Blocked cannot be installed because its host or HQ Edge version is incompatible; a plugin marked Current needs no action.',\n refresh: 'Refresh',\n close: 'Close',\n loading: 'Reading plugin updates…',\n empty: 'No plugin updates available',\n checkFailed: 'Failed to check for updates — please retry',\n installAll: (count: number) => `Install ${count} update${count === 1 ? '' : 's'}`,\n installOne: 'Install',\n installing: 'Installing… (DSH will restart)',\n nothingToInstall: 'Nothing to install right now',\n ignore: 'Ignore this release',\n unignore: 'Show again',\n ignoredOffer: 'The current plugin-set update is ignored',\n groupCount: (count: number) => `${count} plugin${count === 1 ? '' : 's'}`,\n group: {\n actionable: 'Actionable',\n blocked: 'Blocked',\n ignored: 'Ignored',\n current: 'Up to date',\n },\n state: {\n current: 'Current',\n update: 'Update',\n install: 'Install',\n ignored: 'Ignored',\n blocked: 'Blocked',\n },\n blockedReason: {\n host: 'Not applicable to the current EDA host',\n 'hq-edge-version': 'Incompatible with the current HQ Edge version',\n 'runtime-unknown': 'HQ Edge version unknown — cannot verify',\n },\n runtime: {\n title: 'HQ Edge Runtime',\n current: 'Current version',\n latest: 'Latest version',\n set: 'Release set',\n checkedAt: 'Last checked',\n source: 'Update source',\n unknown: 'Version unknown',\n notConfigured: 'No update source configured',\n note: 'Runtime updates are handled by the EDA installer',\n update: 'Install runtime update',\n updateConfirm: (version: string) =>\n `Install HQ Edge runtime ${version}? HQ Edge shuts down to swap the runtime, then restarts.`,\n updating: 'Downloading and verifying the runtime bundle — this can take a minute…',\n updateAccepted: 'Runtime update accepted — HQ Edge is restarting…',\n // ── Update-source editor ──\n sourceHint: 'Where the runtime update manifest is fetched from; saved changes take effect immediately',\n sourceEdit: 'Edit',\n sourceSave: 'Save',\n sourceReset: 'Reset to default',\n saved: 'Update source saved — re-checking for updates…',\n saveFailed: 'Failed to save the update source',\n sourceInputLabel: 'Runtime update manifest URL',\n sourcePlaceholder: 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json',\n },\n // ── Plugin release-manifest source editor ──\n pluginSource: {\n title: 'Plugin update source',\n hint: 'Where the plugin update manifest is fetched from; saved changes take effect immediately',\n edit: 'Edit',\n save: 'Save',\n reset: 'Reset to default',\n saved: 'Update source saved — re-checking for updates…',\n saveFailed: 'Failed to save the update source',\n inputLabel: 'Plugin update manifest URL',\n placeholder:\n 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json',\n },\n // Origin tags, shared by both source editors (hq-edge serves one type).\n originOverride: 'Custom',\n originConfig: 'Configured',\n originUnset: 'Not configured',\n lastError: 'Last check failed',\n installError: 'Install failed',\n installSuccess: 'Update installed — restarting DSH…',\n confirmInstall: 'Confirm install',\n cancel: 'Cancel',\n}\n\nexport const COPY = { zh, en } as const\n","/**\n * Official DSH icon artwork, inlined as SVG strings.\n *\n * The paths are copied verbatim from `@deepseek-ai/dsh-client-ui-primitives`\n * (deepseek-harness `packages/client/ui-primitives/src/icons/index.tsx`) so the\n * Huaqiu plugin-update view draws the same glyphs as the shell's own plugin\n * manager. Only `currentColor` is used, so every glyph inherits the label ink\n * of whatever surface it sits on — the same contract the primitives rely on.\n *\n * The plugin renders plain DOM (never a React tree), so the artwork is exposed\n * as markup strings rather than components.\n *\n * @module\n */\n\n/** Stroke width of the primitives' \"Regular\" weight. */\nconst REGULAR = 1\n\n/** Stroke width of the primitives' \"Medium\" weight. */\nconst MEDIUM = 1.3\n\n/** Wrap paths in a 16×16 stroked glyph. */\nfunction glyph(size: number, strokeWidth: number, paths: readonly string[], viewBox = '0 0 16 16'): string {\n const body = paths.map((d) => `<path d=\"${d}\" stroke=\"currentColor\" />`).join('')\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"${viewBox}\" fill=\"none\" aria-hidden=\"true\" stroke-width=\"${strokeWidth}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${body}</svg>`\n}\n\n/** Wrap fill-only paths (the check glyph) in their own viewBox. */\nfunction fillGlyph(size: number, paths: readonly string[], viewBox: string): string {\n const body = paths.map((d) => `<path d=\"${d}\" fill=\"currentColor\" />`).join('')\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"${viewBox}\" fill=\"none\" aria-hidden=\"true\">${body}</svg>`\n}\n\n/**\n * The plugin pinwheel — the glyph the shell's own Plugins sidebar entry uses\n * (`IconPluginPinwheelOutlineRegular`). Shared by the sidebar row and the\n * package cards so both read as the same feature.\n */\nexport const PLUGIN_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M7.84457 5.06199C11.6605 4.93876 14.7962 6.14848 14.8484 7.76397C14.8875 8.97461 13.1838 10.0696 10.7215 10.5942',\n 'M5.12742 8.07731C5.00419 4.26138 6.21391 1.12568 7.8294 1.07351C9.04004 1.03441 10.135 2.73808 10.6596 5.20037',\n 'M8.02457 10.6802C4.20865 10.8034 1.07294 9.5937 1.02077 7.97821C0.981678 6.76758 2.68535 5.67262 5.14763 5.14798',\n 'M10.7476 7.89535C10.8708 11.7113 9.66109 14.847 8.0456 14.8991C6.83496 14.9382 5.74 13.2346 5.21536 10.7723',\n ])\n\n/** The toolbar's refresh action (`IconRefreshOutlineRegular`). */\nexport const REFRESH_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M14.5001 8C14.5 9.28552 14.1188 10.5422 13.4045 11.611C12.6903 12.6799 11.6752 13.5129 10.4875 14.0049C9.29982 14.4968 7.99295 14.6255 6.73212 14.3747C5.4713 14.124 4.31314 13.505 3.4041 12.596C2.49514 11.687 1.87614 10.5288 1.62537 9.26798C1.37459 8.00716 1.50331 6.70028 1.99525 5.51261C2.48719 4.32494 3.32025 3.30981 4.3891 2.59557C5.45795 1.88134 6.71458 1.50008 8.0001 1.5C9.9001 1.5 11.7001 2.3 13.0001 3.6L14.5001 5.1',\n 'M14.4999 1.5V5.1H10.8999',\n ])\n\n/** The panel's close action (`IconCloseOutlineMedium`). */\nexport const CLOSE_ICON = (size = 16): string =>\n glyph(size, MEDIUM, ['M2.5 2.5L13.5 13.5', 'M13.5 2.5L2.5 13.5'])\n\n/** The notice glyph (`IconWarningOutlineRegular`). */\nexport const WARNING_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z',\n 'M8 4.29199V9.79199',\n 'M8 10.708V11.708',\n ])\n\n/** The settled-state glyph (`IconCheckCircleFillRegular`). */\nexport const CHECK_CIRCLE_ICON = (size = 16): string =>\n fillGlyph(size, [\n 'M28.1936 14.6936L19.8066 23.0806C19.2373 23.65 18.7159 24.1742 18.24 24.5571C17.7389 24.9602 17.1365 25.3359 16.3657 25.458C15.9581 25.5225 15.5428 25.5225 15.1353 25.458C14.3645 25.3359 13.7621 24.9602 13.261 24.5571C12.7851 24.1742 12.2637 23.65 11.6943 23.0806L7.80737 19.1936L10.1936 16.8074L14.0806 20.6943C14.7033 21.317 15.0763 21.6873 15.377 21.9292C15.6523 22.1507 15.7109 22.1325 15.6626 22.1248C15.7208 22.1339 15.7802 22.1339 15.8384 22.1248C15.7901 22.1325 15.8486 22.1507 16.124 21.9292C16.4247 21.6873 16.7977 21.317 17.4204 20.6943L25.8074 12.3074L28.1936 14.6936Z',\n 'M32.8496 18.0005C32.8496 9.79906 26.2019 3.15137 18.0005 3.15137C9.79906 3.15137 3.15137 9.79906 3.15137 18.0005C3.15137 26.2019 9.79906 32.8496 18.0005 32.8496C26.2019 32.8496 32.8496 26.2019 32.8496 18.0005ZM35.7764 18.0005C35.7764 27.8173 27.8173 35.7764 18.0005 35.7764C8.18363 35.7764 0.224609 27.8173 0.224609 18.0005C0.224609 8.18363 8.18363 0.224609 18.0005 0.224609C27.8173 0.224609 35.7764 8.18363 35.7764 18.0005Z',\n ], '0 0 36 36')\n\n/** The install action (`IconDownloadOutlineRegular`). */\nexport const DOWNLOAD_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M8 1.95317V10.0469',\n 'M4.25 6.29688L8 10.0469L11.75 6.29688',\n 'M1.5 10.0469V13.158C1.5 13.3937 1.60536 13.6198 1.79289 13.7865C1.98043 13.9532 2.23478 14.0469 2.5 14.0469H13.5C13.7652 14.0469 14.0196 13.9532 14.2071 13.7865C14.3946 13.6198 14.5 13.3937 14.5 13.158V10.0469',\n ])\n\n/** The head's info affordance (`IconInfoOutlineRegular`, fill-only artwork). */\nexport const INFO_ICON = (size = 11): string =>\n fillGlyph(size, [\n 'M12.5757 7.00012C12.5757 3.92085 10.0794 1.42463 7.00012 1.42456C3.9208 1.42456 1.42456 3.9208 1.42456 7.00012C1.42463 10.0794 3.92085 12.5757 7.00012 12.5757C10.0793 12.5756 12.5756 10.0793 12.5757 7.00012ZM13.8002 7.00012C13.8001 10.7559 10.7559 13.8001 7.00012 13.8002C3.2443 13.8002 0.199291 10.7559 0.199219 7.00012C0.199219 3.24426 3.24426 0.199219 7.00012 0.199219C10.7559 0.199291 13.8002 3.2443 13.8002 7.00012Z',\n 'M7.6127 3.18921V4.55986H6.38735V3.18921H7.6127Z',\n 'M7.6127 5.68921V10.8109H6.38735V5.68921H7.6127Z',\n ], '0 0 14 14')\n\n/** A folded section's chevron (`IconChevronDownOutlineRegular`). */\nexport const CHEVRON_DOWN_ICON = (size = 14): string =>\n glyph(size, REGULAR, ['M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6'])\n","/**\n * Plugin-update review view (browser half).\n *\n * Plain-DOM panel (no React tree) so it can never disturb the DSH shell's\n * reconciliation — same rule as the sidebar entry rows. Rendered content:\n *\n * - head: title + intro + refresh toolbar (the shell plugin-manager head row)\n * - state-grouped sections, each a group head + package cards\n * (48px hairline-framed glyph, title row with a state tag, one-line\n * version transition, end actions) — the shell's card geometry\n * - a facts group for the independent HQ Edge runtime report\n * - footer actions (install / ignore / show again), with a toast that\n * reports the outcome of an install the way the shell's Toast does\n *\n * Every surface rides a `--dsw-alias-*` design token (see VIEW_CSS_TEXT), so\n * the panel follows the shell's light/dark theme and its focus-ring contract.\n *\n * @module\n */\nimport {\n checkUpdates,\n ignoreOffer,\n installSet,\n requestRuntimeUpdate,\n runtimeLatestVersion,\n setPluginUpdateSource,\n setRuntimeUpdateSource,\n unignoreOffer,\n type PluginUpdateItem,\n type PluginUpdateReport,\n type RuntimeUpdateSource,\n} from './api.js'\nimport type { UpdateCopy } from './copy.js'\nimport {\n CHECK_CIRCLE_ICON,\n CLOSE_ICON,\n DOWNLOAD_ICON,\n INFO_ICON,\n PLUGIN_ICON,\n REFRESH_ICON,\n WARNING_ICON,\n} from './icons.js'\n\nexport interface UpdateStoreLike {\n report(): PluginUpdateReport | null\n installing(): boolean\n refresh(): Promise<PluginUpdateReport | null>\n subscribe(listener: () => void): () => void\n}\n\n/** How long the outcome toast stays up, in milliseconds. */\nconst TOAST_HOLD_MS = 4000\n\nfunction el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n className = '',\n text?: string,\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag)\n if (className.length > 0) node.className = className\n if (text !== undefined) node.textContent = text\n return node\n}\n\n/** A button carrying inline SVG artwork (the icon modules emit markup). */\nfunction iconButton(\n className: string,\n markup: string,\n label: string,\n onClick: () => void,\n): HTMLButtonElement {\n const button = el('button', className)\n button.type = 'button'\n button.setAttribute('aria-label', label)\n button.title = label\n button.innerHTML = markup\n button.addEventListener('click', onClick)\n return button\n}\n\n/**\n * Compact a package name to what a person calls it — the shell plugin\n * manager's own rule (`presentation.ts` `shortName`), so both UIs read the\n * same plugin by the same name.\n */\nexport function shortName(name: string): string {\n const unscoped = name.startsWith('@') ? name.slice(name.indexOf('/') + 1) : name\n return unscoped.replace(/^dsh-(?:host-|client-)?/, '')\n}\n\n/** Normalize the server's block reason to a list (it may arrive as a string). */\nfunction blockedByList(item: PluginUpdateItem): string[] {\n const by = item.blockedBy\n if (Array.isArray(by)) return by.filter((entry): entry is string => typeof entry === 'string')\n return typeof by === 'string' && by.length > 0 ? [by] : []\n}\n\n/** The tag tone a state reads as, matching the shell's Tag palette. */\nfunction stateTone(item: PluginUpdateItem): 'success' | 'info' | 'warning' | 'danger' | 'neutral' {\n switch (item.state) {\n case 'update':\n return 'info'\n case 'install':\n return 'warning'\n case 'blocked':\n return 'danger'\n case 'ignored':\n return 'neutral'\n case 'current':\n return 'success'\n }\n}\n\nfunction stateLabel(item: PluginUpdateItem, copy: UpdateCopy): string {\n switch (item.state) {\n case 'current':\n return copy.state.current\n case 'update':\n return copy.state.update\n case 'install':\n return copy.state.install\n case 'ignored':\n return copy.state.ignored\n case 'blocked':\n return copy.state.blocked\n }\n}\n\n/** The one-line version transition under a card's title. */\nfunction versionLine(item: PluginUpdateItem): string {\n switch (item.state) {\n case 'current':\n return item.currentVersion ?? item.availableVersion\n case 'install':\n return item.availableVersion\n default:\n // An absent plugin names only the version it would gain.\n return item.currentVersion === null\n ? item.availableVersion\n : `${item.currentVersion} → ${item.availableVersion}`\n }\n}\n\nfunction blockedReason(item: PluginUpdateItem, copy: UpdateCopy): string | undefined {\n const by = blockedByList(item)\n if (by.includes('host')) return copy.blockedReason.host\n if (by.includes('hq-edge-version')) return copy.blockedReason['hq-edge-version']\n if (by.includes('runtime-unknown')) return copy.blockedReason['runtime-unknown']\n return by.length > 0 ? by.join(', ') : undefined\n}\n\n/** One package card: glyph, name with its state tag, version line, end actions. */\nfunction packageCard(\n item: PluginUpdateItem,\n copy: UpdateCopy,\n installing: boolean,\n store: UpdateStoreLike,\n): HTMLLIElement {\n const card = el('li', 'hqpu-card')\n card.dataset.pluginName = item.name\n card.dataset.state = item.state\n // The full package name is the card's tooltip; the row shows the short name.\n card.title = `${item.name} · ${versionLine(item)}`\n\n const head = el('div', 'hqpu-card-head')\n\n const glyph = el('span', 'hqpu-card-icon')\n glyph.setAttribute('aria-hidden', 'true')\n glyph.innerHTML = PLUGIN_ICON(24)\n\n const main = el('div', 'hqpu-card-main')\n const titleRow = el('div', 'hqpu-title-row')\n const title = el('span', 'hqpu-card-title', shortName(item.name))\n const tag = el('span', 'hqpu-tag')\n tag.dataset.tone = stateTone(item)\n tag.textContent = stateLabel(item, copy)\n titleRow.append(title, tag)\n\n const desc = el('span', 'hqpu-card-desc', versionLine(item))\n\n main.append(titleRow, desc)\n head.append(glyph, main)\n\n const end = el('div', 'hqpu-card-end')\n if (item.state === 'update' || item.state === 'install') {\n const button = el('button', 'hqpu-btn hqpu-btn--sm hqpu-btn--primary')\n button.type = 'button'\n button.disabled = installing\n button.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installOne}</span>`\n button.addEventListener('click', () => {\n void runInstall(store, copy)\n })\n end.append(button)\n }\n head.append(end)\n card.append(head)\n\n // A block reason belongs to a blocked offer only.\n if (item.state === 'blocked') {\n const reason = blockedReason(item, copy)\n if (reason !== undefined) {\n card.append(el('p', 'hqpu-card-reason', reason))\n }\n }\n return card\n}\n\n/** A section: group head with its count, then the cards. */\nfunction group(\n heading: string,\n items: PluginUpdateItem[],\n copy: UpdateCopy,\n installing: boolean,\n store: UpdateStoreLike,\n): HTMLElement | null {\n if (items.length === 0) return null\n const section = el('section', 'hqpu-group')\n section.dataset.group = heading\n\n const head = el('div', 'hqpu-group-head')\n head.append(el('h3', 'hqpu-group-title', heading))\n head.append(el('span', 'hqpu-group-count', copy.groupCount(items.length)))\n\n const list = el('ul', 'hqpu-cards')\n for (const item of items) list.append(packageCard(item, copy, installing, store))\n\n section.append(head, list)\n return section\n}\n\n/** First-read placeholders, sharing the card and text-line layout. */\nfunction skeleton(copy: UpdateCopy): HTMLElement {\n const section = el('section', 'hqpu-group')\n section.setAttribute('role', 'status')\n section.dataset.loading = ''\n const head = el('div', 'hqpu-group-head')\n head.setAttribute('aria-hidden', 'true')\n const heading = el('span', 'hqpu-group-title hqpu-skeleton-text hqpu-skeleton-heading')\n const headingBar = el('span', 'hqpu-skeleton-fill hqpu-skeleton-bar')\n heading.append(headingBar)\n head.append(heading)\n\n const list = el('ul', 'hqpu-cards')\n list.setAttribute('aria-hidden', 'true')\n for (let index = 0; index < 3; index += 1) {\n const card = el('li', 'hqpu-card')\n const cardHead = el('div', 'hqpu-card-head')\n const glyph = el('span', 'hqpu-card-icon hqpu-skeleton-fill hqpu-skeleton-icon')\n const main = el('div', 'hqpu-card-main')\n const titleRow = el('div', 'hqpu-title-row')\n const title = el('span', 'hqpu-card-title hqpu-skeleton-text hqpu-skeleton-title')\n title.append(el('span', 'hqpu-skeleton-fill hqpu-skeleton-bar'))\n titleRow.append(title)\n const desc = el('span', 'hqpu-card-desc hqpu-skeleton-text hqpu-skeleton-desc')\n desc.append(el('span', 'hqpu-skeleton-fill hqpu-skeleton-bar'))\n main.append(titleRow, desc)\n cardHead.append(glyph, main)\n const end = el('div', 'hqpu-card-end')\n end.setAttribute('aria-hidden', 'true')\n cardHead.append(end)\n card.append(cardHead)\n list.append(card)\n }\n section.append(head, list)\n // The skeleton replaces the whole column, so label it for screen readers.\n section.setAttribute('aria-label', copy.loading)\n return section\n}\n\n/** Mutable editor state for one manifest source (lives in the panel closure). */\ninterface SourceState {\n editing: boolean\n saving: boolean\n draft: string\n}\n\n/**\n * Shared state + callbacks for one update-source editor.\n *\n * hq-edge serves both manifest sources from one shape (plugin release\n * manifest, runtime-update manifest), and both are editable from this panel:\n * an override is persisted per host, so pointing discovery at a staging\n * channel (or a local test rig) needs no env var and no restart. The editor\n * state lives in the panel closure for the same reason `armed` does — a store\n * refresh must not discard a half-typed URL or silently close the editor.\n */\nexport interface SourceEditorActions {\n /** The source editor is open (input shown instead of the read-only row). */\n sourceEditing: boolean\n /** A save is in flight. */\n sourceSaving: boolean\n /** The URL being edited (what the input shows). */\n sourceDraft: string\n onSourceEdit(): void\n onSourceDraft(value: string): void\n onSourceSave(): void\n onSourceCancel(): void\n /** Clear the user override, falling back to the configured URL. */\n onSourceReset(): void\n}\n\n/**\n * The runtime-update action row's state and callbacks.\n *\n * Installing the runtime is destructive in a way a plugin install is not: hq-edge\n * shuts itself down so the EDA host can swap the runtime directory. So the row\n * is a two-step confirm (arm → confirm), never a single fire-and-forget click.\n */\nexport interface RuntimeActions extends SourceEditorActions {\n /** A request is in flight (hq-edge accepted it and is restarting). */\n busy: boolean\n /** The confirm step is armed for the version the row names. */\n armed: boolean\n /**\n * hq-edge answered `202 Accepted`: from here on the outcome belongs to the\n * EDA host, and this process is about to be replaced. Terminal — the row\n * must never re-offer the button once this is true, because hq-edge is\n * already gone and a second request could only fail.\n */\n accepted: boolean\n onArm(): void\n onConfirm(): void\n onCancel(): void\n}\n\n/**\n * Normalize the report's `runtime.source` to the full shape.\n *\n * An older hq-edge predates the feature and sends no `source` at all — that\n * must degrade to a read-only panel, not crash the render. A future server\n * may add fields; unknown extras are dropped here rather than trusted.\n */\nfunction normalizeSource(raw: unknown): RuntimeUpdateSource | null {\n if (raw === null || typeof raw !== 'object') return null\n const candidate = raw as Partial<RuntimeUpdateSource>\n if (typeof candidate.origin !== 'string') return null\n return {\n url: typeof candidate.url === 'string' ? candidate.url : '',\n origin: candidate.origin as RuntimeUpdateSource['origin'],\n configuredUrl: typeof candidate.configuredUrl === 'string' ? candidate.configuredUrl : '',\n override: candidate.override === true,\n }\n}\n\n/** The tag naming where the URL in effect came from. */\nfunction originLabel(origin: RuntimeUpdateSource['origin'], copy: UpdateCopy): string {\n switch (origin) {\n case 'override':\n return copy.originOverride\n case 'config':\n return copy.originConfig\n default:\n return copy.originUnset\n }\n}\n\n/** The 更新源 fact row: the URL in effect plus an origin tag. */\nfunction sourceFactRow(\n facts: HTMLElement,\n source: RuntimeUpdateSource,\n copy: UpdateCopy,\n): void {\n const wrap = el('div')\n wrap.append(el('dt', '', copy.runtime.source))\n const dd = el('dd')\n const line = el('div', 'hqpu-source-line')\n const code = el(\n 'code',\n 'hqpu-source-url',\n source.url.length > 0 ? source.url : copy.runtime.notConfigured,\n )\n const tag = el('span', 'hqpu-tag')\n tag.dataset.tone = source.override ? 'info' : source.origin === 'config' ? 'success' : 'neutral'\n tag.textContent = originLabel(source.origin, copy)\n line.append(code, tag)\n dd.append(line)\n wrap.append(dd)\n facts.append(wrap)\n}\n\n/** Which manifest an editor belongs to — drives data attributes + CSS hook. */\nexport type SourceKind = 'runtime' | 'plugin'\n\n/** The localized strings one source editor needs (both kinds share the flow). */\ninterface SourceEditorLabels {\n hint: string\n edit: string\n save: string\n reset: string\n inputLabel: string\n placeholder: string\n}\n\n/** Pick the labels for one kind out of the copy table. */\nfunction sourceLabels(copy: UpdateCopy, kind: SourceKind): SourceEditorLabels {\n return kind === 'runtime'\n ? {\n hint: copy.runtime.sourceHint,\n edit: copy.runtime.sourceEdit,\n save: copy.runtime.sourceSave,\n reset: copy.runtime.sourceReset,\n inputLabel: copy.runtime.sourceInputLabel,\n placeholder: copy.runtime.sourcePlaceholder,\n }\n : {\n hint: copy.pluginSource.hint,\n edit: copy.pluginSource.edit,\n save: copy.pluginSource.save,\n reset: copy.pluginSource.reset,\n inputLabel: copy.pluginSource.inputLabel,\n placeholder: copy.pluginSource.placeholder,\n }\n}\n\n/**\n * One update-source editor, shared by the plugin manifest and the runtime\n * manifest (the only differences are the labels and the data-attribute\n * prefix, so tests and CSS can target each).\n *\n * Read mode is a hint plus a 修改 button; edit mode is the URL field with\n * 保存 / 取消 — plus 恢复默认 while a user override shadows the configured\n * URL. The draft lives in {@link SourceEditorActions} (the panel closure), so\n * a background store refresh re-renders the typed URL instead of discarding it.\n */\nfunction sourceEditor(\n kind: SourceKind,\n source: RuntimeUpdateSource,\n copy: UpdateCopy,\n actions: SourceEditorActions,\n): HTMLElement {\n const labels = sourceLabels(copy, kind)\n const block = el('div', `hqpu-source-editor hqpu-source-editor--${kind}`)\n if (!actions.sourceEditing) {\n block.append(el('p', 'hqpu-status hqpu-source-hint', labels.hint))\n const edit = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', labels.edit)\n edit.type = 'button'\n edit.setAttribute(`data-${kind}-source-edit`, '')\n edit.disabled = actions.sourceSaving\n edit.addEventListener('click', actions.onSourceEdit)\n block.append(edit)\n return block\n }\n const input = el('input', 'hqpu-source-input')\n input.type = 'url'\n input.value = actions.sourceDraft\n input.placeholder = labels.placeholder\n input.setAttribute('aria-label', labels.inputLabel)\n input.setAttribute(`data-${kind}-source-input`, '')\n input.disabled = actions.sourceSaving\n input.addEventListener('input', () => {\n // Draft-only: no re-render, so the caret and IME stay untouched.\n actions.onSourceDraft(input.value)\n })\n // Enter saves, Escape cancels — the field is the whole editor, and Escape\n // must not fall through to the panel-wide close handler.\n input.addEventListener('keydown', (ev) => {\n if (ev.key === 'Enter') {\n ev.preventDefault()\n actions.onSourceSave()\n } else if (ev.key === 'Escape') {\n ev.preventDefault()\n ev.stopPropagation()\n actions.onSourceCancel()\n }\n })\n const save = el('button', 'hqpu-btn hqpu-btn--primary hqpu-btn--sm', labels.save)\n save.type = 'button'\n save.setAttribute(`data-${kind}-source-save`, '')\n save.disabled = actions.sourceSaving\n save.addEventListener('click', actions.onSourceSave)\n const cancel = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', copy.cancel)\n cancel.type = 'button'\n cancel.setAttribute(`data-${kind}-source-cancel`, '')\n cancel.disabled = actions.sourceSaving\n cancel.addEventListener('click', actions.onSourceCancel)\n const controls = el('div', 'hqpu-actions hqpu-source-controls')\n controls.append(input, save, cancel)\n if (source.override) {\n const reset = el('button', 'hqpu-btn hqpu-btn--ghost hqpu-btn--sm', labels.reset)\n reset.type = 'button'\n reset.setAttribute(`data-${kind}-source-reset`, '')\n reset.disabled = actions.sourceSaving\n reset.addEventListener('click', actions.onSourceReset)\n controls.append(reset)\n }\n block.append(controls)\n // Focus on open — and on any re-render while editing, which is exactly what\n // a rejected save wants (the user is dropped back into the field to fix it).\n input.focus()\n return block\n}\n\n/**\n * The plugin release-manifest source, as its own group above the plugin\n * groups: it says where the whole list comes from, so it belongs above the\n * list rather than inside it. Absent on an older hq-edge (no `pluginSource`\n * in the report) — then nothing is rendered at all.\n */\nfunction pluginSourceGroup(\n report: PluginUpdateReport,\n copy: UpdateCopy,\n actions: SourceEditorActions,\n): HTMLElement | null {\n const source = normalizeSource(report.pluginSource)\n if (source === null) return null\n const section = el('section', 'hqpu-group')\n section.dataset.group = 'plugin-source'\n const head = el('div', 'hqpu-group-head')\n head.append(el('h3', 'hqpu-group-title', copy.pluginSource.title))\n section.append(head)\n\n const facts = el('dl', 'hqpu-facts')\n sourceFactRow(facts, source, copy)\n section.append(facts)\n section.append(sourceEditor('plugin', source, copy, actions))\n return section\n}\n\n/** The independent HQ Edge runtime report, as ruled fact rows. */\nfunction runtimeGroup(\n report: PluginUpdateReport,\n copy: UpdateCopy,\n actions: RuntimeActions,\n): HTMLElement {\n const section = el('section', 'hqpu-group')\n section.dataset.group = 'runtime'\n const head = el('div', 'hqpu-group-head')\n head.append(el('h3', 'hqpu-group-title', copy.runtime.title))\n section.append(head)\n\n const facts = el('dl', 'hqpu-facts')\n const runtime = report.runtime\n // `runtime.latest` is a release ENTRY; only its version is ever displayed or\n // sent back as `targetVersion`.\n const latestVersion = runtimeLatestVersion(runtime)\n\n const row = (label: string, value: string, code = false): void => {\n const wrap = el('div')\n wrap.append(el('dt', '', label))\n const dd = el('dd')\n if (code) {\n const node = el('code', '', value)\n dd.append(node)\n } else {\n dd.textContent = value\n }\n wrap.append(dd)\n facts.append(wrap)\n }\n\n // The source row: which manifest URL is in effect and where it came from.\n // Absent on an older hq-edge — then only the legacy notConfigured row shows.\n const source = normalizeSource(runtime.source)\n\n if (runtime.configured === false) {\n if (source !== null) {\n sourceFactRow(facts, source, copy)\n } else {\n row(copy.runtime.source, copy.runtime.notConfigured)\n }\n } else {\n row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown)\n if (runtime.updateAvailable && latestVersion !== null) {\n row(copy.runtime.latest, latestVersion)\n }\n if (report.manifestVersion !== null) {\n row(copy.runtime.set, report.manifestVersion, true)\n }\n if (typeof runtime.checkedAt === 'string' && runtime.checkedAt.length > 0) {\n row(copy.runtime.checkedAt, new Date(runtime.checkedAt).toLocaleString())\n }\n if (source !== null) {\n sourceFactRow(facts, source, copy)\n }\n }\n section.append(facts)\n\n // The editor: point the runtime at another release channel (a staging\n // manifest, a local rig) without an env var and a restart. hq-edge persists\n // the override per host, so it survives restarts of both processes.\n if (source !== null) {\n section.append(sourceEditor('runtime', source, copy, actions))\n }\n\n // ── Runtime update action row ──\n // Only offered when hq-edge reports a newer distribution AND named its\n // version: the request needs `targetVersion`, so an entry without one has\n // nothing to send.\n if (runtime.updateAvailable && latestVersion !== null) {\n const row = el('div', 'hqpu-actions hqpu-runtime-actions')\n if (actions.accepted) {\n // Terminal state: no button. hq-edge has already exited, so re-offering\n // \"update\" here could only produce a transport error.\n const line = el('span', 'hqpu-status hqpu-status--dot')\n line.setAttribute('role', 'status')\n line.append(el('span', 'hqpu-spinner'), el('span', '', copy.runtime.updateAccepted))\n row.append(line)\n } else if (actions.busy) {\n // Downloading + verifying + staging the bundle: tens of seconds to a few\n // minutes. Say what is happening instead of an open-ended spinner.\n const line = el('span', 'hqpu-status hqpu-status--dot')\n line.setAttribute('role', 'status')\n line.append(el('span', 'hqpu-spinner'), el('span', '', copy.runtime.updating))\n row.append(line)\n } else if (actions.armed) {\n row.append(el('p', 'hqpu-status hqpu-runtime-confirm', copy.runtime.updateConfirm(latestVersion)))\n const yes = el('button', 'hqpu-btn hqpu-btn--primary hqpu-btn--sm', copy.confirmInstall)\n yes.type = 'button'\n yes.dataset.runtimeConfirm = ''\n yes.addEventListener('click', actions.onConfirm)\n const no = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', copy.cancel)\n no.type = 'button'\n no.addEventListener('click', actions.onCancel)\n row.append(yes, no)\n } else {\n const start = el('button', 'hqpu-btn hqpu-btn--primary hqpu-btn--sm')\n start.type = 'button'\n start.dataset.runtimeUpdate = ''\n start.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.runtime.update}</span>`\n start.addEventListener('click', actions.onArm)\n row.append(start)\n }\n section.append(row)\n } else {\n section.append(el('p', 'hqpu-status', copy.runtime.note))\n }\n\n if (typeof runtime.lastError === 'string' && runtime.lastError.length > 0) {\n const line = el('p', 'hqpu-status hqpu-status--dot')\n line.setAttribute('role', 'status')\n const dot = el('span', 'hqpu-dot')\n dot.dataset.state = 'error'\n line.append(dot, el('span', '', `${copy.lastError}: ${runtime.lastError}`))\n section.append(line)\n }\n return section\n}\n\nfunction renderList(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n installing: boolean,\n store: UpdateStoreLike,\n): void {\n section.textContent = ''\n if (report === null) {\n section.append(skeleton(copy))\n return\n }\n if (report.items.length === 0) {\n section.append(el('div', 'hqpu-empty', copy.empty))\n return\n }\n\n const byState = (state: PluginUpdateItem['state']): PluginUpdateItem[] =>\n report.items.filter((item) => item.state === state)\n\n // Most important first: what can be acted on, then what is refused, then\n // what was muted, then what is already settled.\n const order: Array<[string, PluginUpdateItem[]]> = [\n [copy.group.actionable, [...byState('update'), ...byState('install')]],\n [copy.group.blocked, byState('blocked')],\n [copy.group.ignored, byState('ignored')],\n [copy.group.current, byState('current')],\n ]\n for (const [heading, items] of order) {\n const node = group(heading, items, copy, installing, store)\n if (node !== null) section.append(node)\n }\n}\n\nfunction renderRuntime(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n actions: RuntimeActions,\n): void {\n section.textContent = ''\n if (report === null) return\n section.append(runtimeGroup(report, copy, actions))\n}\n\n/** The plugin-source group, above the plugin groups. No-op without the field. */\nfunction renderPluginSource(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n actions: SourceEditorActions,\n): void {\n section.textContent = ''\n if (report === null) return\n const node = pluginSourceGroup(report, copy, actions)\n if (node !== null) section.append(node)\n}\n\n/** Run the install transaction, then keep polling until the state settles. */\nasync function runInstall(store: UpdateStoreLike, copy: UpdateCopy): Promise<void> {\n const report = store.report()\n if (report === null) return\n // Without an evaluated set there is nothing to install: hq-edge has not read\n // a manifest yet, so the report is still its empty shell.\n if (report.manifestVersion === null) {\n showToast(copy.checkFailed, 'error')\n return\n }\n try {\n const result = await installSet(report.manifestVersion)\n if (result.error !== undefined) {\n showToast(`${copy.installError}: ${result.error}`, 'error')\n return\n }\n // The set activates atomically and DSH restarts. Keep polling the backend\n // state rather than trusting the response alone (spec §7.1).\n showToast(copy.installSuccess, 'success')\n void pollUntilSettled(store)\n } catch (err: unknown) {\n showToast(\n `${copy.installError}: ${err instanceof Error ? err.message : String(err)}`,\n 'error',\n )\n }\n}\n\n/** The single live toast; a new message replaces the one on screen. */\nlet toastHost: HTMLElement | null = null\nlet toastTimer: number | null = null\n\nfunction showToast(message: string, tone: 'success' | 'error'): void {\n if (toastHost === null) return\n if (toastTimer !== null) window.clearTimeout(toastTimer)\n toastHost.textContent = ''\n const toast = el('div', 'hqpu-toast')\n toast.setAttribute('role', 'status')\n const icon = el('span', 'hqpu-toast-icon')\n icon.dataset.tone = tone\n icon.innerHTML = tone === 'success' ? CHECK_CIRCLE_ICON(16) : WARNING_ICON(16)\n toast.append(icon, el('span', 'hqpu-toast-text', message))\n toastHost.append(toast)\n toastTimer = window.setTimeout(() => {\n toastHost?.replaceChildren()\n toastTimer = null\n }, TOAST_HOLD_MS)\n}\n\nfunction renderActions(\n bar: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n store: UpdateStoreLike,\n installing: boolean,\n): void {\n bar.textContent = ''\n if (report === null) return\n\n if (installing) {\n const line = el('span', 'hqpu-status hqpu-status--dot')\n const spinner = el('span', 'hqpu-spinner')\n line.append(spinner, el('span', '', copy.installing))\n bar.append(line)\n return\n }\n\n const actionable = report.items.filter(\n (item) => item.state === 'update' || item.state === 'install',\n ).length\n // Every write needs an evaluated set; hq-edge reports `null` until it has\n // read a manifest, and posting `null` would only earn a 400.\n const hasSet = report.manifestVersion !== null\n\n if (actionable === 0) {\n // The official page shows no install control when there is nothing to\n // commit, so mirror that with a muted line rather than a dead button.\n bar.append(el('span', 'hqpu-status', copy.nothingToInstall))\n } else {\n const installBtn = el('button', 'hqpu-btn hqpu-btn--primary')\n installBtn.type = 'button'\n installBtn.disabled = !hasSet || report.ignoredSet\n installBtn.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installAll(actionable)}</span>`\n installBtn.addEventListener('click', () => {\n void runInstall(store, copy)\n })\n bar.append(installBtn)\n }\n\n if (!hasSet) return\n\n if (report.ignoredSet) {\n const unignore = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', copy.unignore)\n unignore.type = 'button'\n unignore.addEventListener('click', () => {\n void unignoreOffer(report.manifestVersion as string)\n .then(() => store.refresh())\n .catch(() => undefined)\n })\n bar.append(unignore)\n } else if (actionable > 0) {\n const ignore = el('button', 'hqpu-btn hqpu-btn--ghost hqpu-btn--sm', copy.ignore)\n ignore.type = 'button'\n ignore.addEventListener('click', () => {\n void ignoreOffer(report.manifestVersion as string)\n .then(() => store.refresh())\n .catch(() => undefined)\n })\n bar.append(ignore)\n }\n}\n\n/** After an install, poll the backend until the update report settles\n * (installing flag clears or DSH restarts under us — the next load re-reads\n * the fresh state). */\nfunction pollUntilSettled(store: UpdateStoreLike): void {\n let attempts = 0\n const tick = (): void => {\n void store\n .refresh()\n .then((report) => {\n if (store.installing()) return\n if (report !== null && report.ignoredSet === false && attempts >= 2) return\n })\n .catch(() => undefined)\n .finally(() => {\n attempts += 1\n if (attempts < 10) {\n window.setTimeout(tick, 2000)\n }\n })\n }\n tick()\n}\n\n/**\n * Open (or focus) the update panel. Re-renders from the store; when the store\n * is stale the panel first refreshes the report.\n */\nexport function openUpdateView(store: UpdateStoreLike, copy: UpdateCopy): void {\n const existing = document.querySelector<HTMLElement>('[data-hq-plugin-update-view]')\n if (existing !== null) {\n existing.style.display = ''\n return\n }\n\n const overlay = el('div', 'hqpu-overlay')\n overlay.setAttribute('data-hq-plugin-update-view', '')\n overlay.addEventListener('click', (ev) => {\n if (ev.target === overlay) onClose()\n })\n\n const scrim = el('div', 'hqpu-scrim')\n const panel = el('div', 'hqpu-panel')\n panel.setAttribute('role', 'dialog')\n panel.setAttribute('aria-modal', 'true')\n panel.setAttribute('aria-label', copy.viewTitle)\n panel.tabIndex = -1\n\n // ── Head row: title + intro, refresh toolbar ──\n const head = el('div', 'hqpu-head')\n const headMain = el('div', 'hqpu-head-main')\n headMain.append(el('h1', 'hqpu-title', copy.viewTitle))\n const intro = el('p', 'hqpu-intro')\n intro.append(el('span', '', copy.viewIntro))\n const info = el('button', 'hqpu-info')\n info.type = 'button'\n info.setAttribute('aria-label', copy.viewInfo)\n info.title = copy.viewInfo\n info.innerHTML = INFO_ICON(11)\n intro.append(info)\n headMain.append(intro)\n\n const toolbar = el('div', 'hqpu-toolbar')\n const refreshBtn = iconButton('hqpu-icon-btn', REFRESH_ICON(16), copy.refresh, () => {\n void store.refresh()\n })\n toolbar.append(refreshBtn)\n const closeBtn = iconButton('hqpu-icon-btn', CLOSE_ICON(16), copy.close, () => {\n onClose()\n })\n toolbar.append(closeBtn)\n\n head.append(headMain, toolbar)\n panel.append(head)\n\n // ── Scrolling body: the centred column ──\n const scroll = el('div', 'hqpu-scroll')\n const column = el('div', 'hqpu-column')\n const pluginSourceSection = el('div', 'hqpu-stack')\n const listSection = el('div', 'hqpu-stack')\n const runtimeSection = el('div', 'hqpu-stack')\n const toastSection = el('div', 'hqpu-toast-host')\n column.append(pluginSourceSection, listSection, runtimeSection, toastSection)\n scroll.append(column)\n panel.append(scroll)\n\n // ── Footer actions (ui-primitives Modal .footer) ──\n const footer = el('div', 'hqpu-footer')\n const actionBar = el('div', 'hqpu-actions')\n footer.append(actionBar)\n panel.append(footer)\n\n overlay.append(scrim, panel)\n document.body.appendChild(overlay)\n toastHost = toastSection\n panel.focus()\n\n const disposers: Array<() => void> = []\n\n // ── Runtime update: arm → confirm, then hand off to hq-edge ──\n // Live in the panel's closure (not the report) because they are UI state, and\n // a store refresh must not silently disarm a confirm the user just armed.\n let runtimeArmed = false\n let runtimeBusy = false\n let runtimeAccepted = false\n\n // Source-editor state, same reasoning: a background refresh must not discard\n // a half-typed URL or silently close either editor.\n const runtimeSourceState: SourceState = { editing: false, saving: false, draft: '' }\n const pluginSourceState: SourceState = { editing: false, saving: false, draft: '' }\n\n // Delegated, not spread: `...sourceActionsFor(...)` would evaluate the\n // getters once and freeze `sourceEditing: false` — the editor would never\n // open. Read every member through the live object instead.\n const runtimeSourceActions = sourceActionsFor('runtime', runtimeSourceState)\n const runtimeActions: RuntimeActions = {\n get busy() {\n return runtimeBusy\n },\n get armed() {\n return runtimeArmed\n },\n get accepted() {\n return runtimeAccepted\n },\n onArm: () => {\n runtimeArmed = true\n render()\n },\n onCancel: () => {\n runtimeArmed = false\n render()\n },\n onConfirm: () => {\n void requestRuntime()\n },\n get sourceEditing() {\n return runtimeSourceActions.sourceEditing\n },\n get sourceSaving() {\n return runtimeSourceActions.sourceSaving\n },\n get sourceDraft() {\n return runtimeSourceActions.sourceDraft\n },\n onSourceEdit: () => runtimeSourceActions.onSourceEdit(),\n onSourceDraft: (value: string) => runtimeSourceActions.onSourceDraft(value),\n onSourceSave: () => runtimeSourceActions.onSourceSave(),\n onSourceCancel: () => runtimeSourceActions.onSourceCancel(),\n onSourceReset: () => runtimeSourceActions.onSourceReset(),\n }\n\n const pluginActions: SourceEditorActions = sourceActionsFor('plugin', pluginSourceState)\n\n /** Build the {@link SourceEditorActions} half for one manifest source. */\n function sourceActionsFor(\n kind: SourceKind,\n state: SourceState,\n ): SourceEditorActions {\n return {\n get sourceEditing() {\n return state.editing\n },\n get sourceSaving() {\n return state.saving\n },\n get sourceDraft() {\n return state.draft\n },\n onSourceEdit: () => {\n // Prefill with the URL in effect, so 保存 is a no-op unless edited.\n const report = store.report()\n const source =\n kind === 'runtime' ? normalizeSource(report?.runtime.source) : normalizeSource(report?.pluginSource)\n state.draft = source?.url ?? ''\n state.editing = true\n render()\n },\n onSourceDraft: (value: string) => {\n state.draft = value\n },\n onSourceSave: () => {\n void submitSource(kind, state)\n },\n onSourceCancel: () => {\n state.editing = false\n render()\n },\n onSourceReset: () => {\n void submitSource(kind, state, '')\n },\n }\n }\n\n /**\n * Save one manifest source — or clear the override with an empty URL —\n * then re-run discovery.\n *\n * The re-check is not optional: the report is cached, so without a fresh\n * `POST /check` the facts and the update button would keep naming the OLD\n * manifest's offer after the source changed. hq-edge answers 422 with a\n * message meant for a person on refusal (bad scheme, credentials, …), which\n * the toast shows verbatim.\n */\n async function submitSource(\n kind: SourceKind,\n state: SourceState,\n url: string = state.draft,\n ): Promise<void> {\n state.saving = true\n render()\n const labels = kind === 'runtime' ? copy.runtime : copy.pluginSource\n try {\n if (kind === 'runtime') await setRuntimeUpdateSource(url)\n else await setPluginUpdateSource(url)\n state.editing = false\n showToast(labels.saved, 'success')\n try {\n await checkUpdates()\n await store.refresh()\n } catch {\n // The source IS saved; a failed re-check only leaves the previous\n // offer on screen until the next refresh — the toolbar re-runs it.\n }\n } catch (err: unknown) {\n showToast(\n `${labels.saveFailed}: ${err instanceof Error ? err.message : String(err)}`,\n 'error',\n )\n } finally {\n state.saving = false\n render()\n }\n }\n\n async function requestRuntime(): Promise<void> {\n const version = runtimeLatestVersion(store.report()?.runtime)\n if (version === null) {\n showToast(copy.checkFailed, 'error')\n return\n }\n runtimeArmed = false\n runtimeBusy = true\n render()\n try {\n const result = await requestRuntimeUpdate(version)\n if (result?.error !== undefined) {\n showToast(`${copy.installError}: ${result.error}`, 'error')\n return\n }\n // 202 Accepted: hq-edge exits to let the EDA host swap the runtime and\n // relaunch. This panel is about to go away with the restart, so say so —\n // and latch it, so a store refresh cannot re-offer the button for a\n // server that is already gone.\n runtimeAccepted = true\n showToast(copy.runtime.updateAccepted, 'success')\n } catch (err: unknown) {\n showToast(\n `${copy.installError}: ${err instanceof Error ? err.message : String(err)}`,\n 'error',\n )\n } finally {\n runtimeBusy = false\n render()\n }\n }\n\n const render = (): void => {\n const report = store.report()\n const installing = store.installing()\n const stale = report === null\n refreshBtn.disabled = stale\n if (stale) {\n listSection.textContent = ''\n listSection.append(skeleton(copy))\n } else {\n renderList(listSection, report, copy, installing, store)\n }\n renderPluginSource(pluginSourceSection, report, copy, pluginActions)\n renderRuntime(runtimeSection, report, copy, runtimeActions)\n renderActions(actionBar, report, copy, store, installing)\n }\n\n const unsubscribe = store.subscribe(render)\n disposers.push(unsubscribe)\n\n // Refresh on open (bounded; never block the shell).\n void store.refresh().then(() => {\n // Store notification re-renders already; nothing extra to do.\n })\n\n // Escape closes, the shell-wide dialog contract.\n const onKeyDown = (ev: KeyboardEvent): void => {\n if (ev.key === 'Escape') {\n ev.stopPropagation()\n onClose()\n }\n }\n document.addEventListener('keydown', onKeyDown)\n disposers.push(() => document.removeEventListener('keydown', onKeyDown))\n\n function onClose(): void {\n for (const dispose of disposers) {\n try {\n dispose()\n } catch {\n /* already disposed */\n }\n }\n if (toastTimer !== null) window.clearTimeout(toastTimer)\n toastTimer = null\n if (toastHost === toastSection) toastHost = null\n overlay.remove()\n }\n\n render()\n}\n","/**\n * `@huaqiu/dsh-tool-plugin-update` — browser half.\n *\n * Mounts one sidebar toolbutton in the Huaqiu family block (\"插件更新\") with a\n * numeric badge equal to the actionable plugin-update count (spec §6.3/§7.1:\n * only compatible `update` + `install` items after host/HQ-Edge filtering and\n * after excluding the ignored offer; runtime updates are NOT counted). Click\n * opens the review/confirm/ignore modal.\n *\n * The authoritative state is the hq-edge report fetched through the plugin's\n * same-origin forwarding routes. A tiny in-memory store owns the latest report\n * + installing flag; the sidebar badge and the modal subscribe to it. No\n * timers beyond the install-poll and an idle 60 s refresh.\n *\n * @module\n */\nimport {\n HUAQIU_SIDEBAR_ENTRY_FAMILY,\n detectHostLocale,\n mountSidebarEntry,\n observeHostLocale,\n type HostLocale,\n} from '@huaqiu/dsh-sidebar-entry'\nimport { checkUpdates, fetchReport, type PluginUpdateReport } from './api.js'\nimport { COPY } from './copy.js'\nimport { PLUGIN_ICON } from './icons.js'\nimport { openUpdateView, type UpdateStoreLike } from './update-view.js'\n\n/** Stable data attributes identifying the injected entry row. */\nexport const UPDATE_ENTRY_SELECTOR = '[data-hq-plugin-update-entry]'\nexport const UPDATE_ENTRY_ATTRIBUTE = 'data-hq-plugin-update-entry'\n\n/** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */\nconst PLUGIN_ID = 'huaqiu-plugin-update'\n\n/** Row class names (defined by {@link ENTRY_CSS_TEXT}). */\nconst ENTRY_CSS: Record<string, string> = {\n entry: 'hqpu-sidebar-entry',\n entryIcon: 'hqpu-sidebar-entry__icon',\n entryLabel: 'hqpu-sidebar-entry__label',\n entryBadge: 'hqpu-sidebar-entry__badge',\n}\n\n/** Localized row copy: zh is the fallback, matching the family convention. */\nfunction entryCopy(lang: HostLocale | undefined): { label: string; tooltip: string } {\n return lang === 'en'\n ? { label: COPY.en.entryLabel, tooltip: COPY.en.entryTooltip }\n : { label: COPY.zh.entryLabel, tooltip: COPY.zh.entryTooltip }\n}\n\n/** The sidebar glyph — the same pinwheel the shell's own Plugins entry draws\n * (`IconPluginPinwheelOutlineRegular`), so both rows read as one feature. */\nexport const UPDATE_ICON = PLUGIN_ICON(16)\n\n/** Sidebar entry styles — matched to the family rows (same inset/padding/gap\n * as the shell's own Plugins entry, `SidebarRoot.module.css` `.panelRow`).\n * Every colour rides a `--dsw-alias-*` token so the row follows the shell's\n * light/dark theme; the badge foreground uses the toast label, the shell's\n * \"always light ink on a saturated surface\" token. */\nconst ENTRY_CSS_TEXT = `\n.hqpu-sidebar-entry {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin: 0 2px;\n min-height: 36px;\n padding: 7px 8px;\n background: transparent;\n border: none;\n border-radius: var(--dsw-radius-md);\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font: inherit;\n line-height: 22px;\n text-align: left;\n white-space: nowrap;\n}\n.hqpu-sidebar-entry:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.hqpu-sidebar-entry[data-active] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-sidebar-entry:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: -2px;\n}\n.hqpu-sidebar-entry__icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n.hqpu-sidebar-entry__icon svg {\n display: block;\n width: 16px;\n height: 16px;\n}\n.hqpu-sidebar-entry__label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1;\n}\n.hqpu-sidebar-entry__badge {\n flex: none;\n min-width: 16px;\n height: 16px;\n padding: 0 5px;\n border-radius: 8px;\n background: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-toast-label);\n font-size: 11px;\n line-height: 16px;\n font-weight: 500;\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n/* Collapsed rail: icon-only, centered — same 36px square the shell uses. */\n[data-sidebar-collapsed] .hqpu-sidebar-entry {\n justify-content: center;\n width: 36px;\n height: 36px;\n margin: 0;\n padding: 0;\n}\n[data-sidebar-collapsed] .hqpu-sidebar-entry__icon svg {\n width: 18px;\n height: 18px;\n}\n[data-sidebar-collapsed] .hqpu-sidebar-entry__label,\n[data-sidebar-collapsed] .hqpu-sidebar-entry__badge {\n display: none;\n}\n`\n\n/**\n * View styles (hqpu-*). Plain overlay + panel, drawn with the SAME design\n * tokens and geometry the shell's own plugin manager uses\n * (`ui-plugin-manager/src/client/PluginManagerPage.module.css` +\n * `ui-primitives/src/{Modal,Toast,Tag,StateDot}.module.css`), so this page\n * reads as part of the product rather than a bolt-on.\n *\n * Two rules keep it honest across themes:\n * 1. every colour is a `--dsw-alias-*` token — no literal hex/rgb, so light\n * and dark both come out right (the old sheet invented\n * `--dsw-alias-bg-surface` / `--dsw-alias-line-border` / `--dsw-alias-bg-badge`,\n * none of which exist, and so always fell back to a white panel);\n * 2. `color-mix()` tints come from the state token itself, so a palette\n * change moves fill and ink together.\n *\n * The chrome (panel, scrim, header, footer) follows `ui-primitives/Modal`,\n * because this view IS a dialog; the body follows `PluginManagerPage`, because\n * that is the content it heads. The one token the bundled shell does not\n * publish is backfilled by {@link ensureDesignTokens} — see the note there.\n */\nconst VIEW_CSS_TEXT = `\n/* ── Overlay + panel (ui-primitives Modal.module.css) ─────────────────── */\n.hqpu-overlay {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n /* The frame's own top inset keeps a tall card clear of the window chrome. */\n padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;\n}\n.hqpu-scrim {\n position: absolute;\n /* Only paint excludes the native caption; the root still blocks clicks. */\n inset: var(--dsh-frame-chrome-top, 0px) 0 0;\n background: var(--dsw-alias-bg-mask-1);\n animation: hqpu-fade var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);\n}\n.hqpu-panel {\n position: relative;\n z-index: 1;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: min(920px, 100%);\n max-height: 100%;\n overflow: hidden;\n border: 0;\n border-radius: var(--dsw-radius-panel);\n background: var(--dsw-alias-bg-layer-2);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-primary);\n animation: hqpu-panel-in var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);\n}\n.hqpu-panel:focus {\n outline: none;\n}\n@keyframes hqpu-fade {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes hqpu-panel-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n/* ── Head row (ui-primitives Modal .header) ─────────────────────────────── */\n.hqpu-head {\n display: flex;\n flex: none;\n flex-wrap: wrap;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n box-sizing: border-box;\n /* The official Title row: 22/14/12/24 — the toolbar sits 10px further right\n * than the body content on purpose, matching the shell's own close button. */\n padding: 22px 14px 12px 24px;\n}\n.hqpu-head-main {\n min-width: 0;\n max-width: 100%;\n}\n.hqpu-title {\n margin: 0;\n font-size: 20px;\n font-weight: 500;\n line-height: 28px;\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-intro {\n display: flex;\n align-items: center;\n gap: 4px;\n margin: 4px 0 0;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-info {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 20px;\n height: 20px;\n padding: 0;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-caption);\n cursor: pointer;\n}\n.hqpu-info:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-info:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n.hqpu-toolbar {\n display: flex;\n flex: none;\n align-items: center;\n gap: 16px;\n}\n\n/* ── Scrolling body: the page's centred column ────────────────────────── */\n.hqpu-scroll {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n box-sizing: border-box;\n /* The panel's 20px gap already separates this from the head and footer. */\n padding: 0 clamp(16px, 3vw, 32px) 4px;\n /* Anything that scrolls inside the panel draws the l2 thumb. */\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n}\n.hqpu-column {\n display: flex;\n flex-direction: column;\n gap: 24px;\n width: 100%;\n max-width: 880px;\n margin: 0 auto;\n}\n/* A stack of groups (or the single toast host) inside the column. */\n.hqpu-stack {\n display: flex;\n flex-direction: column;\n gap: 24px;\n}\n.hqpu-toast-host:empty {\n display: none;\n}\n\n/* ── Groups (PluginManagerPage .group) ────────────────────────────────── */\n.hqpu-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.hqpu-group-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.hqpu-group-title {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 14px;\n font-weight: 500;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-group-count {\n flex: none;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n/* ── Cards (PluginManagerPage .card) ──────────────────────────────────── */\n.hqpu-cards {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.hqpu-card {\n min-width: 0;\n /* The hover surface reaches 8px past the content on both sides. */\n margin: 0 -8px;\n border-radius: var(--dsw-radius-xl);\n}\n.hqpu-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.hqpu-card-head {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 8px;\n}\n.hqpu-card-icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-lg);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-card-icon svg {\n display: block;\n width: 24px;\n height: 24px;\n}\n.hqpu-card-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.hqpu-title-row {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n min-width: 0;\n}\n.hqpu-card-title {\n min-width: 0;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.hqpu-card-desc {\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 1;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.hqpu-card-end {\n position: relative;\n z-index: 1;\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: 8px;\n}\n.hqpu-card-reason {\n margin: 0;\n padding: 0 8px 8px 70px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-error-primary);\n overflow-wrap: anywhere;\n}\n\n/* ── Tags (ui-primitives Tag.module.css) ──────────────────────────────── */\n.hqpu-tag {\n display: inline-flex;\n align-items: center;\n flex: none;\n border-radius: 999px;\n corner-shape: round;\n padding: 1px 8px;\n font-size: 11px;\n font-weight: 500;\n line-height: 17px;\n white-space: nowrap;\n}\n.hqpu-tag[data-tone='success'] {\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);\n color: var(--dsw-alias-state-success-primary);\n}\n.hqpu-tag[data-tone='info'] {\n background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);\n color: var(--dsw-alias-state-business-primary);\n}\n.hqpu-tag[data-tone='warning'] {\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n}\n.hqpu-tag[data-tone='danger'] {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n color: var(--dsw-alias-state-error-primary);\n}\n.hqpu-tag[data-tone='neutral'] {\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ── Fact rows (PluginManagerPage .facts / .rows) ─────────────────────── */\n.hqpu-facts {\n margin: 0;\n display: flex;\n flex-direction: column;\n}\n.hqpu-facts > div {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n min-height: 44px;\n padding: 8px 2px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n}\n.hqpu-facts > div:last-child {\n border-bottom: 0;\n}\n.hqpu-facts dt {\n flex: none;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-facts dd {\n min-width: 0;\n margin: 0;\n font-size: 13px;\n line-height: 20px;\n text-align: end;\n overflow-wrap: anywhere;\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-facts code {\n padding: 2px 6px;\n border-radius: var(--dsw-radius-xs);\n background: var(--dsw-alias-bg-module-platform);\n font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: 12px;\n}\n\n/* ── Banner (PluginManagerPage .installSafety) ────────────────────────── */\n.hqpu-banner {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 10px 12px;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-state-warn-tertiary);\n font-size: 12.5px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-banner > svg {\n flex: none;\n margin-top: 2px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* ── Status, empty, failure (PluginManagerPage .status / .empty / .failure) */\n.hqpu-status {\n margin: 0;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n}\n.hqpu-status--dot {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n.hqpu-failure {\n display: flex;\n align-items: center;\n gap: 10px;\n color: var(--dsw-alias-state-error-primary);\n}\n.hqpu-empty {\n padding: 32px 0;\n text-align: center;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── State dot + spinner (ui-primitives StateDot.module.css) ──────────── */\n.hqpu-dot {\n position: relative;\n display: inline-block;\n flex: none;\n width: 10px;\n height: 10px;\n}\n.hqpu-dot::after {\n content: '';\n position: absolute;\n inset: 20%;\n border-radius: 50%;\n corner-shape: round;\n background: currentColor;\n}\n.hqpu-dot[data-state='done'] { color: var(--dsw-alias-state-success-primary); }\n.hqpu-dot[data-state='warning'] { color: var(--dsw-alias-state-warn-primary); }\n.hqpu-dot[data-state='error'] { color: var(--dsw-alias-state-error-primary); }\n.hqpu-dot[data-state='idle'] { color: var(--dsw-alias-state-idle-primary); }\n.hqpu-spinner {\n flex: none;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: currentColor;\n animation: hqpu-spin 1.5s linear infinite;\n}\n@keyframes hqpu-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ── Skeleton (PluginManagerPage .skeleton*) ──────────────────────────── */\n.hqpu-skeleton-fill {\n background: var(--dsw-alias-bg-skeleton);\n animation: hqpu-skeleton 2s cubic-bezier(0.36, 0, 0.64, 1) infinite;\n}\n.hqpu-skeleton-text {\n display: flex;\n align-items: center;\n height: 1lh;\n}\n.hqpu-skeleton-bar {\n width: 100%;\n height: 12px;\n border-radius: var(--dsw-radius-xs);\n}\n.hqpu-skeleton-heading { width: 96px; }\n.hqpu-skeleton-title { width: min(144px, 60%); }\n.hqpu-skeleton-desc { width: min(280px, 85%); }\n.hqpu-skeleton-icon { border-color: transparent; }\n@keyframes hqpu-skeleton {\n 0% { opacity: 1; }\n 40% { opacity: 0.6; }\n 80%, 100% { opacity: 1; }\n}\n\n/* ── Toast (ui-primitives Toast.module.css, held inside the panel) ────── */\n.hqpu-toast {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 16px;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-alias-toast-bg);\n color: var(--dsw-alias-toast-label);\n font-size: 14px;\n line-height: 22px;\n box-shadow: var(--dsw-shadow-lv3);\n animation: hqpu-toast-in 160ms ease-out;\n}\n.hqpu-toast-icon {\n display: grid;\n place-items: center;\n flex: none;\n color: var(--dsw-alias-state-warn-label);\n}\n.hqpu-toast-icon[data-tone='success'] { color: var(--dsw-alias-state-success-primary); }\n.hqpu-toast-text { min-width: 0; }\n@keyframes hqpu-toast-in {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: none; }\n}\n\n/* ── Buttons (PluginManagerPage .addButton / .iconButton) ─────────────── */\n.hqpu-icon-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 28px;\n height: 28px;\n padding: 0;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-caption);\n cursor: pointer;\n}\n.hqpu-icon-btn svg { display: block; }\n.hqpu-icon-btn:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-icon-btn:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.hqpu-icon-btn:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n.hqpu-btn {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 32px;\n padding: 0 12px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n cursor: pointer;\n}\n.hqpu-btn svg { display: block; }\n.hqpu-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.hqpu-btn:disabled { opacity: 0.5; cursor: default; }\n.hqpu-btn:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n.hqpu-btn--primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.hqpu-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }\n.hqpu-btn--outline {\n background: transparent;\n border: 0.5px solid var(--dsw-alias-border-l4);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-btn--outline:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.hqpu-btn--ghost { background: transparent; color: var(--dsw-alias-label-secondary); }\n.hqpu-btn--ghost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.hqpu-btn--danger { color: var(--dsw-alias-state-error-primary); }\n.hqpu-btn--sm { height: 28px; padding: 0 10px; font-size: 12.5px; }\n.hqpu-actions {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* ── Runtime update action row (arm → confirm step) ───────────────────── */\n.hqpu-runtime-actions {\n margin-top: 12px;\n}\n.hqpu-runtime-confirm {\n flex: 1 1 100%;\n margin: 0;\n}\n\n/* ── Runtime update source (fact row + editable URL field) ────────────── */\n.hqpu-source-line {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n max-width: 100%;\n}\n.hqpu-source-url {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.hqpu-source-editor {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 12px;\n}\n/* The plugin-source editor sits inside its own group right under the fact\n * row, so it needs less air than the runtime one under the facts block. */\n.hqpu-source-editor--plugin {\n margin-top: 4px;\n}\n.hqpu-source-hint {\n flex: 1 1 100%;\n margin: 0;\n}\n.hqpu-source-controls {\n flex: 1 1 100%;\n}\n.hqpu-source-input {\n flex: 1 1 240px;\n min-width: 0;\n height: 28px;\n box-sizing: border-box;\n padding: 0 10px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12.5px;\n}\n.hqpu-source-input:disabled {\n opacity: 0.5;\n}\n.hqpu-source-input:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n\n/* ── Footer action row (ui-primitives Modal .footer) ───────────────────── */\n.hqpu-footer {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 0 24px 20px;\n}\n\n.hqpu-visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n\n/* ── Reduced motion: drop every decorative animation ──────────────────── */\n@media (prefers-reduced-motion: reduce) {\n .hqpu-scrim,\n .hqpu-panel,\n .hqpu-skeleton-fill,\n .hqpu-spinner,\n .hqpu-toast {\n animation: none;\n }\n}\n`\n\nconst STYLE_TAG_ID = 'data-plugin-css=\"huaqiu-plugin-update\"'\n\n/**\n * Design tokens the bundled DSH shell may not publish.\n *\n * hq-edge ships an older `ui-theme` build. A live probe of the running shell\n * (both themes) shows 13 of the tokens this stylesheet reads are absent:\n * `--dsw-radius-{xs,sm,md,lg,xl,panel}`, `--dsw-focus-ring-width`,\n * `--dsw-focus-ring-color`, `--dsw-alias-toast-label`,\n * `--dsw-alias-state-idle-primary`, `--dsw-font-mono`,\n * `--dsh-frame-overlay-top` and `--dsh-frame-chrome-top`.\n *\n * An unresolved `var()` is not an error — it silently degrades the property to\n * its initial value. That is how this panel once rendered with\n * `border-radius: 0px` and a 3px `auto` focus ring while every colour was\n * already correct.\n *\n * The last three (`--dsw-font-mono`, `--dsh-frame-*`) are deliberately NOT\n * backfilled: the shell's own primitives rely on the inline `var(…, fallback)`\n * form for those (`Modal.module.css`, `PluginManagerPage.module.css`), so this\n * sheet does too. Everything else is backfilled below with the canonical value\n * from `ui-theme/src/styles/{base,focus,design-platform}.css`.\n *\n * Values are feature-detected at runtime and scoped to this plugin's own roots\n * rather than `:root`, so a newer shell always wins and filling a gap can never\n * restyle the host.\n */\n\n/** The two roots this plugin styles: the view overlay (built in\n * update-view.ts) and the sidebar row ({@link ENTRY_CSS}.entry). */\nconst STYLE_ROOTS = '.hqpu-overlay, .hqpu-sidebar-entry'\n\n/** The shell's dark-theme carrier — audited live as `body[data-ds-dark-theme]`. */\nconst DARK_THEME_SELECTOR = 'body[data-ds-dark-theme]'\n\n/** Same roots, one rule per theme, for tokens whose official value flips. */\nconst DARK_STYLE_ROOTS = STYLE_ROOTS.split(',')\n .map((root) => `${DARK_THEME_SELECTOR} ${root.trim()}`)\n .join(',')\n\ntype DesignTokenFallback =\n | { token: string; value: string }\n | { token: string; value: { light: string; dark: string } }\n\nconst DESIGN_TOKEN_FALLBACKS: DesignTokenFallback[] = [\n // base.css — geometry; theme-invariant.\n { token: '--dsw-radius-xs', value: '4px' },\n { token: '--dsw-radius-sm', value: '8px' },\n { token: '--dsw-radius-md', value: '12px' },\n { token: '--dsw-radius-lg', value: '16px' },\n { token: '--dsw-radius-xl', value: '20px' },\n { token: '--dsw-radius-panel', value: '28px' },\n // focus.css — the standard ring geometry. The colour already falls back\n // inline in the sheet; publishing it too keeps the ring intact if that\n // fallback is ever edited away.\n { token: '--dsw-focus-ring-width', value: '2px' },\n { token: '--dsw-focus-ring-color', value: 'var(--dsw-alias-state-business-primary, #C7000B)' },\n // design-platform.css — `--dsw-static-neutral-bluish-00` is white in both\n // themes, which is exactly what the official alias resolves to.\n { token: '--dsw-alias-toast-label', value: 'var(--dsw-static-neutral-bluish-00, #fff)' },\n // The one token whose official value flips with the theme. `--dsw-static-*`\n // are theme-invariant, so one rule per carrier reproduces neutral-300\n // (light) / neutral-600 (dark) without knowing the current theme.\n {\n token: '--dsw-alias-state-idle-primary',\n value: {\n light: 'var(--dsw-static-neutral-300, #d4d4d4)',\n dark: 'var(--dsw-static-neutral-600, #545557)',\n },\n },\n]\n\nconst TOKEN_STYLE_TAG_ID = 'data-plugin-css=\"huaqiu-plugin-update-tokens\"'\n\n/**\n * Publish canonical values for the design tokens the shell is missing.\n *\n * Idempotent and cheap: safe to call on every open, so a shell that publishes\n * its tokens late still wins (the stale fallbacks are removed in that case).\n */\nexport function ensureDesignTokens(): void {\n if (typeof document === 'undefined' || document.body === null) return\n const probe = getComputedStyle(document.body)\n const missing = DESIGN_TOKEN_FALLBACKS.filter(\n (entry) => probe.getPropertyValue(entry.token).trim() === '',\n )\n\n const existing = document.querySelector<HTMLStyleElement>(`style[${TOKEN_STYLE_TAG_ID}]`)\n if (missing.length === 0) {\n existing?.remove()\n return\n }\n\n const flat = missing.filter(\n (entry): entry is { token: string; value: string } => typeof entry.value === 'string',\n )\n const themed = missing.filter(\n (entry): entry is { token: string; value: { light: string; dark: string } } =>\n typeof entry.value !== 'string',\n )\n\n const rules: string[] = []\n const lightDecls = [\n ...flat.map((entry) => ` ${entry.token}: ${entry.value};`),\n ...themed.map((entry) => ` ${entry.token}: ${entry.value.light};`),\n ]\n rules.push(`${STYLE_ROOTS} {\\n${lightDecls.join('\\n')}\\n}`)\n if (themed.length > 0) {\n const darkDecls = themed.map((entry) => ` ${entry.token}: ${entry.value.dark};`)\n rules.push(`${DARK_STYLE_ROOTS} {\\n${darkDecls.join('\\n')}\\n}`)\n }\n\n const tag = existing ?? document.createElement('style')\n tag.dataset.pluginCss = 'huaqiu-plugin-update-tokens'\n tag.textContent = rules.join('\\n')\n if (tag.parentNode === null) document.head.appendChild(tag)\n}\n\n/** Inject styles once (idempotent). */\nexport function injectUpdateStyles(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.pluginCss = 'huaqiu-plugin-update'\n tag.textContent = `${ENTRY_CSS_TEXT}\\n${VIEW_CSS_TEXT}`\n document.head.appendChild(tag)\n}\n\n/** Minimal structural client context (dsh-client-runtime provides these). */\nexport interface ClientContext {\n get<T = unknown>(name: string): T | undefined\n effect?(fn: () => (() => void) | void, label?: string): void\n}\n\n/** In-memory update-report store shared by the badge and the modal. */\nclass UpdateStore implements UpdateStoreLike {\n private current: PluginUpdateReport | null = null\n private installingFlag = false\n private readonly listeners = new Set<() => void>()\n private refreshTimer: number | null = null\n\n report(): PluginUpdateReport | null {\n return this.current\n }\n\n installing(): boolean {\n return this.installingFlag\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /** Badge count per spec §6.3: actionable `update` + `install`, excluding the\n * ignored offer (the server already folds ignore into item states; keep the\n * client-side filter as a defensive second pass). */\n badgeCount(): number {\n const report = this.current\n if (report === null) return 0\n return report.items.filter(\n (item) => item.state === 'update' || item.state === 'install',\n ).length\n }\n\n private emit(): void {\n for (const listener of [...this.listeners]) {\n try {\n listener()\n } catch {\n /* one bad listener must not strand the rest */\n }\n }\n }\n\n async refresh(): Promise<PluginUpdateReport | null> {\n try {\n const report = await fetchReport()\n this.current = report\n this.emit()\n return report\n } catch {\n return this.current\n }\n }\n\n async check(): Promise<PluginUpdateReport | null> {\n try {\n const report = await checkUpdates()\n this.current = report\n this.emit()\n return report\n } catch {\n return this.current\n }\n }\n\n /** Mark the set as installing (set by the modal before the restart). */\n setInstalling(flag: boolean): void {\n this.installingFlag = flag\n this.emit()\n }\n\n /** Idle refresh while mounted (60 s) — no overlapping poll loop. */\n startIdleRefresh(): void {\n if (this.refreshTimer !== null) return\n this.refreshTimer = window.setInterval(() => {\n void this.refresh()\n }, 60_000) as unknown as number\n }\n\n stop(): void {\n if (this.refreshTimer !== null) {\n window.clearInterval(this.refreshTimer)\n this.refreshTimer = null\n }\n this.listeners.clear()\n }\n}\n\n/**\n * Client cordis inject: no service dependencies — the plugin reads the\n * hq-edge report purely through same-origin fetch (forwarded by its own node\n * half). `inject` stays empty so the loader never waits on another service.\n */\nexport const inject: string[] = []\n\n/**\n * Client plugin body — mount the sidebar toolbutton + review modal.\n * @param ctx - real cordis client context.\n * @returns disposer removing the row and all subscriptions.\n */\nexport function apply(ctx: ClientContext): () => void {\n if (typeof document === 'undefined') return () => {}\n injectUpdateStyles()\n ensureDesignTokens()\n\n const store = new UpdateStore()\n const copy = { zh: COPY.zh, en: COPY.en }\n const copyFor = (): typeof COPY.zh => (locale === 'en' ? copy.en : copy.zh)\n\n // Locale: re-render the row copy when <html lang> changes. The sidebar\n // helper subscribes via `refresh.subscribe`; we fan out to its listener.\n let locale: HostLocale | undefined = detectHostLocale()\n const localeSubscribers = new Set<() => void>()\n const refreshSubscribe = (listener: () => void): (() => void) => {\n localeSubscribers.add(listener)\n return () => {\n localeSubscribers.delete(listener)\n }\n }\n const unsubscribeLocale = observeHostLocale((next) => {\n locale = next\n for (const fn of [...localeSubscribers]) {\n try {\n fn()\n } catch {\n /* one bad subscriber must not strand the rest */\n }\n }\n })\n\n const openView = (): void => {\n // Re-detect the shell's design tokens on every open: a shell that\n // publishes them after this plugin mounted must win, and one that never\n // does must keep the canonical fallbacks.\n ensureDesignTokens()\n // Always re-check on open so the review screen never shows stale data.\n void store.check().then((report) => {\n if (report !== null) openUpdateView(store, copyFor())\n })\n }\n\n const disposeEntry = mountSidebarEntry({\n rowAttribute: UPDATE_ENTRY_ATTRIBUTE,\n rowSelector: UPDATE_ENTRY_SELECTOR,\n plugin: PLUGIN_ID,\n icon: UPDATE_ICON,\n css: ENTRY_CSS,\n // Read the live locale on every refresh (the locale observer fires\n // `refresh.subscribe`'s listener, which re-applies these) so the row\n // re-localizes when the host UI language changes — matching the sibling\n // family rows, which read a live `currentLang` variable the same way.\n label: () => entryCopy(locale).label,\n tooltip: () => entryCopy(locale).tooltip,\n refresh: { subscribe: refreshSubscribe },\n badge: {\n subscribe: (listener) => store.subscribe(listener),\n count: () => store.badgeCount(),\n },\n onToggle: openView,\n position: 'after',\n familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY,\n })\n\n store.startIdleRefresh()\n void store.refresh()\n\n const dispose = (): void => {\n try {\n disposeEntry()\n } catch {\n /* already disposed */\n }\n unsubscribeLocale()\n store.stop()\n }\n if (typeof ctx.effect === 'function') {\n // `ctx.effect` takes a SETUP function and calls it immediately, keeping\n // whatever it returns as the teardown. Passing `dispose` itself would run\n // the teardown synchronously — disconnecting the sidebar-entry observers\n // and stopping the store before the shell ever renders the sidebar, so the\n // row could never be placed. Hand it a factory that returns the teardown\n // instead (the same shape the sibling tool plugins use).\n ctx.effect(() => dispose, 'plugin-update')\n }\n return dispose\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../dsh-sidebar-entry/lib/index.mjs","../src/client/version.ts","../src/client/api.ts","../src/client/copy.ts","../src/client/icons.ts","../src/client/update-view.ts","../src/client/index.ts"],"sourcesContent":["//#region src/index.ts\n/** Find the sidebar shell root element, or undefined while not yet mounted. */\nfunction sidebarRoot() {\n\tconst column = document.querySelector(\"[data-pane=\\\"sidebar\\\"], [class*=\\\"sidebarCol\\\"]\");\n\tif (column === null) return void 0;\n\treturn column.querySelector(\"[class*=\\\"logoRow\\\"]\")?.parentElement ?? column.firstElementChild;\n}\n/** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */\nfunction newSessionButton(root) {\n\tconst nested = root.querySelector(\"button[class*=\\\"newSession\\\"]\");\n\tif (nested !== null) return nested;\n\tfor (const child of root.children) if (child.tagName === \"BUTTON\") return child;\n}\n/** Build the entry row (a detached button; insert once the shell is up). */\nfunction createEntry(options) {\n\tconst entry = document.createElement(\"button\");\n\tentry.type = \"button\";\n\tentry.setAttribute(options.rowAttribute, \"\");\n\tif (options.plugin !== void 0) {\n\t\tentry.setAttribute(\"data-dsh-plugin\", options.plugin);\n\t\tentry.setAttribute(\"data-dsh-part\", \"sidebar-entry\");\n\t}\n\tentry.className = options.css[\"entry\"] ?? \"\";\n\tconst labelSpan = document.createElement(\"span\");\n\tlabelSpan.className = options.css[\"entryLabel\"] ?? \"\";\n\tconst iconSpan = document.createElement(\"span\");\n\ticonSpan.className = options.css[\"entryIcon\"] ?? \"\";\n\ticonSpan.innerHTML = options.icon;\n\tentry.append(iconSpan, labelSpan);\n\tlet badgeSpan = null;\n\tconst applyBadge = () => {\n\t\tif (options.badge === void 0) return;\n\t\tconst count = options.badge.count();\n\t\tif (count <= 0) {\n\t\t\tbadgeSpan?.remove();\n\t\t\tbadgeSpan = null;\n\t\t\treturn;\n\t\t}\n\t\tif (badgeSpan === null) {\n\t\t\tbadgeSpan = document.createElement(\"span\");\n\t\t\tbadgeSpan.className = options.css[\"entryBadge\"] ?? \"\";\n\t\t\tentry.append(badgeSpan);\n\t\t}\n\t\tbadgeSpan.textContent = String(count);\n\t\tentry.setAttribute(\"data-has-badge\", \"\");\n\t};\n\tconst applyLabel = () => {\n\t\tentry.setAttribute(\"aria-label\", options.label());\n\t\tif (options.tooltip !== void 0) entry.setAttribute(\"title\", options.tooltip());\n\t\tlabelSpan.textContent = options.label();\n\t};\n\tapplyLabel();\n\tapplyBadge();\n\tentry.addEventListener(\"click\", options.onToggle);\n\treturn {\n\t\tentry,\n\t\tapplyLabel,\n\t\tapplyBadge\n\t};\n}\n/**\n* Re-insert the entry after the New Session row, in the family's intended\n* order.\n*\n* The `familySelectors` array order is the single source of truth for visual\n* order, so the rendered order is stable regardless of which plugin mounts\n* first or of observer callback order — a re-render can never swap two rows.\n* (Earlier builds used the `position: 'before' | 'after'` convention, which\n* clustered rows by mount order and let a late-mounting `before` row steal the\n* lead. The array order supersedes it; `position` remains only as a fallback\n* for a row whose selector is absent from the family list.)\n*/\nfunction placeEntry(root, entry, options) {\n\tconst button = newSessionButton(root);\n\tif (button === void 0) return false;\n\tif (entry.parentElement !== root) {\n\t\tconst row = button.closest(\"[class*=\\\"logoRow\\\"]\");\n\t\tconst base = row !== null && row.parentElement === root ? row : button;\n\t\tconst order = options.familySelectors;\n\t\tconst present = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(order.join(\", \"))).map((el) => ({\n\t\t\tel,\n\t\t\tidx: order.findIndex((sel) => el.matches(sel))\n\t\t}));\n\t\tconst selfIndex = order.indexOf(options.rowSelector);\n\t\tlet anchor;\n\t\tif (selfIndex === -1) anchor = options.position === \"before\" ? present.length > 0 ? present[0].el : base.nextElementSibling : present.length > 0 ? present[present.length - 1].el.nextElementSibling : base.nextElementSibling;\n\t\telse {\n\t\t\tconst later = present.find((member) => member.idx > selfIndex);\n\t\t\tif (later !== void 0) anchor = later.el;\n\t\t\telse {\n\t\t\t\tconst earlier = [...present].reverse().find((member) => member.idx < selfIndex);\n\t\t\t\tanchor = earlier !== void 0 ? earlier.el.nextElementSibling : base.nextElementSibling;\n\t\t\t}\n\t\t}\n\t\troot.insertBefore(entry, anchor ?? null);\n\t}\n\treturn true;\n}\n/**\n* Mount the sidebar entry, waiting for the shell to render and self-healing\n* on later React re-renders.\n* @param options - the row's attribute/icon/copy/action/ordering configuration.\n* @returns disposer removing the entry and its observers.\n*/\nfunction mountSidebarEntry(options) {\n\tif (typeof document !== \"undefined\" && document.querySelector(options.rowSelector) !== null) return () => {};\n\tconst { entry, applyLabel, applyBadge } = createEntry(options);\n\tlet root;\n\tlet placed = false;\n\tlet unsubscribeRefresh;\n\tif (options.refresh !== void 0) try {\n\t\tunsubscribeRefresh = options.refresh.subscribe(applyLabel);\n\t} catch {}\n\tlet unsubscribeBadge;\n\tif (options.badge !== void 0) try {\n\t\tunsubscribeBadge = options.badge.subscribe(applyBadge);\n\t} catch {}\n\tconst tryPlace = () => {\n\t\tif (root !== void 0 && !root.isConnected) {\n\t\t\trootObserver.disconnect();\n\t\t\troot = void 0;\n\t\t\tplaced = false;\n\t\t}\n\t\tif (placed) {\n\t\t\tif (document.body.contains(entry)) return;\n\t\t\trootObserver.disconnect();\n\t\t\troot = void 0;\n\t\t\tplaced = false;\n\t\t}\n\t\troot ??= sidebarRoot();\n\t\tif (root === void 0) return;\n\t\tplaced = placeEntry(root, entry, options);\n\t\tif (placed) rootObserver.observe(root, {\n\t\t\tchildList: true,\n\t\t\tsubtree: true\n\t\t});\n\t};\n\tconst waitObserver = new MutationObserver(() => {\n\t\ttryPlace();\n\t});\n\twaitObserver.observe(document.body, {\n\t\tchildList: true,\n\t\tsubtree: true\n\t});\n\tconst rootObserver = new MutationObserver(() => {\n\t\tif (root === void 0 || !root.isConnected) {\n\t\t\tplaced = false;\n\t\t\ttryPlace();\n\t\t\treturn;\n\t\t}\n\t\tif (!root.contains(entry)) placed = placeEntry(root, entry, options);\n\t});\n\toptions.active === void 0 || (() => {\n\t\tconst syncActive = () => {\n\t\t\tif (options.active.isOpen()) entry.dataset.active = \"true\";\n\t\t\telse delete entry.dataset.active;\n\t\t};\n\t\tconst unsubscribe = options.active.subscribe(syncActive);\n\t\tsyncActive();\n\t\treturn unsubscribe;\n\t})();\n\ttryPlace();\n\treturn () => {\n\t\twaitObserver.disconnect();\n\t\trootObserver.disconnect();\n\t\tunsubscribeRefresh?.();\n\t\tunsubscribeBadge?.();\n\t\tentry.remove();\n\t};\n}\n/**\n* Every row selector in the Huaqiu sidebar family, in intended visual order.\n*\n* Family ordering is only stable when every participating plugin passes the\n* SAME list to {@link mountSidebarEntry} — the core positions a row relative to\n* the family block, never relative to transient shell geometry. Adding a new\n* family row means adding its selector here (and to no plugin-local list).\n*/\nconst HUAQIU_SIDEBAR_ENTRY_FAMILY = [\n\t\"[data-hq-plugin-update-entry]\",\n\t\"[data-hqcg-footprint-entry]\",\n\t\"[data-hqcg-symbol-entry]\",\n\t\"[data-hqcg-schematic-entry]\",\n\t\"[data-hq-dummy-entry]\"\n];\n/**\n* Read the host UI language from `<html lang>`.\n*\n* `dsh-client-locale` rewrites the attribute on every language change (dsh\n* web) and HQ Edge mirrors it, so this is the one source every family plugin\n* can read without importing a `locale` service.\n* @returns 'zh' / 'en', or undefined when the tag is absent or unsupported\n* (callers fall back to their own default).\n*/\nfunction detectHostLocale() {\n\tif (typeof document === \"undefined\") return void 0;\n\tconst tag = document.documentElement?.getAttribute(\"lang\");\n\tif (tag === null || tag === void 0 || tag === \"\") return void 0;\n\tconst primary = tag.toLowerCase().split(\"-\")[0];\n\treturn primary === \"zh\" || primary === \"en\" ? primary : void 0;\n}\n/**\n* Watch `<html lang>` and report every resolved change.\n*\n* Idempotent-safe: repeated calls share one observer and each caller gets its\n* own disposer; the observer disconnects when the last listener leaves.\n* @param listener - called with the newly resolved language.\n* @returns disposer removing this listener.\n*/\n/** Shared observer/listener set backing {@link observeHostLocale}. */\nconst localeListeners = /* @__PURE__ */ new Set();\nlet localeObserver = null;\nfunction observeHostLocale(listener) {\n\tif (typeof document === \"undefined\" || typeof MutationObserver === \"undefined\" || document.documentElement === null) return () => {};\n\tlet current = detectHostLocale();\n\tconst listeners = localeListeners;\n\tlisteners.add(listener);\n\tif (localeObserver === null) {\n\t\tlocaleObserver = new MutationObserver(() => {\n\t\t\tconst next = detectHostLocale();\n\t\t\tif (next === current) return;\n\t\t\tcurrent = next;\n\t\t\tfor (const fn of [...listeners]) try {\n\t\t\t\tfn(next);\n\t\t\t} catch {}\n\t\t});\n\t\tlocaleObserver.observe(document.documentElement, {\n\t\t\tattributes: true,\n\t\t\tattributeFilter: [\"lang\"]\n\t\t});\n\t}\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t\tif (listeners.size === 0 && localeObserver !== null) {\n\t\t\tlocaleObserver.disconnect();\n\t\t\tlocaleObserver = null;\n\t\t}\n\t};\n}\n//#endregion\nexport { HUAQIU_SIDEBAR_ENTRY_FAMILY, detectHostLocale, mountSidebarEntry, observeHostLocale };\n","/**\n * SemVer direction gate for plugin-update items (browser half).\n *\n * The rule this module exists to enforce, for EVERY plugin in the report:\n * an item is only \"update available\" — and only gets an install control —\n * when the version offered by the release manifest is strictly GREATER than\n * the version installed in the active bundle. A manifest entry that merely\n * differs from the installed version offers nothing: equality is the\n * already-installed case, and a manifest that LAGS behind the installed\n * build (a stale mirror or a rollback channel publishing 0.6.2 against an\n * installed 0.6.4) must never surface as a downgrade \"update\".\n *\n * Why this lives on the client even though hq-edge owns the decision: the\n * report crosses a process boundary, and an older hq-edge still carries the\n * old \"differs ⇒ update\" rule — which rendered `analytics 可更新\n * 0.6.4 → 0.6.2` with a live 安装 button for the whole plugin set. This is\n * the same defensive second pass the badge already applies to the ignored\n * offer: one authoritative decision upstream, one guard where it is shown.\n *\n * The comparator is a strict SemVer 2.0.0 implementation (no dependency —\n * this package ships no runtime dependencies beyond the workspace log +\n * sidebar helpers). Anything that is not clean SemVer on either side makes\n * the comparison `null`, and an unprovable direction fails CLOSED: the item\n * reads as current, never as an update.\n *\n * @module\n */\nimport type { PluginUpdateItem, PluginUpdateReport } from './api.js'\n\n/** Strict SemVer 2.0.0: MAJOR.MINOR.PATCH[-prerelease][+build]. */\nconst SEMVER_RE =\n /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/\n\ninterface ParsedVersion {\n major: number\n minor: number\n patch: number\n /** Prerelease identifiers (empty = a release, which outranks any prerelease). */\n prerelease: string[]\n}\n\n/** Parse a strict SemVer string, or null when it is not one. */\nfunction parseSemver(version: string): ParsedVersion | null {\n const match = SEMVER_RE.exec(version.trim())\n if (match === null) return null\n return {\n major: Number(match[1]),\n minor: Number(match[2]),\n patch: Number(match[3]),\n prerelease: match[4] === undefined ? [] : match[4].split('.'),\n }\n}\n\n/** True when one prerelease identifier is purely numeric. */\nfunction isNumericIdentifier(id: string): boolean {\n return /^\\d+$/.test(id)\n}\n\n/**\n * Compare two prerelease identifier lists per SemVer §11:\n * numeric identifiers compare numerically and rank below alphanumeric ones;\n * a larger set of identifiers ranks higher when all shared ones are equal.\n */\nfunction comparePrerelease(a: string[], b: string[]): number {\n // No prerelease outranks any prerelease.\n if (a.length === 0 && b.length === 0) return 0\n if (a.length === 0) return 1\n if (b.length === 0) return -1\n const shared = Math.min(a.length, b.length)\n for (let index = 0; index < shared; index += 1) {\n const left = a[index]\n const right = b[index]\n if (left === undefined || right === undefined) continue\n if (left === right) continue\n const leftNumeric = isNumericIdentifier(left)\n const rightNumeric = isNumericIdentifier(right)\n if (leftNumeric && rightNumeric) {\n return Number(left) < Number(right) ? -1 : 1\n }\n if (leftNumeric !== rightNumeric) {\n // Numeric identifiers always have lower precedence than alphanumeric.\n return leftNumeric ? -1 : 1\n }\n return left < right ? -1 : 1\n }\n return a.length === b.length ? 0 : a.length < b.length ? -1 : 1\n}\n\n/**\n * Compare two strict SemVer strings: negative when `a < b`, positive when\n * `a > b`, 0 when equal, and null when either side is not SemVer (build\n * metadata is ignored in precedence, per spec).\n */\nexport function compareSemver(a: string, b: string): number | null {\n const left = parseSemver(a)\n const right = parseSemver(b)\n if (left === null || right === null) return null\n if (left.major !== right.major) return left.major < right.major ? -1 : 1\n if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1\n if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1\n return comparePrerelease(left.prerelease, right.prerelease)\n}\n\n/**\n * Whether the offered release is an actionable upgrade over the installed\n * version: an absent plugin (nothing installed) is always installable, and\n * an installed one only when the offered SemVer is strictly greater. An\n * unprovable direction (non-SemVer on either side) is NOT an upgrade.\n */\nexport function isVersionUpgrade(currentVersion: string | null, availableVersion: string): boolean {\n if (currentVersion === null) return true\n const order = compareSemver(availableVersion, currentVersion)\n return order !== null && order > 0\n}\n\n/**\n * Re-evaluate one report item against the direction rule.\n *\n * A server-reported `update` that is not a strict SemVer upgrade is demoted\n * to `current`: the plugin is at (or ahead of) the offered release, there is\n * nothing to install, and no install control may be offered for it. Every\n * other state passes through untouched — `install` (plugin absent locally)\n * has no local version to compare, and `blocked`/`ignored`/`current` are\n * already non-actionable. Unknown server fields are preserved verbatim: the\n * client stays a thin reader of the hq-edge report shape.\n */\nexport function normalizePluginUpdateItem<T extends PluginUpdateItem>(item: T): T {\n if (item.state !== 'update') return item\n if (isVersionUpgrade(item.currentVersion, item.availableVersion)) return item\n return { ...item, state: 'current' }\n}\n\n/** States that carry an install action (the badge/actionable definition). */\nfunction isActionableState(item: PluginUpdateItem): boolean {\n return item.state === 'update' || item.state === 'install'\n}\n\n/**\n * Apply {@link normalizePluginUpdateItem} to a whole report and recompute\n * `actionableCount` from the corrected states, so the server's count can\n * never disagree with what the panel and the sidebar badge render.\n */\nexport function normalizePluginUpdateReport<T extends PluginUpdateReport>(report: T): T {\n if (!Array.isArray(report.items)) return report\n const items = report.items.map(normalizePluginUpdateItem)\n return {\n ...report,\n items,\n actionableCount: items.filter(isActionableState).length,\n }\n}\n","/**\n * Plugin-update API client (browser half).\n *\n * Every call is a same-origin fetch against the DSH webServer prefix route\n * (`/api/v1/huaqiu/plugin-update/…`) which the node half forwards to hq-edge.\n * Response shapes mirror hq-edge `apps/server/src/routes/plugin-updates.ts`.\n *\n * @module\n */\nimport { normalizePluginUpdateReport } from './version.js'\n\nexport const UPDATE_BASE = '/api/v1/huaqiu/plugin-update'\n\nexport interface PluginUpdateItem {\n name: string\n /** Version installed in the active bundle; null when the plugin is absent. */\n currentVersion: string | null\n availableVersion: string\n /**\n * Server state. `update` is re-checked on arrival against the SemVer\n * direction rule (`version.ts`): an offer that is not strictly newer than\n * `currentVersion` is demoted to `current`, so the panel and the badge can\n * never offer a downgrade even against an older hq-edge.\n */\n state: 'current' | 'update' | 'install' | 'blocked' | 'ignored'\n /** The block reason, or null when the item is not blocked. The server sends a\n * single code (`\"runtime-unknown\"`), so it is normalized before use. */\n blockedBy?: string | string[] | null\n ignored?: boolean\n reasons?: string[]\n notes?: string[]\n}\n\n/**\n * One downloadable HQ Edge runtime distribution, as hq-edge reports it\n * (`RuntimeUpdateEntry` in `apps/server/src/dsh/PluginUpdateService.ts`).\n *\n * Note the shape: `runtime.latest` is this ENTRY OBJECT, not a bare version\n * string. Reading it as a string is what once rendered \"[object Object]\".\n */\nexport interface RuntimeUpdateEntry {\n /** Clean SemVer of the available distribution. */\n version: string\n /** SHA-256 of the release archive, lowercase hex (64 chars). */\n sha256: string\n /** Platform the archive targets: win | darwin | linux | all. */\n platform: string\n /** Optional CPU architecture: x64 | arm64 (absent = any). */\n arch?: string\n /** Exact absolute HTTPS URL of the release archive. */\n url: string\n}\n\nexport interface RuntimeUpdateInfo {\n configured: boolean\n currentVersion?: string | null\n /** The latest compatible distribution, or null when the manifest had none. */\n latest?: RuntimeUpdateEntry | null\n updateAvailable: boolean\n checkedAt?: string | null\n lastError?: string | null\n /**\n * Where the runtime-update manifest is read from. Optional: an older hq-edge\n * does not send it, and the panel must degrade to a read-only source row.\n */\n source?: RuntimeUpdateSource\n}\n\n/**\n * The runtime-update manifest source, editable from this panel.\n *\n * `origin` says which URL is in effect: `override` is a URL the user set here\n * (persisted by hq-edge per host), `config` is the packaged/env default, and\n * `unset` means runtime updates are not configured at all.\n */\nexport interface RuntimeUpdateSource {\n /** The manifest URL currently in effect; empty when none is known. */\n url: string\n origin: 'override' | 'config' | 'unset'\n /** The URL configuration supplies, independent of any user override. */\n configuredUrl: string\n /** True when a user override is shadowing the configured URL. */\n override: boolean\n}\n\n/**\n * The plugin release-manifest source (the gitee\n * `plugins-release-manifest.json` by default) — same shape and same edit flow\n * as {@link RuntimeUpdateSource}, because hq-edge serves both from one shared\n * type.\n */\nexport type PluginUpdateSource = RuntimeUpdateSource\n\n/**\n * The latest runtime version to display, or null when there is none.\n *\n * hq-edge sends the full release entry; an older or future server could send\n * a bare string. Both are accepted here so the facts row can never again fall\n * back to `String(object)` — i.e. \"[object Object]\" — in the DOM.\n */\nexport function runtimeLatestVersion(\n runtime: RuntimeUpdateInfo | null | undefined,\n): string | null {\n const latest: unknown = runtime?.latest\n if (typeof latest === 'string') return latest.length > 0 ? latest : null\n if (latest !== null && typeof latest === 'object' && 'version' in latest) {\n const version = (latest as { version?: unknown }).version\n if (typeof version === 'string' && version.length > 0) return version\n }\n return null\n}\n\nexport interface PluginUpdateReport {\n /** The evaluated release set; null while no manifest has been read. */\n manifestVersion: string | null\n pluginSetVersion?: string\n items: PluginUpdateItem[]\n actionableCount: number\n ignoredSet: boolean\n fetchedAt?: string\n lastError?: string | null\n runtime: RuntimeUpdateInfo\n /**\n * Where the plugin release manifest is read from. Optional: an older\n * hq-edge does not send it, and the panel must degrade to no source row.\n */\n pluginSource?: PluginUpdateSource\n}\n\nasync function request<T>(path: string, init?: RequestInit): Promise<T> {\n const res = await fetch(`${UPDATE_BASE}${path}`, {\n cache: 'no-store',\n ...init,\n headers: { 'content-type': 'application/json; charset=utf-8', ...(init?.headers ?? {}) },\n })\n const body = (await res.json().catch(() => null)) as T | null\n if (!res.ok) {\n const err = (body as { error?: string } | null)?.error ?? `HTTP ${res.status}`\n throw new Error(err)\n }\n if (body === null) throw new Error('empty response')\n return body\n}\n\n/** GET the cached (or empty-shell) plugin-update report. */\nexport function fetchReport(): Promise<PluginUpdateReport> {\n return request<PluginUpdateReport>('/report').then(normalizePluginUpdateReport)\n}\n\n/** POST an explicit refresh; returns the freshly computed report. */\nexport function checkUpdates(): Promise<PluginUpdateReport> {\n return request<PluginUpdateReport>('/check', { method: 'POST' }).then(normalizePluginUpdateReport)\n}\n\nexport interface InstallResult {\n ok: boolean\n installedSetVersion?: string\n installed?: number\n skipped?: number\n restarting?: boolean\n error?: string\n}\n\n/** POST the install transaction for the confirmed set version. */\nexport function installSet(setVersion: string): Promise<InstallResult> {\n return request<InstallResult>('/install', {\n method: 'POST',\n body: JSON.stringify({ setVersion }),\n })\n}\n\n/** POST ignore for the current offer (set-scoped, persists across restarts). */\nexport function ignoreOffer(setVersion: string): Promise<unknown> {\n return request<unknown>('/ignore', { method: 'POST', body: JSON.stringify({ setVersion }) })\n}\n\n/**\n * The outcome of a user-confirmed HQ Edge runtime update.\n *\n * hq-edge answers `202 { requestId, targetVersion, state: 'accepted' }` and\n * then shuts itself down for the EDA host to swap the runtime, so a success\n * here means \"accepted\", not \"installed\". Failures arrive as 409 (already\n * updating → `busy`), 422 (preflight/manifest/download refused) or 503.\n */\nexport interface RuntimeUpdateRequestResult {\n requestId?: string\n targetVersion?: string\n state?: string\n ok?: boolean\n error?: string\n busy?: boolean\n}\n\n/** POST the user-confirmed HQ Edge runtime update for one target version. */\nexport function requestRuntimeUpdate(\n targetVersion: string,\n): Promise<RuntimeUpdateRequestResult> {\n return request<RuntimeUpdateRequestResult>('/runtime-request', {\n method: 'POST',\n body: JSON.stringify({ targetVersion }),\n })\n}\n\n/** POST unignore for the set version (reverses an ignore). */\nexport function unignoreOffer(setVersion: string): Promise<unknown> {\n return request<unknown>('/unignore', { method: 'POST', body: JSON.stringify({ setVersion }) })\n}\n\n/**\n * GET the runtime-update manifest source: the URL in effect, its origin\n * (`override` | `config` | `unset`) and the configured URL behind it.\n *\n * Read-only upstream, so this is open like `/report` — only the mutation\n * below is loopback-guarded (and reaches it same-origin anyway).\n */\nexport function getRuntimeUpdateSource(): Promise<RuntimeUpdateSource> {\n return request<RuntimeUpdateSource>('/runtime-source')\n}\n\n/**\n * POST a new runtime-update manifest URL, or `\"\"` to clear a user override\n * and fall back to the configured URL.\n *\n * hq-edge validates the URL itself (`https` required except for loopback, no\n * embedded credentials) and answers 422 with a message meant for a person —\n * this client surfaces it verbatim rather than pre-validating, so the rule\n * lives in exactly one place. A successful answer is the NEW source; the\n * caller must then re-run discovery, because the cached report still names\n * the old manifest's offer.\n */\nexport function setRuntimeUpdateSource(url: string): Promise<RuntimeUpdateSource> {\n return request<RuntimeUpdateSource>('/runtime-source', {\n method: 'POST',\n body: JSON.stringify({ url }),\n })\n}\n\n/**\n * GET the plugin release-manifest source: the URL discovery reads, its origin\n * (`override` | `config` | `unset`) and the configured URL behind it. Same\n * open/read-only contract as the runtime source.\n */\nexport function getPluginUpdateSource(): Promise<PluginUpdateSource> {\n return request<PluginUpdateSource>('/plugin-source')\n}\n\n/**\n * POST a new plugin release-manifest URL, or `\"\"` to clear a user override.\n *\n * hq-edge persists the override per host AND re-runs discovery in the\n * background, so the next report (and the sidebar badge) already reflect the\n * new manifest. This client still re-checks explicitly after a save — the\n * background pass is a safety net for other consumers, not a substitute.\n */\nexport function setPluginUpdateSource(url: string): Promise<PluginUpdateSource> {\n return request<PluginUpdateSource>('/plugin-source', {\n method: 'POST',\n body: JSON.stringify({ url }),\n })\n}\n","/**\n * Localized copy for the plugin-update UI (browser half).\n *\n * zh is the fallback, matching the rest of the Huaqiu plugin family\n * (`<html lang>` drives the resolution via the shared sidebar-entry locale\n * helpers). The wording follows the shell's own plugin manager\n * (`ui-plugin-manager/src/client/locales.ts`) so both read as one product.\n *\n * @module\n */\n\nconst zh = {\n entryLabel: '插件更新',\n entryTooltip: '查看可用的插件更新',\n viewTitle: '插件更新',\n viewIntro: '检查并安装可用的插件更新',\n viewInfo: '在这里查看插件集的更新。标注为「被阻止」的插件因主机或 HQ Edge 版本不兼容而无法安装;已是最新的插件无需操作。',\n refresh: '刷新',\n close: '关闭',\n loading: '正在读取插件更新…',\n empty: '没有可用的插件更新',\n checkFailed: '检查更新失败,请重试',\n installAll: (count: number) => `安装 ${count} 项更新`,\n installOne: '安装',\n installing: '正在安装…(DSH 将自动重启)',\n nothingToInstall: '暂无可安装的更新',\n ignore: '忽略此版本',\n unignore: '恢复提醒',\n ignoredOffer: '已忽略当前插件集更新',\n groupCount: (count: number) => `${count} 个插件`,\n group: {\n actionable: '待处理',\n blocked: '无法安装',\n ignored: '已忽略',\n current: '已是最新',\n },\n state: {\n current: '当前',\n update: '可更新',\n install: '待安装',\n ignored: '已忽略',\n blocked: '被阻止',\n },\n blockedReason: {\n host: '不适用于当前 EDA 主机',\n 'hq-edge-version': '当前 HQ Edge 版本不兼容',\n 'runtime-unknown': 'HQ Edge 版本未知,无法校验',\n },\n runtime: {\n title: 'HQ Edge 运行时',\n current: '当前版本',\n latest: '最新版本',\n set: '发布插件集',\n checkedAt: '上次检查',\n source: '更新源',\n unknown: '版本未知',\n notConfigured: '未配置更新源',\n note: '运行时更新由 EDA 安装器处理',\n update: '安装运行时更新',\n updateConfirm: (version: string) =>\n `安装 HQ Edge 运行时 ${version}?HQ Edge 会先关闭并完成替换,然后重新启动。`,\n updating: '正在下载并校验运行时包,可能需要一分钟…',\n updateAccepted: '运行时更新已受理,HQ Edge 正在重启…',\n // ── Update-source editor ──\n sourceHint: '运行时更新清单的下载地址,保存后立即生效',\n sourceEdit: '修改',\n sourceSave: '保存',\n sourceReset: '恢复默认',\n saved: '更新源已保存,正在重新检查更新…',\n saveFailed: '保存更新源失败',\n sourceInputLabel: '运行时更新清单地址',\n sourcePlaceholder: 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json',\n },\n // ── Plugin release-manifest source editor ──\n pluginSource: {\n title: '插件更新源',\n hint: '插件更新清单的下载地址,保存后立即生效',\n edit: '修改',\n save: '保存',\n reset: '恢复默认',\n saved: '更新源已保存,正在重新检查更新…',\n saveFailed: '保存更新源失败',\n inputLabel: '插件更新清单地址',\n placeholder: 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json',\n },\n // Origin tags, shared by both source editors (hq-edge serves one type).\n originOverride: '自定义',\n originConfig: '默认配置',\n originUnset: '未配置',\n lastError: '上次检查失败',\n installError: '安装失败',\n installSuccess: '更新已安装,正在重启 DSH…',\n confirmInstall: '确认安装',\n cancel: '取消',\n}\n\nexport type UpdateCopy = typeof zh\n\nconst en: UpdateCopy = {\n entryLabel: 'Plugin Updates',\n entryTooltip: 'Review available plugin updates',\n viewTitle: 'Plugin Updates',\n viewIntro: 'Check for and install available plugin updates',\n viewInfo: 'Review the plugin set updates here. A plugin marked Blocked cannot be installed because its host or HQ Edge version is incompatible; a plugin marked Current needs no action.',\n refresh: 'Refresh',\n close: 'Close',\n loading: 'Reading plugin updates…',\n empty: 'No plugin updates available',\n checkFailed: 'Failed to check for updates — please retry',\n installAll: (count: number) => `Install ${count} update${count === 1 ? '' : 's'}`,\n installOne: 'Install',\n installing: 'Installing… (DSH will restart)',\n nothingToInstall: 'Nothing to install right now',\n ignore: 'Ignore this release',\n unignore: 'Show again',\n ignoredOffer: 'The current plugin-set update is ignored',\n groupCount: (count: number) => `${count} plugin${count === 1 ? '' : 's'}`,\n group: {\n actionable: 'Actionable',\n blocked: 'Blocked',\n ignored: 'Ignored',\n current: 'Up to date',\n },\n state: {\n current: 'Current',\n update: 'Update',\n install: 'Install',\n ignored: 'Ignored',\n blocked: 'Blocked',\n },\n blockedReason: {\n host: 'Not applicable to the current EDA host',\n 'hq-edge-version': 'Incompatible with the current HQ Edge version',\n 'runtime-unknown': 'HQ Edge version unknown — cannot verify',\n },\n runtime: {\n title: 'HQ Edge Runtime',\n current: 'Current version',\n latest: 'Latest version',\n set: 'Release set',\n checkedAt: 'Last checked',\n source: 'Update source',\n unknown: 'Version unknown',\n notConfigured: 'No update source configured',\n note: 'Runtime updates are handled by the EDA installer',\n update: 'Install runtime update',\n updateConfirm: (version: string) =>\n `Install HQ Edge runtime ${version}? HQ Edge shuts down to swap the runtime, then restarts.`,\n updating: 'Downloading and verifying the runtime bundle — this can take a minute…',\n updateAccepted: 'Runtime update accepted — HQ Edge is restarting…',\n // ── Update-source editor ──\n sourceHint: 'Where the runtime update manifest is fetched from; saved changes take effect immediately',\n sourceEdit: 'Edit',\n sourceSave: 'Save',\n sourceReset: 'Reset to default',\n saved: 'Update source saved — re-checking for updates…',\n saveFailed: 'Failed to save the update source',\n sourceInputLabel: 'Runtime update manifest URL',\n sourcePlaceholder: 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json',\n },\n // ── Plugin release-manifest source editor ──\n pluginSource: {\n title: 'Plugin update source',\n hint: 'Where the plugin update manifest is fetched from; saved changes take effect immediately',\n edit: 'Edit',\n save: 'Save',\n reset: 'Reset to default',\n saved: 'Update source saved — re-checking for updates…',\n saveFailed: 'Failed to save the update source',\n inputLabel: 'Plugin update manifest URL',\n placeholder:\n 'https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json',\n },\n // Origin tags, shared by both source editors (hq-edge serves one type).\n originOverride: 'Custom',\n originConfig: 'Configured',\n originUnset: 'Not configured',\n lastError: 'Last check failed',\n installError: 'Install failed',\n installSuccess: 'Update installed — restarting DSH…',\n confirmInstall: 'Confirm install',\n cancel: 'Cancel',\n}\n\nexport const COPY = { zh, en } as const\n","/**\n * Official DSH icon artwork, inlined as SVG strings.\n *\n * The paths are copied verbatim from `@deepseek-ai/dsh-client-ui-primitives`\n * (deepseek-harness `packages/client/ui-primitives/src/icons/index.tsx`) so the\n * Huaqiu plugin-update view draws the same glyphs as the shell's own plugin\n * manager. Only `currentColor` is used, so every glyph inherits the label ink\n * of whatever surface it sits on — the same contract the primitives rely on.\n *\n * The plugin renders plain DOM (never a React tree), so the artwork is exposed\n * as markup strings rather than components.\n *\n * @module\n */\n\n/** Stroke width of the primitives' \"Regular\" weight. */\nconst REGULAR = 1\n\n/** Stroke width of the primitives' \"Medium\" weight. */\nconst MEDIUM = 1.3\n\n/** Wrap paths in a 16×16 stroked glyph. */\nfunction glyph(size: number, strokeWidth: number, paths: readonly string[], viewBox = '0 0 16 16'): string {\n const body = paths.map((d) => `<path d=\"${d}\" stroke=\"currentColor\" />`).join('')\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"${viewBox}\" fill=\"none\" aria-hidden=\"true\" stroke-width=\"${strokeWidth}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${body}</svg>`\n}\n\n/** Wrap fill-only paths (the check glyph) in their own viewBox. */\nfunction fillGlyph(size: number, paths: readonly string[], viewBox: string): string {\n const body = paths.map((d) => `<path d=\"${d}\" fill=\"currentColor\" />`).join('')\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"${viewBox}\" fill=\"none\" aria-hidden=\"true\">${body}</svg>`\n}\n\n/**\n * The plugin pinwheel — the glyph the shell's own Plugins sidebar entry uses\n * (`IconPluginPinwheelOutlineRegular`). Shared by the sidebar row and the\n * package cards so both read as the same feature.\n */\nexport const PLUGIN_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M7.84457 5.06199C11.6605 4.93876 14.7962 6.14848 14.8484 7.76397C14.8875 8.97461 13.1838 10.0696 10.7215 10.5942',\n 'M5.12742 8.07731C5.00419 4.26138 6.21391 1.12568 7.8294 1.07351C9.04004 1.03441 10.135 2.73808 10.6596 5.20037',\n 'M8.02457 10.6802C4.20865 10.8034 1.07294 9.5937 1.02077 7.97821C0.981678 6.76758 2.68535 5.67262 5.14763 5.14798',\n 'M10.7476 7.89535C10.8708 11.7113 9.66109 14.847 8.0456 14.8991C6.83496 14.9382 5.74 13.2346 5.21536 10.7723',\n ])\n\n/** The toolbar's refresh action (`IconRefreshOutlineRegular`). */\nexport const REFRESH_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M14.5001 8C14.5 9.28552 14.1188 10.5422 13.4045 11.611C12.6903 12.6799 11.6752 13.5129 10.4875 14.0049C9.29982 14.4968 7.99295 14.6255 6.73212 14.3747C5.4713 14.124 4.31314 13.505 3.4041 12.596C2.49514 11.687 1.87614 10.5288 1.62537 9.26798C1.37459 8.00716 1.50331 6.70028 1.99525 5.51261C2.48719 4.32494 3.32025 3.30981 4.3891 2.59557C5.45795 1.88134 6.71458 1.50008 8.0001 1.5C9.9001 1.5 11.7001 2.3 13.0001 3.6L14.5001 5.1',\n 'M14.4999 1.5V5.1H10.8999',\n ])\n\n/** The panel's close action (`IconCloseOutlineMedium`). */\nexport const CLOSE_ICON = (size = 16): string =>\n glyph(size, MEDIUM, ['M2.5 2.5L13.5 13.5', 'M13.5 2.5L2.5 13.5'])\n\n/** The notice glyph (`IconWarningOutlineRegular`). */\nexport const WARNING_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z',\n 'M8 4.29199V9.79199',\n 'M8 10.708V11.708',\n ])\n\n/** The settled-state glyph (`IconCheckCircleFillRegular`). */\nexport const CHECK_CIRCLE_ICON = (size = 16): string =>\n fillGlyph(size, [\n 'M28.1936 14.6936L19.8066 23.0806C19.2373 23.65 18.7159 24.1742 18.24 24.5571C17.7389 24.9602 17.1365 25.3359 16.3657 25.458C15.9581 25.5225 15.5428 25.5225 15.1353 25.458C14.3645 25.3359 13.7621 24.9602 13.261 24.5571C12.7851 24.1742 12.2637 23.65 11.6943 23.0806L7.80737 19.1936L10.1936 16.8074L14.0806 20.6943C14.7033 21.317 15.0763 21.6873 15.377 21.9292C15.6523 22.1507 15.7109 22.1325 15.6626 22.1248C15.7208 22.1339 15.7802 22.1339 15.8384 22.1248C15.7901 22.1325 15.8486 22.1507 16.124 21.9292C16.4247 21.6873 16.7977 21.317 17.4204 20.6943L25.8074 12.3074L28.1936 14.6936Z',\n 'M32.8496 18.0005C32.8496 9.79906 26.2019 3.15137 18.0005 3.15137C9.79906 3.15137 3.15137 9.79906 3.15137 18.0005C3.15137 26.2019 9.79906 32.8496 18.0005 32.8496C26.2019 32.8496 32.8496 26.2019 32.8496 18.0005ZM35.7764 18.0005C35.7764 27.8173 27.8173 35.7764 18.0005 35.7764C8.18363 35.7764 0.224609 27.8173 0.224609 18.0005C0.224609 8.18363 8.18363 0.224609 18.0005 0.224609C27.8173 0.224609 35.7764 8.18363 35.7764 18.0005Z',\n ], '0 0 36 36')\n\n/** The install action (`IconDownloadOutlineRegular`). */\nexport const DOWNLOAD_ICON = (size = 16): string =>\n glyph(size, REGULAR, [\n 'M8 1.95317V10.0469',\n 'M4.25 6.29688L8 10.0469L11.75 6.29688',\n 'M1.5 10.0469V13.158C1.5 13.3937 1.60536 13.6198 1.79289 13.7865C1.98043 13.9532 2.23478 14.0469 2.5 14.0469H13.5C13.7652 14.0469 14.0196 13.9532 14.2071 13.7865C14.3946 13.6198 14.5 13.3937 14.5 13.158V10.0469',\n ])\n\n/** The head's info affordance (`IconInfoOutlineRegular`, fill-only artwork). */\nexport const INFO_ICON = (size = 11): string =>\n fillGlyph(size, [\n 'M12.5757 7.00012C12.5757 3.92085 10.0794 1.42463 7.00012 1.42456C3.9208 1.42456 1.42456 3.9208 1.42456 7.00012C1.42463 10.0794 3.92085 12.5757 7.00012 12.5757C10.0793 12.5756 12.5756 10.0793 12.5757 7.00012ZM13.8002 7.00012C13.8001 10.7559 10.7559 13.8001 7.00012 13.8002C3.2443 13.8002 0.199291 10.7559 0.199219 7.00012C0.199219 3.24426 3.24426 0.199219 7.00012 0.199219C10.7559 0.199291 13.8002 3.2443 13.8002 7.00012Z',\n 'M7.6127 3.18921V4.55986H6.38735V3.18921H7.6127Z',\n 'M7.6127 5.68921V10.8109H6.38735V5.68921H7.6127Z',\n ], '0 0 14 14')\n\n/** A folded section's chevron (`IconChevronDownOutlineRegular`). */\nexport const CHEVRON_DOWN_ICON = (size = 14): string =>\n glyph(size, REGULAR, ['M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6'])\n","/**\n * Plugin-update review view (browser half).\n *\n * Plain-DOM panel (no React tree) so it can never disturb the DSH shell's\n * reconciliation — same rule as the sidebar entry rows. Rendered content:\n *\n * - head: title + intro + refresh toolbar (the shell plugin-manager head row)\n * - state-grouped sections, each a group head + package cards\n * (48px hairline-framed glyph, title row with a state tag, one-line\n * version transition, end actions) — the shell's card geometry\n * - a facts group for the independent HQ Edge runtime report\n * - footer actions (install / ignore / show again), with a toast that\n * reports the outcome of an install the way the shell's Toast does\n *\n * Every surface rides a `--dsw-alias-*` design token (see VIEW_CSS_TEXT), so\n * the panel follows the shell's light/dark theme and its focus-ring contract.\n *\n * @module\n */\nimport {\n checkUpdates,\n ignoreOffer,\n installSet,\n requestRuntimeUpdate,\n runtimeLatestVersion,\n setPluginUpdateSource,\n setRuntimeUpdateSource,\n unignoreOffer,\n type PluginUpdateItem,\n type PluginUpdateReport,\n type RuntimeUpdateSource,\n} from './api.js'\nimport type { UpdateCopy } from './copy.js'\nimport {\n CHECK_CIRCLE_ICON,\n CLOSE_ICON,\n DOWNLOAD_ICON,\n INFO_ICON,\n PLUGIN_ICON,\n REFRESH_ICON,\n WARNING_ICON,\n} from './icons.js'\n\nexport interface UpdateStoreLike {\n report(): PluginUpdateReport | null\n installing(): boolean\n refresh(): Promise<PluginUpdateReport | null>\n subscribe(listener: () => void): () => void\n}\n\n/** How long the outcome toast stays up, in milliseconds. */\nconst TOAST_HOLD_MS = 4000\n\nfunction el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n className = '',\n text?: string,\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag)\n if (className.length > 0) node.className = className\n if (text !== undefined) node.textContent = text\n return node\n}\n\n/** A button carrying inline SVG artwork (the icon modules emit markup). */\nfunction iconButton(\n className: string,\n markup: string,\n label: string,\n onClick: () => void,\n): HTMLButtonElement {\n const button = el('button', className)\n button.type = 'button'\n button.setAttribute('aria-label', label)\n button.title = label\n button.innerHTML = markup\n button.addEventListener('click', onClick)\n return button\n}\n\n/**\n * Compact a package name to what a person calls it — the shell plugin\n * manager's own rule (`presentation.ts` `shortName`), so both UIs read the\n * same plugin by the same name.\n */\nexport function shortName(name: string): string {\n const unscoped = name.startsWith('@') ? name.slice(name.indexOf('/') + 1) : name\n return unscoped.replace(/^dsh-(?:host-|client-)?/, '')\n}\n\n/** Normalize the server's block reason to a list (it may arrive as a string). */\nfunction blockedByList(item: PluginUpdateItem): string[] {\n const by = item.blockedBy\n if (Array.isArray(by)) return by.filter((entry): entry is string => typeof entry === 'string')\n return typeof by === 'string' && by.length > 0 ? [by] : []\n}\n\n/** The tag tone a state reads as, matching the shell's Tag palette. */\nfunction stateTone(item: PluginUpdateItem): 'success' | 'info' | 'warning' | 'danger' | 'neutral' {\n switch (item.state) {\n case 'update':\n return 'info'\n case 'install':\n return 'warning'\n case 'blocked':\n return 'danger'\n case 'ignored':\n return 'neutral'\n case 'current':\n return 'success'\n }\n}\n\nfunction stateLabel(item: PluginUpdateItem, copy: UpdateCopy): string {\n switch (item.state) {\n case 'current':\n return copy.state.current\n case 'update':\n return copy.state.update\n case 'install':\n return copy.state.install\n case 'ignored':\n return copy.state.ignored\n case 'blocked':\n return copy.state.blocked\n }\n}\n\n/** The one-line version transition under a card's title. */\nfunction versionLine(item: PluginUpdateItem): string {\n switch (item.state) {\n case 'current':\n return item.currentVersion ?? item.availableVersion\n case 'install':\n return item.availableVersion\n default:\n // An absent plugin names only the version it would gain.\n return item.currentVersion === null\n ? item.availableVersion\n : `${item.currentVersion} → ${item.availableVersion}`\n }\n}\n\nfunction blockedReason(item: PluginUpdateItem, copy: UpdateCopy): string | undefined {\n const by = blockedByList(item)\n if (by.includes('host')) return copy.blockedReason.host\n if (by.includes('hq-edge-version')) return copy.blockedReason['hq-edge-version']\n if (by.includes('runtime-unknown')) return copy.blockedReason['runtime-unknown']\n return by.length > 0 ? by.join(', ') : undefined\n}\n\n/** One package card: glyph, name with its state tag, version line, end actions. */\nfunction packageCard(\n item: PluginUpdateItem,\n copy: UpdateCopy,\n installing: boolean,\n store: UpdateStoreLike,\n): HTMLLIElement {\n const card = el('li', 'hqpu-card')\n card.dataset.pluginName = item.name\n card.dataset.state = item.state\n // The full package name is the card's tooltip; the row shows the short name.\n card.title = `${item.name} · ${versionLine(item)}`\n\n const head = el('div', 'hqpu-card-head')\n\n const glyph = el('span', 'hqpu-card-icon')\n glyph.setAttribute('aria-hidden', 'true')\n glyph.innerHTML = PLUGIN_ICON(24)\n\n const main = el('div', 'hqpu-card-main')\n const titleRow = el('div', 'hqpu-title-row')\n const title = el('span', 'hqpu-card-title', shortName(item.name))\n const tag = el('span', 'hqpu-tag')\n tag.dataset.tone = stateTone(item)\n tag.textContent = stateLabel(item, copy)\n titleRow.append(title, tag)\n\n const desc = el('span', 'hqpu-card-desc', versionLine(item))\n\n main.append(titleRow, desc)\n head.append(glyph, main)\n\n const end = el('div', 'hqpu-card-end')\n if (item.state === 'update' || item.state === 'install') {\n const button = el('button', 'hqpu-btn hqpu-btn--sm hqpu-btn--primary')\n button.type = 'button'\n button.disabled = installing\n button.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installOne}</span>`\n button.addEventListener('click', () => {\n void runInstall(store, copy)\n })\n end.append(button)\n }\n head.append(end)\n card.append(head)\n\n // A block reason belongs to a blocked offer only.\n if (item.state === 'blocked') {\n const reason = blockedReason(item, copy)\n if (reason !== undefined) {\n card.append(el('p', 'hqpu-card-reason', reason))\n }\n }\n return card\n}\n\n/** A section: group head with its count, then the cards. */\nfunction group(\n heading: string,\n items: PluginUpdateItem[],\n copy: UpdateCopy,\n installing: boolean,\n store: UpdateStoreLike,\n): HTMLElement | null {\n if (items.length === 0) return null\n const section = el('section', 'hqpu-group')\n section.dataset.group = heading\n\n const head = el('div', 'hqpu-group-head')\n head.append(el('h3', 'hqpu-group-title', heading))\n head.append(el('span', 'hqpu-group-count', copy.groupCount(items.length)))\n\n const list = el('ul', 'hqpu-cards')\n for (const item of items) list.append(packageCard(item, copy, installing, store))\n\n section.append(head, list)\n return section\n}\n\n/** First-read placeholders, sharing the card and text-line layout. */\nfunction skeleton(copy: UpdateCopy): HTMLElement {\n const section = el('section', 'hqpu-group')\n section.setAttribute('role', 'status')\n section.dataset.loading = ''\n const head = el('div', 'hqpu-group-head')\n head.setAttribute('aria-hidden', 'true')\n const heading = el('span', 'hqpu-group-title hqpu-skeleton-text hqpu-skeleton-heading')\n const headingBar = el('span', 'hqpu-skeleton-fill hqpu-skeleton-bar')\n heading.append(headingBar)\n head.append(heading)\n\n const list = el('ul', 'hqpu-cards')\n list.setAttribute('aria-hidden', 'true')\n for (let index = 0; index < 3; index += 1) {\n const card = el('li', 'hqpu-card')\n const cardHead = el('div', 'hqpu-card-head')\n const glyph = el('span', 'hqpu-card-icon hqpu-skeleton-fill hqpu-skeleton-icon')\n const main = el('div', 'hqpu-card-main')\n const titleRow = el('div', 'hqpu-title-row')\n const title = el('span', 'hqpu-card-title hqpu-skeleton-text hqpu-skeleton-title')\n title.append(el('span', 'hqpu-skeleton-fill hqpu-skeleton-bar'))\n titleRow.append(title)\n const desc = el('span', 'hqpu-card-desc hqpu-skeleton-text hqpu-skeleton-desc')\n desc.append(el('span', 'hqpu-skeleton-fill hqpu-skeleton-bar'))\n main.append(titleRow, desc)\n cardHead.append(glyph, main)\n const end = el('div', 'hqpu-card-end')\n end.setAttribute('aria-hidden', 'true')\n cardHead.append(end)\n card.append(cardHead)\n list.append(card)\n }\n section.append(head, list)\n // The skeleton replaces the whole column, so label it for screen readers.\n section.setAttribute('aria-label', copy.loading)\n return section\n}\n\n/** Mutable editor state for one manifest source (lives in the panel closure). */\ninterface SourceState {\n editing: boolean\n saving: boolean\n draft: string\n}\n\n/**\n * Shared state + callbacks for one update-source editor.\n *\n * hq-edge serves both manifest sources from one shape (plugin release\n * manifest, runtime-update manifest), and both are editable from this panel:\n * an override is persisted per host, so pointing discovery at a staging\n * channel (or a local test rig) needs no env var and no restart. The editor\n * state lives in the panel closure for the same reason `armed` does — a store\n * refresh must not discard a half-typed URL or silently close the editor.\n */\nexport interface SourceEditorActions {\n /** The source editor is open (input shown instead of the read-only row). */\n sourceEditing: boolean\n /** A save is in flight. */\n sourceSaving: boolean\n /** The URL being edited (what the input shows). */\n sourceDraft: string\n onSourceEdit(): void\n onSourceDraft(value: string): void\n onSourceSave(): void\n onSourceCancel(): void\n /** Clear the user override, falling back to the configured URL. */\n onSourceReset(): void\n}\n\n/**\n * The runtime-update action row's state and callbacks.\n *\n * Installing the runtime is destructive in a way a plugin install is not: hq-edge\n * shuts itself down so the EDA host can swap the runtime directory. So the row\n * is a two-step confirm (arm → confirm), never a single fire-and-forget click.\n */\nexport interface RuntimeActions extends SourceEditorActions {\n /** A request is in flight (hq-edge accepted it and is restarting). */\n busy: boolean\n /** The confirm step is armed for the version the row names. */\n armed: boolean\n /**\n * hq-edge answered `202 Accepted`: from here on the outcome belongs to the\n * EDA host, and this process is about to be replaced. Terminal — the row\n * must never re-offer the button once this is true, because hq-edge is\n * already gone and a second request could only fail.\n */\n accepted: boolean\n onArm(): void\n onConfirm(): void\n onCancel(): void\n}\n\n/**\n * Normalize the report's `runtime.source` to the full shape.\n *\n * An older hq-edge predates the feature and sends no `source` at all — that\n * must degrade to a read-only panel, not crash the render. A future server\n * may add fields; unknown extras are dropped here rather than trusted.\n */\nfunction normalizeSource(raw: unknown): RuntimeUpdateSource | null {\n if (raw === null || typeof raw !== 'object') return null\n const candidate = raw as Partial<RuntimeUpdateSource>\n if (typeof candidate.origin !== 'string') return null\n return {\n url: typeof candidate.url === 'string' ? candidate.url : '',\n origin: candidate.origin as RuntimeUpdateSource['origin'],\n configuredUrl: typeof candidate.configuredUrl === 'string' ? candidate.configuredUrl : '',\n override: candidate.override === true,\n }\n}\n\n/** The tag naming where the URL in effect came from. */\nfunction originLabel(origin: RuntimeUpdateSource['origin'], copy: UpdateCopy): string {\n switch (origin) {\n case 'override':\n return copy.originOverride\n case 'config':\n return copy.originConfig\n default:\n return copy.originUnset\n }\n}\n\n/** The 更新源 fact row: the URL in effect plus an origin tag. */\nfunction sourceFactRow(\n facts: HTMLElement,\n source: RuntimeUpdateSource,\n copy: UpdateCopy,\n): void {\n const wrap = el('div')\n wrap.append(el('dt', '', copy.runtime.source))\n const dd = el('dd')\n const line = el('div', 'hqpu-source-line')\n const code = el(\n 'code',\n 'hqpu-source-url',\n source.url.length > 0 ? source.url : copy.runtime.notConfigured,\n )\n const tag = el('span', 'hqpu-tag')\n tag.dataset.tone = source.override ? 'info' : source.origin === 'config' ? 'success' : 'neutral'\n tag.textContent = originLabel(source.origin, copy)\n line.append(code, tag)\n dd.append(line)\n wrap.append(dd)\n facts.append(wrap)\n}\n\n/** Which manifest an editor belongs to — drives data attributes + CSS hook. */\nexport type SourceKind = 'runtime' | 'plugin'\n\n/** The localized strings one source editor needs (both kinds share the flow). */\ninterface SourceEditorLabels {\n hint: string\n edit: string\n save: string\n reset: string\n inputLabel: string\n placeholder: string\n}\n\n/** Pick the labels for one kind out of the copy table. */\nfunction sourceLabels(copy: UpdateCopy, kind: SourceKind): SourceEditorLabels {\n return kind === 'runtime'\n ? {\n hint: copy.runtime.sourceHint,\n edit: copy.runtime.sourceEdit,\n save: copy.runtime.sourceSave,\n reset: copy.runtime.sourceReset,\n inputLabel: copy.runtime.sourceInputLabel,\n placeholder: copy.runtime.sourcePlaceholder,\n }\n : {\n hint: copy.pluginSource.hint,\n edit: copy.pluginSource.edit,\n save: copy.pluginSource.save,\n reset: copy.pluginSource.reset,\n inputLabel: copy.pluginSource.inputLabel,\n placeholder: copy.pluginSource.placeholder,\n }\n}\n\n/**\n * One update-source editor, shared by the plugin manifest and the runtime\n * manifest (the only differences are the labels and the data-attribute\n * prefix, so tests and CSS can target each).\n *\n * Read mode is a hint plus a 修改 button; edit mode is the URL field with\n * 保存 / 取消 — plus 恢复默认 while a user override shadows the configured\n * URL. The draft lives in {@link SourceEditorActions} (the panel closure), so\n * a background store refresh re-renders the typed URL instead of discarding it.\n */\nfunction sourceEditor(\n kind: SourceKind,\n source: RuntimeUpdateSource,\n copy: UpdateCopy,\n actions: SourceEditorActions,\n): HTMLElement {\n const labels = sourceLabels(copy, kind)\n const block = el('div', `hqpu-source-editor hqpu-source-editor--${kind}`)\n if (!actions.sourceEditing) {\n block.append(el('p', 'hqpu-status hqpu-source-hint', labels.hint))\n const edit = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', labels.edit)\n edit.type = 'button'\n edit.setAttribute(`data-${kind}-source-edit`, '')\n edit.disabled = actions.sourceSaving\n edit.addEventListener('click', actions.onSourceEdit)\n block.append(edit)\n return block\n }\n const input = el('input', 'hqpu-source-input')\n input.type = 'url'\n input.value = actions.sourceDraft\n input.placeholder = labels.placeholder\n input.setAttribute('aria-label', labels.inputLabel)\n input.setAttribute(`data-${kind}-source-input`, '')\n input.disabled = actions.sourceSaving\n input.addEventListener('input', () => {\n // Draft-only: no re-render, so the caret and IME stay untouched.\n actions.onSourceDraft(input.value)\n })\n // Enter saves, Escape cancels — the field is the whole editor, and Escape\n // must not fall through to the panel-wide close handler.\n input.addEventListener('keydown', (ev) => {\n if (ev.key === 'Enter') {\n ev.preventDefault()\n actions.onSourceSave()\n } else if (ev.key === 'Escape') {\n ev.preventDefault()\n ev.stopPropagation()\n actions.onSourceCancel()\n }\n })\n const save = el('button', 'hqpu-btn hqpu-btn--primary hqpu-btn--sm', labels.save)\n save.type = 'button'\n save.setAttribute(`data-${kind}-source-save`, '')\n save.disabled = actions.sourceSaving\n save.addEventListener('click', actions.onSourceSave)\n const cancel = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', copy.cancel)\n cancel.type = 'button'\n cancel.setAttribute(`data-${kind}-source-cancel`, '')\n cancel.disabled = actions.sourceSaving\n cancel.addEventListener('click', actions.onSourceCancel)\n const controls = el('div', 'hqpu-actions hqpu-source-controls')\n controls.append(input, save, cancel)\n if (source.override) {\n const reset = el('button', 'hqpu-btn hqpu-btn--ghost hqpu-btn--sm', labels.reset)\n reset.type = 'button'\n reset.setAttribute(`data-${kind}-source-reset`, '')\n reset.disabled = actions.sourceSaving\n reset.addEventListener('click', actions.onSourceReset)\n controls.append(reset)\n }\n block.append(controls)\n // Focus on open — and on any re-render while editing, which is exactly what\n // a rejected save wants (the user is dropped back into the field to fix it).\n input.focus()\n return block\n}\n\n/**\n * The plugin release-manifest source, as its own group above the plugin\n * groups: it says where the whole list comes from, so it belongs above the\n * list rather than inside it. Absent on an older hq-edge (no `pluginSource`\n * in the report) — then nothing is rendered at all.\n */\nfunction pluginSourceGroup(\n report: PluginUpdateReport,\n copy: UpdateCopy,\n actions: SourceEditorActions,\n): HTMLElement | null {\n const source = normalizeSource(report.pluginSource)\n if (source === null) return null\n const section = el('section', 'hqpu-group')\n section.dataset.group = 'plugin-source'\n const head = el('div', 'hqpu-group-head')\n head.append(el('h3', 'hqpu-group-title', copy.pluginSource.title))\n section.append(head)\n\n const facts = el('dl', 'hqpu-facts')\n sourceFactRow(facts, source, copy)\n section.append(facts)\n section.append(sourceEditor('plugin', source, copy, actions))\n return section\n}\n\n/** The independent HQ Edge runtime report, as ruled fact rows. */\nfunction runtimeGroup(\n report: PluginUpdateReport,\n copy: UpdateCopy,\n actions: RuntimeActions,\n): HTMLElement {\n const section = el('section', 'hqpu-group')\n section.dataset.group = 'runtime'\n const head = el('div', 'hqpu-group-head')\n head.append(el('h3', 'hqpu-group-title', copy.runtime.title))\n section.append(head)\n\n const facts = el('dl', 'hqpu-facts')\n const runtime = report.runtime\n // `runtime.latest` is a release ENTRY; only its version is ever displayed or\n // sent back as `targetVersion`.\n const latestVersion = runtimeLatestVersion(runtime)\n\n const row = (label: string, value: string, code = false): void => {\n const wrap = el('div')\n wrap.append(el('dt', '', label))\n const dd = el('dd')\n if (code) {\n const node = el('code', '', value)\n dd.append(node)\n } else {\n dd.textContent = value\n }\n wrap.append(dd)\n facts.append(wrap)\n }\n\n // The source row: which manifest URL is in effect and where it came from.\n // Absent on an older hq-edge — then only the legacy notConfigured row shows.\n const source = normalizeSource(runtime.source)\n\n if (runtime.configured === false) {\n if (source !== null) {\n sourceFactRow(facts, source, copy)\n } else {\n row(copy.runtime.source, copy.runtime.notConfigured)\n }\n } else {\n row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown)\n if (runtime.updateAvailable && latestVersion !== null) {\n row(copy.runtime.latest, latestVersion)\n }\n if (report.manifestVersion !== null) {\n row(copy.runtime.set, report.manifestVersion, true)\n }\n if (typeof runtime.checkedAt === 'string' && runtime.checkedAt.length > 0) {\n row(copy.runtime.checkedAt, new Date(runtime.checkedAt).toLocaleString())\n }\n if (source !== null) {\n sourceFactRow(facts, source, copy)\n }\n }\n section.append(facts)\n\n // The editor: point the runtime at another release channel (a staging\n // manifest, a local rig) without an env var and a restart. hq-edge persists\n // the override per host, so it survives restarts of both processes.\n if (source !== null) {\n section.append(sourceEditor('runtime', source, copy, actions))\n }\n\n // ── Runtime update action row ──\n // Only offered when hq-edge reports a newer distribution AND named its\n // version: the request needs `targetVersion`, so an entry without one has\n // nothing to send.\n if (runtime.updateAvailable && latestVersion !== null) {\n const row = el('div', 'hqpu-actions hqpu-runtime-actions')\n if (actions.accepted) {\n // Terminal state: no button. hq-edge has already exited, so re-offering\n // \"update\" here could only produce a transport error.\n const line = el('span', 'hqpu-status hqpu-status--dot')\n line.setAttribute('role', 'status')\n line.append(el('span', 'hqpu-spinner'), el('span', '', copy.runtime.updateAccepted))\n row.append(line)\n } else if (actions.busy) {\n // Downloading + verifying + staging the bundle: tens of seconds to a few\n // minutes. Say what is happening instead of an open-ended spinner.\n const line = el('span', 'hqpu-status hqpu-status--dot')\n line.setAttribute('role', 'status')\n line.append(el('span', 'hqpu-spinner'), el('span', '', copy.runtime.updating))\n row.append(line)\n } else if (actions.armed) {\n row.append(el('p', 'hqpu-status hqpu-runtime-confirm', copy.runtime.updateConfirm(latestVersion)))\n const yes = el('button', 'hqpu-btn hqpu-btn--primary hqpu-btn--sm', copy.confirmInstall)\n yes.type = 'button'\n yes.dataset.runtimeConfirm = ''\n yes.addEventListener('click', actions.onConfirm)\n const no = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', copy.cancel)\n no.type = 'button'\n no.addEventListener('click', actions.onCancel)\n row.append(yes, no)\n } else {\n const start = el('button', 'hqpu-btn hqpu-btn--primary hqpu-btn--sm')\n start.type = 'button'\n start.dataset.runtimeUpdate = ''\n start.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.runtime.update}</span>`\n start.addEventListener('click', actions.onArm)\n row.append(start)\n }\n section.append(row)\n } else {\n section.append(el('p', 'hqpu-status', copy.runtime.note))\n }\n\n if (typeof runtime.lastError === 'string' && runtime.lastError.length > 0) {\n const line = el('p', 'hqpu-status hqpu-status--dot')\n line.setAttribute('role', 'status')\n const dot = el('span', 'hqpu-dot')\n dot.dataset.state = 'error'\n line.append(dot, el('span', '', `${copy.lastError}: ${runtime.lastError}`))\n section.append(line)\n }\n return section\n}\n\nfunction renderList(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n installing: boolean,\n store: UpdateStoreLike,\n): void {\n section.textContent = ''\n if (report === null) {\n section.append(skeleton(copy))\n return\n }\n if (report.items.length === 0) {\n section.append(el('div', 'hqpu-empty', copy.empty))\n return\n }\n\n const byState = (state: PluginUpdateItem['state']): PluginUpdateItem[] =>\n report.items.filter((item) => item.state === state)\n\n // Most important first: what can be acted on, then what is refused, then\n // what was muted, then what is already settled.\n const order: Array<[string, PluginUpdateItem[]]> = [\n [copy.group.actionable, [...byState('update'), ...byState('install')]],\n [copy.group.blocked, byState('blocked')],\n [copy.group.ignored, byState('ignored')],\n [copy.group.current, byState('current')],\n ]\n for (const [heading, items] of order) {\n const node = group(heading, items, copy, installing, store)\n if (node !== null) section.append(node)\n }\n}\n\nfunction renderRuntime(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n actions: RuntimeActions,\n): void {\n section.textContent = ''\n if (report === null) return\n section.append(runtimeGroup(report, copy, actions))\n}\n\n/** The plugin-source group, above the plugin groups. No-op without the field. */\nfunction renderPluginSource(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n actions: SourceEditorActions,\n): void {\n section.textContent = ''\n if (report === null) return\n const node = pluginSourceGroup(report, copy, actions)\n if (node !== null) section.append(node)\n}\n\n/** Run the install transaction, then keep polling until the state settles. */\nasync function runInstall(store: UpdateStoreLike, copy: UpdateCopy): Promise<void> {\n const report = store.report()\n if (report === null) return\n // Without an evaluated set there is nothing to install: hq-edge has not read\n // a manifest yet, so the report is still its empty shell.\n if (report.manifestVersion === null) {\n showToast(copy.checkFailed, 'error')\n return\n }\n try {\n const result = await installSet(report.manifestVersion)\n if (result.error !== undefined) {\n showToast(`${copy.installError}: ${result.error}`, 'error')\n return\n }\n // The set activates atomically and DSH restarts. Keep polling the backend\n // state rather than trusting the response alone (spec §7.1).\n showToast(copy.installSuccess, 'success')\n void pollUntilSettled(store)\n } catch (err: unknown) {\n showToast(\n `${copy.installError}: ${err instanceof Error ? err.message : String(err)}`,\n 'error',\n )\n }\n}\n\n/** The single live toast; a new message replaces the one on screen. */\nlet toastHost: HTMLElement | null = null\nlet toastTimer: number | null = null\n\nfunction showToast(message: string, tone: 'success' | 'error'): void {\n if (toastHost === null) return\n if (toastTimer !== null) window.clearTimeout(toastTimer)\n toastHost.textContent = ''\n const toast = el('div', 'hqpu-toast')\n toast.setAttribute('role', 'status')\n const icon = el('span', 'hqpu-toast-icon')\n icon.dataset.tone = tone\n icon.innerHTML = tone === 'success' ? CHECK_CIRCLE_ICON(16) : WARNING_ICON(16)\n toast.append(icon, el('span', 'hqpu-toast-text', message))\n toastHost.append(toast)\n toastTimer = window.setTimeout(() => {\n toastHost?.replaceChildren()\n toastTimer = null\n }, TOAST_HOLD_MS)\n}\n\nfunction renderActions(\n bar: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n store: UpdateStoreLike,\n installing: boolean,\n): void {\n bar.textContent = ''\n if (report === null) return\n\n if (installing) {\n const line = el('span', 'hqpu-status hqpu-status--dot')\n const spinner = el('span', 'hqpu-spinner')\n line.append(spinner, el('span', '', copy.installing))\n bar.append(line)\n return\n }\n\n const actionable = report.items.filter(\n (item) => item.state === 'update' || item.state === 'install',\n ).length\n // Every write needs an evaluated set; hq-edge reports `null` until it has\n // read a manifest, and posting `null` would only earn a 400.\n const hasSet = report.manifestVersion !== null\n\n if (actionable === 0) {\n // The official page shows no install control when there is nothing to\n // commit, so mirror that with a muted line rather than a dead button.\n bar.append(el('span', 'hqpu-status', copy.nothingToInstall))\n } else {\n const installBtn = el('button', 'hqpu-btn hqpu-btn--primary')\n installBtn.type = 'button'\n installBtn.disabled = !hasSet || report.ignoredSet\n installBtn.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installAll(actionable)}</span>`\n installBtn.addEventListener('click', () => {\n void runInstall(store, copy)\n })\n bar.append(installBtn)\n }\n\n if (!hasSet) return\n\n if (report.ignoredSet) {\n const unignore = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', copy.unignore)\n unignore.type = 'button'\n unignore.addEventListener('click', () => {\n void unignoreOffer(report.manifestVersion as string)\n .then(() => store.refresh())\n .catch(() => undefined)\n })\n bar.append(unignore)\n } else if (actionable > 0) {\n const ignore = el('button', 'hqpu-btn hqpu-btn--ghost hqpu-btn--sm', copy.ignore)\n ignore.type = 'button'\n ignore.addEventListener('click', () => {\n void ignoreOffer(report.manifestVersion as string)\n .then(() => store.refresh())\n .catch(() => undefined)\n })\n bar.append(ignore)\n }\n}\n\n/** After an install, poll the backend until the update report settles\n * (installing flag clears or DSH restarts under us — the next load re-reads\n * the fresh state). */\nfunction pollUntilSettled(store: UpdateStoreLike): void {\n let attempts = 0\n const tick = (): void => {\n void store\n .refresh()\n .then((report) => {\n if (store.installing()) return\n if (report !== null && report.ignoredSet === false && attempts >= 2) return\n })\n .catch(() => undefined)\n .finally(() => {\n attempts += 1\n if (attempts < 10) {\n window.setTimeout(tick, 2000)\n }\n })\n }\n tick()\n}\n\n/**\n * Open (or focus) the update panel. Re-renders from the store; when the store\n * is stale the panel first refreshes the report.\n */\nexport function openUpdateView(store: UpdateStoreLike, copy: UpdateCopy): void {\n const existing = document.querySelector<HTMLElement>('[data-hq-plugin-update-view]')\n if (existing !== null) {\n existing.style.display = ''\n return\n }\n\n const overlay = el('div', 'hqpu-overlay')\n overlay.setAttribute('data-hq-plugin-update-view', '')\n overlay.addEventListener('click', (ev) => {\n if (ev.target === overlay) onClose()\n })\n\n const scrim = el('div', 'hqpu-scrim')\n const panel = el('div', 'hqpu-panel')\n panel.setAttribute('role', 'dialog')\n panel.setAttribute('aria-modal', 'true')\n panel.setAttribute('aria-label', copy.viewTitle)\n panel.tabIndex = -1\n\n // ── Head row: title + intro, refresh toolbar ──\n const head = el('div', 'hqpu-head')\n const headMain = el('div', 'hqpu-head-main')\n headMain.append(el('h1', 'hqpu-title', copy.viewTitle))\n const intro = el('p', 'hqpu-intro')\n intro.append(el('span', '', copy.viewIntro))\n const info = el('button', 'hqpu-info')\n info.type = 'button'\n info.setAttribute('aria-label', copy.viewInfo)\n info.title = copy.viewInfo\n info.innerHTML = INFO_ICON(11)\n intro.append(info)\n headMain.append(intro)\n\n const toolbar = el('div', 'hqpu-toolbar')\n const refreshBtn = iconButton('hqpu-icon-btn', REFRESH_ICON(16), copy.refresh, () => {\n void store.refresh()\n })\n toolbar.append(refreshBtn)\n const closeBtn = iconButton('hqpu-icon-btn', CLOSE_ICON(16), copy.close, () => {\n onClose()\n })\n toolbar.append(closeBtn)\n\n head.append(headMain, toolbar)\n panel.append(head)\n\n // ── Scrolling body: the centred column ──\n const scroll = el('div', 'hqpu-scroll')\n const column = el('div', 'hqpu-column')\n const pluginSourceSection = el('div', 'hqpu-stack')\n const listSection = el('div', 'hqpu-stack')\n const runtimeSection = el('div', 'hqpu-stack')\n const toastSection = el('div', 'hqpu-toast-host')\n column.append(pluginSourceSection, listSection, runtimeSection, toastSection)\n scroll.append(column)\n panel.append(scroll)\n\n // ── Footer actions (ui-primitives Modal .footer) ──\n const footer = el('div', 'hqpu-footer')\n const actionBar = el('div', 'hqpu-actions')\n footer.append(actionBar)\n panel.append(footer)\n\n overlay.append(scrim, panel)\n document.body.appendChild(overlay)\n toastHost = toastSection\n panel.focus()\n\n const disposers: Array<() => void> = []\n\n // ── Runtime update: arm → confirm, then hand off to hq-edge ──\n // Live in the panel's closure (not the report) because they are UI state, and\n // a store refresh must not silently disarm a confirm the user just armed.\n let runtimeArmed = false\n let runtimeBusy = false\n let runtimeAccepted = false\n\n // Source-editor state, same reasoning: a background refresh must not discard\n // a half-typed URL or silently close either editor.\n const runtimeSourceState: SourceState = { editing: false, saving: false, draft: '' }\n const pluginSourceState: SourceState = { editing: false, saving: false, draft: '' }\n\n // Delegated, not spread: `...sourceActionsFor(...)` would evaluate the\n // getters once and freeze `sourceEditing: false` — the editor would never\n // open. Read every member through the live object instead.\n const runtimeSourceActions = sourceActionsFor('runtime', runtimeSourceState)\n const runtimeActions: RuntimeActions = {\n get busy() {\n return runtimeBusy\n },\n get armed() {\n return runtimeArmed\n },\n get accepted() {\n return runtimeAccepted\n },\n onArm: () => {\n runtimeArmed = true\n render()\n },\n onCancel: () => {\n runtimeArmed = false\n render()\n },\n onConfirm: () => {\n void requestRuntime()\n },\n get sourceEditing() {\n return runtimeSourceActions.sourceEditing\n },\n get sourceSaving() {\n return runtimeSourceActions.sourceSaving\n },\n get sourceDraft() {\n return runtimeSourceActions.sourceDraft\n },\n onSourceEdit: () => runtimeSourceActions.onSourceEdit(),\n onSourceDraft: (value: string) => runtimeSourceActions.onSourceDraft(value),\n onSourceSave: () => runtimeSourceActions.onSourceSave(),\n onSourceCancel: () => runtimeSourceActions.onSourceCancel(),\n onSourceReset: () => runtimeSourceActions.onSourceReset(),\n }\n\n const pluginActions: SourceEditorActions = sourceActionsFor('plugin', pluginSourceState)\n\n /** Build the {@link SourceEditorActions} half for one manifest source. */\n function sourceActionsFor(\n kind: SourceKind,\n state: SourceState,\n ): SourceEditorActions {\n return {\n get sourceEditing() {\n return state.editing\n },\n get sourceSaving() {\n return state.saving\n },\n get sourceDraft() {\n return state.draft\n },\n onSourceEdit: () => {\n // Prefill with the URL in effect, so 保存 is a no-op unless edited.\n const report = store.report()\n const source =\n kind === 'runtime' ? normalizeSource(report?.runtime.source) : normalizeSource(report?.pluginSource)\n state.draft = source?.url ?? ''\n state.editing = true\n render()\n },\n onSourceDraft: (value: string) => {\n state.draft = value\n },\n onSourceSave: () => {\n void submitSource(kind, state)\n },\n onSourceCancel: () => {\n state.editing = false\n render()\n },\n onSourceReset: () => {\n void submitSource(kind, state, '')\n },\n }\n }\n\n /**\n * Save one manifest source — or clear the override with an empty URL —\n * then re-run discovery.\n *\n * The re-check is not optional: the report is cached, so without a fresh\n * `POST /check` the facts and the update button would keep naming the OLD\n * manifest's offer after the source changed. hq-edge answers 422 with a\n * message meant for a person on refusal (bad scheme, credentials, …), which\n * the toast shows verbatim.\n */\n async function submitSource(\n kind: SourceKind,\n state: SourceState,\n url: string = state.draft,\n ): Promise<void> {\n state.saving = true\n render()\n const labels = kind === 'runtime' ? copy.runtime : copy.pluginSource\n try {\n if (kind === 'runtime') await setRuntimeUpdateSource(url)\n else await setPluginUpdateSource(url)\n state.editing = false\n showToast(labels.saved, 'success')\n try {\n await checkUpdates()\n await store.refresh()\n } catch {\n // The source IS saved; a failed re-check only leaves the previous\n // offer on screen until the next refresh — the toolbar re-runs it.\n }\n } catch (err: unknown) {\n showToast(\n `${labels.saveFailed}: ${err instanceof Error ? err.message : String(err)}`,\n 'error',\n )\n } finally {\n state.saving = false\n render()\n }\n }\n\n async function requestRuntime(): Promise<void> {\n const version = runtimeLatestVersion(store.report()?.runtime)\n if (version === null) {\n showToast(copy.checkFailed, 'error')\n return\n }\n runtimeArmed = false\n runtimeBusy = true\n render()\n try {\n const result = await requestRuntimeUpdate(version)\n if (result?.error !== undefined) {\n showToast(`${copy.installError}: ${result.error}`, 'error')\n return\n }\n // 202 Accepted: hq-edge exits to let the EDA host swap the runtime and\n // relaunch. This panel is about to go away with the restart, so say so —\n // and latch it, so a store refresh cannot re-offer the button for a\n // server that is already gone.\n runtimeAccepted = true\n showToast(copy.runtime.updateAccepted, 'success')\n } catch (err: unknown) {\n showToast(\n `${copy.installError}: ${err instanceof Error ? err.message : String(err)}`,\n 'error',\n )\n } finally {\n runtimeBusy = false\n render()\n }\n }\n\n const render = (): void => {\n const report = store.report()\n const installing = store.installing()\n const stale = report === null\n refreshBtn.disabled = stale\n if (stale) {\n listSection.textContent = ''\n listSection.append(skeleton(copy))\n } else {\n renderList(listSection, report, copy, installing, store)\n }\n renderPluginSource(pluginSourceSection, report, copy, pluginActions)\n renderRuntime(runtimeSection, report, copy, runtimeActions)\n renderActions(actionBar, report, copy, store, installing)\n }\n\n const unsubscribe = store.subscribe(render)\n disposers.push(unsubscribe)\n\n // Refresh on open (bounded; never block the shell).\n void store.refresh().then(() => {\n // Store notification re-renders already; nothing extra to do.\n })\n\n // Escape closes, the shell-wide dialog contract.\n const onKeyDown = (ev: KeyboardEvent): void => {\n if (ev.key === 'Escape') {\n ev.stopPropagation()\n onClose()\n }\n }\n document.addEventListener('keydown', onKeyDown)\n disposers.push(() => document.removeEventListener('keydown', onKeyDown))\n\n function onClose(): void {\n for (const dispose of disposers) {\n try {\n dispose()\n } catch {\n /* already disposed */\n }\n }\n if (toastTimer !== null) window.clearTimeout(toastTimer)\n toastTimer = null\n if (toastHost === toastSection) toastHost = null\n overlay.remove()\n }\n\n render()\n}\n","/**\n * `@huaqiu/dsh-tool-plugin-update` — browser half.\n *\n * Mounts one sidebar toolbutton in the Huaqiu family block (\"插件更新\") with a\n * numeric badge equal to the actionable plugin-update count (spec §6.3/§7.1:\n * only compatible `update` + `install` items after host/HQ-Edge filtering and\n * after excluding the ignored offer; runtime updates are NOT counted). Click\n * opens the review/confirm/ignore modal.\n *\n * The authoritative state is the hq-edge report fetched through the plugin's\n * same-origin forwarding routes. A tiny in-memory store owns the latest report\n * + installing flag; the sidebar badge and the modal subscribe to it. No\n * timers beyond the install-poll and an idle 60 s refresh.\n *\n * @module\n */\nimport {\n HUAQIU_SIDEBAR_ENTRY_FAMILY,\n detectHostLocale,\n mountSidebarEntry,\n observeHostLocale,\n type HostLocale,\n} from '@huaqiu/dsh-sidebar-entry'\nimport { checkUpdates, fetchReport, type PluginUpdateReport } from './api.js'\nimport { COPY } from './copy.js'\nimport { PLUGIN_ICON } from './icons.js'\nimport { openUpdateView, type UpdateStoreLike } from './update-view.js'\n\n/** Stable data attributes identifying the injected entry row. */\nexport const UPDATE_ENTRY_SELECTOR = '[data-hq-plugin-update-entry]'\nexport const UPDATE_ENTRY_ATTRIBUTE = 'data-hq-plugin-update-entry'\n\n/** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */\nconst PLUGIN_ID = 'huaqiu-plugin-update'\n\n/** Row class names (defined by {@link ENTRY_CSS_TEXT}). */\nconst ENTRY_CSS: Record<string, string> = {\n entry: 'hqpu-sidebar-entry',\n entryIcon: 'hqpu-sidebar-entry__icon',\n entryLabel: 'hqpu-sidebar-entry__label',\n entryBadge: 'hqpu-sidebar-entry__badge',\n}\n\n/** Localized row copy: zh is the fallback, matching the family convention. */\nfunction entryCopy(lang: HostLocale | undefined): { label: string; tooltip: string } {\n return lang === 'en'\n ? { label: COPY.en.entryLabel, tooltip: COPY.en.entryTooltip }\n : { label: COPY.zh.entryLabel, tooltip: COPY.zh.entryTooltip }\n}\n\n/** The sidebar glyph — the same pinwheel the shell's own Plugins entry draws\n * (`IconPluginPinwheelOutlineRegular`), so both rows read as one feature. */\nexport const UPDATE_ICON = PLUGIN_ICON(16)\n\n/** Sidebar entry styles — matched to the family rows (same inset/padding/gap\n * as the shell's own Plugins entry, `SidebarRoot.module.css` `.panelRow`).\n * Every colour rides a `--dsw-alias-*` token so the row follows the shell's\n * light/dark theme; the badge foreground uses the toast label, the shell's\n * \"always light ink on a saturated surface\" token. */\nconst ENTRY_CSS_TEXT = `\n.hqpu-sidebar-entry {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin: 0 2px;\n min-height: 36px;\n padding: 7px 8px;\n background: transparent;\n border: none;\n border-radius: var(--dsw-radius-md);\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font: inherit;\n line-height: 22px;\n text-align: left;\n white-space: nowrap;\n}\n.hqpu-sidebar-entry:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.hqpu-sidebar-entry[data-active] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-sidebar-entry:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: -2px;\n}\n.hqpu-sidebar-entry__icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n.hqpu-sidebar-entry__icon svg {\n display: block;\n width: 16px;\n height: 16px;\n}\n.hqpu-sidebar-entry__label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1;\n}\n.hqpu-sidebar-entry__badge {\n flex: none;\n min-width: 16px;\n height: 16px;\n padding: 0 5px;\n border-radius: 8px;\n background: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-toast-label);\n font-size: 11px;\n line-height: 16px;\n font-weight: 500;\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n/* Collapsed rail: icon-only, centered — same 36px square the shell uses. */\n[data-sidebar-collapsed] .hqpu-sidebar-entry {\n justify-content: center;\n width: 36px;\n height: 36px;\n margin: 0;\n padding: 0;\n}\n[data-sidebar-collapsed] .hqpu-sidebar-entry__icon svg {\n width: 18px;\n height: 18px;\n}\n[data-sidebar-collapsed] .hqpu-sidebar-entry__label,\n[data-sidebar-collapsed] .hqpu-sidebar-entry__badge {\n display: none;\n}\n`\n\n/**\n * View styles (hqpu-*). Plain overlay + panel, drawn with the SAME design\n * tokens and geometry the shell's own plugin manager uses\n * (`ui-plugin-manager/src/client/PluginManagerPage.module.css` +\n * `ui-primitives/src/{Modal,Toast,Tag,StateDot}.module.css`), so this page\n * reads as part of the product rather than a bolt-on.\n *\n * Two rules keep it honest across themes:\n * 1. every colour is a `--dsw-alias-*` token — no literal hex/rgb, so light\n * and dark both come out right (the old sheet invented\n * `--dsw-alias-bg-surface` / `--dsw-alias-line-border` / `--dsw-alias-bg-badge`,\n * none of which exist, and so always fell back to a white panel);\n * 2. `color-mix()` tints come from the state token itself, so a palette\n * change moves fill and ink together.\n *\n * The chrome (panel, scrim, header, footer) follows `ui-primitives/Modal`,\n * because this view IS a dialog; the body follows `PluginManagerPage`, because\n * that is the content it heads. The one token the bundled shell does not\n * publish is backfilled by {@link ensureDesignTokens} — see the note there.\n */\nconst VIEW_CSS_TEXT = `\n/* ── Overlay + panel (ui-primitives Modal.module.css) ─────────────────── */\n.hqpu-overlay {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n /* The frame's own top inset keeps a tall card clear of the window chrome. */\n padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;\n}\n.hqpu-scrim {\n position: absolute;\n /* Only paint excludes the native caption; the root still blocks clicks. */\n inset: var(--dsh-frame-chrome-top, 0px) 0 0;\n background: var(--dsw-alias-bg-mask-1);\n animation: hqpu-fade var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);\n}\n.hqpu-panel {\n position: relative;\n z-index: 1;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: min(920px, 100%);\n max-height: 100%;\n overflow: hidden;\n border: 0;\n border-radius: var(--dsw-radius-panel);\n background: var(--dsw-alias-bg-layer-2);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-primary);\n animation: hqpu-panel-in var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);\n}\n.hqpu-panel:focus {\n outline: none;\n}\n@keyframes hqpu-fade {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes hqpu-panel-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n/* ── Head row (ui-primitives Modal .header) ─────────────────────────────── */\n.hqpu-head {\n display: flex;\n flex: none;\n flex-wrap: wrap;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n box-sizing: border-box;\n /* The official Title row: 22/14/12/24 — the toolbar sits 10px further right\n * than the body content on purpose, matching the shell's own close button. */\n padding: 22px 14px 12px 24px;\n}\n.hqpu-head-main {\n min-width: 0;\n max-width: 100%;\n}\n.hqpu-title {\n margin: 0;\n font-size: 20px;\n font-weight: 500;\n line-height: 28px;\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-intro {\n display: flex;\n align-items: center;\n gap: 4px;\n margin: 4px 0 0;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-info {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 20px;\n height: 20px;\n padding: 0;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-caption);\n cursor: pointer;\n}\n.hqpu-info:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-info:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n.hqpu-toolbar {\n display: flex;\n flex: none;\n align-items: center;\n gap: 16px;\n}\n\n/* ── Scrolling body: the page's centred column ────────────────────────── */\n.hqpu-scroll {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n box-sizing: border-box;\n /* The panel's 20px gap already separates this from the head and footer. */\n padding: 0 clamp(16px, 3vw, 32px) 4px;\n /* Anything that scrolls inside the panel draws the l2 thumb. */\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n}\n.hqpu-column {\n display: flex;\n flex-direction: column;\n gap: 24px;\n width: 100%;\n max-width: 880px;\n margin: 0 auto;\n}\n/* A stack of groups (or the single toast host) inside the column. */\n.hqpu-stack {\n display: flex;\n flex-direction: column;\n gap: 24px;\n}\n.hqpu-toast-host:empty {\n display: none;\n}\n\n/* ── Groups (PluginManagerPage .group) ────────────────────────────────── */\n.hqpu-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.hqpu-group-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.hqpu-group-title {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 14px;\n font-weight: 500;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-group-count {\n flex: none;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n/* ── Cards (PluginManagerPage .card) ──────────────────────────────────── */\n.hqpu-cards {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.hqpu-card {\n min-width: 0;\n /* The hover surface reaches 8px past the content on both sides. */\n margin: 0 -8px;\n border-radius: var(--dsw-radius-xl);\n}\n.hqpu-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.hqpu-card-head {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 8px;\n}\n.hqpu-card-icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-lg);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-card-icon svg {\n display: block;\n width: 24px;\n height: 24px;\n}\n.hqpu-card-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.hqpu-title-row {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n min-width: 0;\n}\n.hqpu-card-title {\n min-width: 0;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.hqpu-card-desc {\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 1;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.hqpu-card-end {\n position: relative;\n z-index: 1;\n display: inline-flex;\n flex: none;\n align-items: center;\n gap: 8px;\n}\n.hqpu-card-reason {\n margin: 0;\n padding: 0 8px 8px 70px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-error-primary);\n overflow-wrap: anywhere;\n}\n\n/* ── Tags (ui-primitives Tag.module.css) ──────────────────────────────── */\n.hqpu-tag {\n display: inline-flex;\n align-items: center;\n flex: none;\n border-radius: 999px;\n corner-shape: round;\n padding: 1px 8px;\n font-size: 11px;\n font-weight: 500;\n line-height: 17px;\n white-space: nowrap;\n}\n.hqpu-tag[data-tone='success'] {\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);\n color: var(--dsw-alias-state-success-primary);\n}\n.hqpu-tag[data-tone='info'] {\n background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);\n color: var(--dsw-alias-state-business-primary);\n}\n.hqpu-tag[data-tone='warning'] {\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n}\n.hqpu-tag[data-tone='danger'] {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n color: var(--dsw-alias-state-error-primary);\n}\n.hqpu-tag[data-tone='neutral'] {\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ── Fact rows (PluginManagerPage .facts / .rows) ─────────────────────── */\n.hqpu-facts {\n margin: 0;\n display: flex;\n flex-direction: column;\n}\n.hqpu-facts > div {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n min-height: 44px;\n padding: 8px 2px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n}\n.hqpu-facts > div:last-child {\n border-bottom: 0;\n}\n.hqpu-facts dt {\n flex: none;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-facts dd {\n min-width: 0;\n margin: 0;\n font-size: 13px;\n line-height: 20px;\n text-align: end;\n overflow-wrap: anywhere;\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-facts code {\n padding: 2px 6px;\n border-radius: var(--dsw-radius-xs);\n background: var(--dsw-alias-bg-module-platform);\n font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: 12px;\n}\n\n/* ── Banner (PluginManagerPage .installSafety) ────────────────────────── */\n.hqpu-banner {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 10px 12px;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-state-warn-tertiary);\n font-size: 12.5px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-banner > svg {\n flex: none;\n margin-top: 2px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* ── Status, empty, failure (PluginManagerPage .status / .empty / .failure) */\n.hqpu-status {\n margin: 0;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n}\n.hqpu-status--dot {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n.hqpu-failure {\n display: flex;\n align-items: center;\n gap: 10px;\n color: var(--dsw-alias-state-error-primary);\n}\n.hqpu-empty {\n padding: 32px 0;\n text-align: center;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── State dot + spinner (ui-primitives StateDot.module.css) ──────────── */\n.hqpu-dot {\n position: relative;\n display: inline-block;\n flex: none;\n width: 10px;\n height: 10px;\n}\n.hqpu-dot::after {\n content: '';\n position: absolute;\n inset: 20%;\n border-radius: 50%;\n corner-shape: round;\n background: currentColor;\n}\n.hqpu-dot[data-state='done'] { color: var(--dsw-alias-state-success-primary); }\n.hqpu-dot[data-state='warning'] { color: var(--dsw-alias-state-warn-primary); }\n.hqpu-dot[data-state='error'] { color: var(--dsw-alias-state-error-primary); }\n.hqpu-dot[data-state='idle'] { color: var(--dsw-alias-state-idle-primary); }\n.hqpu-spinner {\n flex: none;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: currentColor;\n animation: hqpu-spin 1.5s linear infinite;\n}\n@keyframes hqpu-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ── Skeleton (PluginManagerPage .skeleton*) ──────────────────────────── */\n.hqpu-skeleton-fill {\n background: var(--dsw-alias-bg-skeleton);\n animation: hqpu-skeleton 2s cubic-bezier(0.36, 0, 0.64, 1) infinite;\n}\n.hqpu-skeleton-text {\n display: flex;\n align-items: center;\n height: 1lh;\n}\n.hqpu-skeleton-bar {\n width: 100%;\n height: 12px;\n border-radius: var(--dsw-radius-xs);\n}\n.hqpu-skeleton-heading { width: 96px; }\n.hqpu-skeleton-title { width: min(144px, 60%); }\n.hqpu-skeleton-desc { width: min(280px, 85%); }\n.hqpu-skeleton-icon { border-color: transparent; }\n@keyframes hqpu-skeleton {\n 0% { opacity: 1; }\n 40% { opacity: 0.6; }\n 80%, 100% { opacity: 1; }\n}\n\n/* ── Toast (ui-primitives Toast.module.css, held inside the panel) ────── */\n.hqpu-toast {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 12px 16px;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-alias-toast-bg);\n color: var(--dsw-alias-toast-label);\n font-size: 14px;\n line-height: 22px;\n box-shadow: var(--dsw-shadow-lv3);\n animation: hqpu-toast-in 160ms ease-out;\n}\n.hqpu-toast-icon {\n display: grid;\n place-items: center;\n flex: none;\n color: var(--dsw-alias-state-warn-label);\n}\n.hqpu-toast-icon[data-tone='success'] { color: var(--dsw-alias-state-success-primary); }\n.hqpu-toast-text { min-width: 0; }\n@keyframes hqpu-toast-in {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: none; }\n}\n\n/* ── Buttons (PluginManagerPage .addButton / .iconButton) ─────────────── */\n.hqpu-icon-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 28px;\n height: 28px;\n padding: 0;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-caption);\n cursor: pointer;\n}\n.hqpu-icon-btn svg { display: block; }\n.hqpu-icon-btn:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-icon-btn:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.hqpu-icon-btn:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n.hqpu-btn {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 32px;\n padding: 0 12px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n cursor: pointer;\n}\n.hqpu-btn svg { display: block; }\n.hqpu-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid); }\n.hqpu-btn:disabled { opacity: 0.5; cursor: default; }\n.hqpu-btn:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n.hqpu-btn--primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.hqpu-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }\n.hqpu-btn--outline {\n background: transparent;\n border: 0.5px solid var(--dsw-alias-border-l4);\n color: var(--dsw-alias-label-secondary);\n}\n.hqpu-btn--outline:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.hqpu-btn--ghost { background: transparent; color: var(--dsw-alias-label-secondary); }\n.hqpu-btn--ghost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.hqpu-btn--danger { color: var(--dsw-alias-state-error-primary); }\n.hqpu-btn--sm { height: 28px; padding: 0 10px; font-size: 12.5px; }\n.hqpu-actions {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* ── Runtime update action row (arm → confirm step) ───────────────────── */\n.hqpu-runtime-actions {\n margin-top: 12px;\n}\n.hqpu-runtime-confirm {\n flex: 1 1 100%;\n margin: 0;\n}\n\n/* ── Runtime update source (fact row + editable URL field) ────────────── */\n.hqpu-source-line {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n max-width: 100%;\n}\n.hqpu-source-url {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.hqpu-source-editor {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 12px;\n}\n/* The plugin-source editor sits inside its own group right under the fact\n * row, so it needs less air than the runtime one under the facts block. */\n.hqpu-source-editor--plugin {\n margin-top: 4px;\n}\n.hqpu-source-hint {\n flex: 1 1 100%;\n margin: 0;\n}\n.hqpu-source-controls {\n flex: 1 1 100%;\n}\n.hqpu-source-input {\n flex: 1 1 240px;\n min-width: 0;\n height: 28px;\n box-sizing: border-box;\n padding: 0 10px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12.5px;\n}\n.hqpu-source-input:disabled {\n opacity: 0.5;\n}\n.hqpu-source-input:focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 1px;\n}\n\n/* ── Footer action row (ui-primitives Modal .footer) ───────────────────── */\n.hqpu-footer {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 0 24px 20px;\n}\n\n.hqpu-visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n\n/* ── Reduced motion: drop every decorative animation ──────────────────── */\n@media (prefers-reduced-motion: reduce) {\n .hqpu-scrim,\n .hqpu-panel,\n .hqpu-skeleton-fill,\n .hqpu-spinner,\n .hqpu-toast {\n animation: none;\n }\n}\n`\n\nconst STYLE_TAG_ID = 'data-plugin-css=\"huaqiu-plugin-update\"'\n\n/**\n * Design tokens the bundled DSH shell may not publish.\n *\n * hq-edge ships an older `ui-theme` build. A live probe of the running shell\n * (both themes) shows 13 of the tokens this stylesheet reads are absent:\n * `--dsw-radius-{xs,sm,md,lg,xl,panel}`, `--dsw-focus-ring-width`,\n * `--dsw-focus-ring-color`, `--dsw-alias-toast-label`,\n * `--dsw-alias-state-idle-primary`, `--dsw-font-mono`,\n * `--dsh-frame-overlay-top` and `--dsh-frame-chrome-top`.\n *\n * An unresolved `var()` is not an error — it silently degrades the property to\n * its initial value. That is how this panel once rendered with\n * `border-radius: 0px` and a 3px `auto` focus ring while every colour was\n * already correct.\n *\n * The last three (`--dsw-font-mono`, `--dsh-frame-*`) are deliberately NOT\n * backfilled: the shell's own primitives rely on the inline `var(…, fallback)`\n * form for those (`Modal.module.css`, `PluginManagerPage.module.css`), so this\n * sheet does too. Everything else is backfilled below with the canonical value\n * from `ui-theme/src/styles/{base,focus,design-platform}.css`.\n *\n * Values are feature-detected at runtime and scoped to this plugin's own roots\n * rather than `:root`, so a newer shell always wins and filling a gap can never\n * restyle the host.\n */\n\n/** The two roots this plugin styles: the view overlay (built in\n * update-view.ts) and the sidebar row ({@link ENTRY_CSS}.entry). */\nconst STYLE_ROOTS = '.hqpu-overlay, .hqpu-sidebar-entry'\n\n/** The shell's dark-theme carrier — audited live as `body[data-ds-dark-theme]`. */\nconst DARK_THEME_SELECTOR = 'body[data-ds-dark-theme]'\n\n/** Same roots, one rule per theme, for tokens whose official value flips. */\nconst DARK_STYLE_ROOTS = STYLE_ROOTS.split(',')\n .map((root) => `${DARK_THEME_SELECTOR} ${root.trim()}`)\n .join(',')\n\ntype DesignTokenFallback =\n | { token: string; value: string }\n | { token: string; value: { light: string; dark: string } }\n\nconst DESIGN_TOKEN_FALLBACKS: DesignTokenFallback[] = [\n // base.css — geometry; theme-invariant.\n { token: '--dsw-radius-xs', value: '4px' },\n { token: '--dsw-radius-sm', value: '8px' },\n { token: '--dsw-radius-md', value: '12px' },\n { token: '--dsw-radius-lg', value: '16px' },\n { token: '--dsw-radius-xl', value: '20px' },\n { token: '--dsw-radius-panel', value: '28px' },\n // focus.css — the standard ring geometry. The colour already falls back\n // inline in the sheet; publishing it too keeps the ring intact if that\n // fallback is ever edited away.\n { token: '--dsw-focus-ring-width', value: '2px' },\n { token: '--dsw-focus-ring-color', value: 'var(--dsw-alias-state-business-primary, #C7000B)' },\n // design-platform.css — `--dsw-static-neutral-bluish-00` is white in both\n // themes, which is exactly what the official alias resolves to.\n { token: '--dsw-alias-toast-label', value: 'var(--dsw-static-neutral-bluish-00, #fff)' },\n // The one token whose official value flips with the theme. `--dsw-static-*`\n // are theme-invariant, so one rule per carrier reproduces neutral-300\n // (light) / neutral-600 (dark) without knowing the current theme.\n {\n token: '--dsw-alias-state-idle-primary',\n value: {\n light: 'var(--dsw-static-neutral-300, #d4d4d4)',\n dark: 'var(--dsw-static-neutral-600, #545557)',\n },\n },\n]\n\nconst TOKEN_STYLE_TAG_ID = 'data-plugin-css=\"huaqiu-plugin-update-tokens\"'\n\n/**\n * Publish canonical values for the design tokens the shell is missing.\n *\n * Idempotent and cheap: safe to call on every open, so a shell that publishes\n * its tokens late still wins (the stale fallbacks are removed in that case).\n */\nexport function ensureDesignTokens(): void {\n if (typeof document === 'undefined' || document.body === null) return\n const probe = getComputedStyle(document.body)\n const missing = DESIGN_TOKEN_FALLBACKS.filter(\n (entry) => probe.getPropertyValue(entry.token).trim() === '',\n )\n\n const existing = document.querySelector<HTMLStyleElement>(`style[${TOKEN_STYLE_TAG_ID}]`)\n if (missing.length === 0) {\n existing?.remove()\n return\n }\n\n const flat = missing.filter(\n (entry): entry is { token: string; value: string } => typeof entry.value === 'string',\n )\n const themed = missing.filter(\n (entry): entry is { token: string; value: { light: string; dark: string } } =>\n typeof entry.value !== 'string',\n )\n\n const rules: string[] = []\n const lightDecls = [\n ...flat.map((entry) => ` ${entry.token}: ${entry.value};`),\n ...themed.map((entry) => ` ${entry.token}: ${entry.value.light};`),\n ]\n rules.push(`${STYLE_ROOTS} {\\n${lightDecls.join('\\n')}\\n}`)\n if (themed.length > 0) {\n const darkDecls = themed.map((entry) => ` ${entry.token}: ${entry.value.dark};`)\n rules.push(`${DARK_STYLE_ROOTS} {\\n${darkDecls.join('\\n')}\\n}`)\n }\n\n const tag = existing ?? document.createElement('style')\n tag.dataset.pluginCss = 'huaqiu-plugin-update-tokens'\n tag.textContent = rules.join('\\n')\n if (tag.parentNode === null) document.head.appendChild(tag)\n}\n\n/** Inject styles once (idempotent). */\nexport function injectUpdateStyles(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.pluginCss = 'huaqiu-plugin-update'\n tag.textContent = `${ENTRY_CSS_TEXT}\\n${VIEW_CSS_TEXT}`\n document.head.appendChild(tag)\n}\n\n/** Minimal structural client context (dsh-client-runtime provides these). */\nexport interface ClientContext {\n get<T = unknown>(name: string): T | undefined\n effect?(fn: () => (() => void) | void, label?: string): void\n}\n\n/** In-memory update-report store shared by the badge and the modal. */\nclass UpdateStore implements UpdateStoreLike {\n private current: PluginUpdateReport | null = null\n private installingFlag = false\n private readonly listeners = new Set<() => void>()\n private refreshTimer: number | null = null\n\n report(): PluginUpdateReport | null {\n return this.current\n }\n\n installing(): boolean {\n return this.installingFlag\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /** Badge count per spec §6.3: actionable `update` + `install`, excluding the\n * ignored offer (the server already folds ignore into item states; keep the\n * client-side filter as a defensive second pass). */\n badgeCount(): number {\n const report = this.current\n if (report === null) return 0\n return report.items.filter(\n (item) => item.state === 'update' || item.state === 'install',\n ).length\n }\n\n private emit(): void {\n for (const listener of [...this.listeners]) {\n try {\n listener()\n } catch {\n /* one bad listener must not strand the rest */\n }\n }\n }\n\n async refresh(): Promise<PluginUpdateReport | null> {\n try {\n const report = await fetchReport()\n this.current = report\n this.emit()\n return report\n } catch {\n return this.current\n }\n }\n\n async check(): Promise<PluginUpdateReport | null> {\n try {\n const report = await checkUpdates()\n this.current = report\n this.emit()\n return report\n } catch {\n return this.current\n }\n }\n\n /** Mark the set as installing (set by the modal before the restart). */\n setInstalling(flag: boolean): void {\n this.installingFlag = flag\n this.emit()\n }\n\n /** Idle refresh while mounted (60 s) — no overlapping poll loop. */\n startIdleRefresh(): void {\n if (this.refreshTimer !== null) return\n this.refreshTimer = window.setInterval(() => {\n void this.refresh()\n }, 60_000) as unknown as number\n }\n\n stop(): void {\n if (this.refreshTimer !== null) {\n window.clearInterval(this.refreshTimer)\n this.refreshTimer = null\n }\n this.listeners.clear()\n }\n}\n\n/**\n * Client cordis inject: no service dependencies — the plugin reads the\n * hq-edge report purely through same-origin fetch (forwarded by its own node\n * half). `inject` stays empty so the loader never waits on another service.\n */\nexport const inject: string[] = []\n\n/**\n * Client plugin body — mount the sidebar toolbutton + review modal.\n * @param ctx - real cordis client context.\n * @returns disposer removing the row and all subscriptions.\n */\nexport function apply(ctx: ClientContext): () => void {\n if (typeof document === 'undefined') return () => {}\n injectUpdateStyles()\n ensureDesignTokens()\n\n const store = new UpdateStore()\n const copy = { zh: COPY.zh, en: COPY.en }\n const copyFor = (): typeof COPY.zh => (locale === 'en' ? copy.en : copy.zh)\n\n // Locale: re-render the row copy when <html lang> changes. The sidebar\n // helper subscribes via `refresh.subscribe`; we fan out to its listener.\n let locale: HostLocale | undefined = detectHostLocale()\n const localeSubscribers = new Set<() => void>()\n const refreshSubscribe = (listener: () => void): (() => void) => {\n localeSubscribers.add(listener)\n return () => {\n localeSubscribers.delete(listener)\n }\n }\n const unsubscribeLocale = observeHostLocale((next) => {\n locale = next\n for (const fn of [...localeSubscribers]) {\n try {\n fn()\n } catch {\n /* one bad subscriber must not strand the rest */\n }\n }\n })\n\n const openView = (): void => {\n // Re-detect the shell's design tokens on every open: a shell that\n // publishes them after this plugin mounted must win, and one that never\n // does must keep the canonical fallbacks.\n ensureDesignTokens()\n // Always re-check on open so the review screen never shows stale data.\n void store.check().then((report) => {\n if (report !== null) openUpdateView(store, copyFor())\n })\n }\n\n const disposeEntry = mountSidebarEntry({\n rowAttribute: UPDATE_ENTRY_ATTRIBUTE,\n rowSelector: UPDATE_ENTRY_SELECTOR,\n plugin: PLUGIN_ID,\n icon: UPDATE_ICON,\n css: ENTRY_CSS,\n // Read the live locale on every refresh (the locale observer fires\n // `refresh.subscribe`'s listener, which re-applies these) so the row\n // re-localizes when the host UI language changes — matching the sibling\n // family rows, which read a live `currentLang` variable the same way.\n label: () => entryCopy(locale).label,\n tooltip: () => entryCopy(locale).tooltip,\n refresh: { subscribe: refreshSubscribe },\n badge: {\n subscribe: (listener) => store.subscribe(listener),\n count: () => store.badgeCount(),\n },\n onToggle: openView,\n position: 'after',\n familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY,\n })\n\n store.startIdleRefresh()\n void store.refresh()\n\n const dispose = (): void => {\n try {\n disposeEntry()\n } catch {\n /* already disposed */\n }\n unsubscribeLocale()\n store.stop()\n }\n if (typeof ctx.effect === 'function') {\n // `ctx.effect` takes a SETUP function and calls it immediately, keeping\n // whatever it returns as the teardown. Passing `dispose` itself would run\n // the teardown synchronously — disconnecting the sidebar-entry observers\n // and stopping the store before the shell ever renders the sidebar, so the\n // row could never be placed. Hand it a factory that returns the teardown\n // instead (the same shape the sibling tool plugins use).\n ctx.effect(() => dispose, 'plugin-update')\n }\n return dispose\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-tool-plugin-update",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.mjs",
|
|
6
6
|
"types": "./lib/index.d.mts",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.2"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@huaqiu/dsh-plugin-log": "0.6.
|
|
35
|
-
"@huaqiu/dsh-sidebar-entry": "0.6.
|
|
34
|
+
"@huaqiu/dsh-plugin-log": "0.6.5",
|
|
35
|
+
"@huaqiu/dsh-sidebar-entry": "0.6.5"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"lib",
|
package/src/client/api.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
+
import { normalizePluginUpdateReport } from './version.js'
|
|
10
11
|
|
|
11
12
|
export const UPDATE_BASE = '/api/v1/huaqiu/plugin-update'
|
|
12
13
|
|
|
@@ -15,6 +16,12 @@ export interface PluginUpdateItem {
|
|
|
15
16
|
/** Version installed in the active bundle; null when the plugin is absent. */
|
|
16
17
|
currentVersion: string | null
|
|
17
18
|
availableVersion: string
|
|
19
|
+
/**
|
|
20
|
+
* Server state. `update` is re-checked on arrival against the SemVer
|
|
21
|
+
* direction rule (`version.ts`): an offer that is not strictly newer than
|
|
22
|
+
* `currentVersion` is demoted to `current`, so the panel and the badge can
|
|
23
|
+
* never offer a downgrade even against an older hq-edge.
|
|
24
|
+
*/
|
|
18
25
|
state: 'current' | 'update' | 'install' | 'blocked' | 'ignored'
|
|
19
26
|
/** The block reason, or null when the item is not blocked. The server sends a
|
|
20
27
|
* single code (`"runtime-unknown"`), so it is normalized before use. */
|
|
@@ -137,12 +144,12 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
137
144
|
|
|
138
145
|
/** GET the cached (or empty-shell) plugin-update report. */
|
|
139
146
|
export function fetchReport(): Promise<PluginUpdateReport> {
|
|
140
|
-
return request<PluginUpdateReport>('/report')
|
|
147
|
+
return request<PluginUpdateReport>('/report').then(normalizePluginUpdateReport)
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
/** POST an explicit refresh; returns the freshly computed report. */
|
|
144
151
|
export function checkUpdates(): Promise<PluginUpdateReport> {
|
|
145
|
-
return request<PluginUpdateReport>('/check', { method: 'POST' })
|
|
152
|
+
return request<PluginUpdateReport>('/check', { method: 'POST' }).then(normalizePluginUpdateReport)
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
export interface InstallResult {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SemVer direction gate for plugin-update items (browser half).
|
|
3
|
+
*
|
|
4
|
+
* The rule this module exists to enforce, for EVERY plugin in the report:
|
|
5
|
+
* an item is only "update available" — and only gets an install control —
|
|
6
|
+
* when the version offered by the release manifest is strictly GREATER than
|
|
7
|
+
* the version installed in the active bundle. A manifest entry that merely
|
|
8
|
+
* differs from the installed version offers nothing: equality is the
|
|
9
|
+
* already-installed case, and a manifest that LAGS behind the installed
|
|
10
|
+
* build (a stale mirror or a rollback channel publishing 0.6.2 against an
|
|
11
|
+
* installed 0.6.4) must never surface as a downgrade "update".
|
|
12
|
+
*
|
|
13
|
+
* Why this lives on the client even though hq-edge owns the decision: the
|
|
14
|
+
* report crosses a process boundary, and an older hq-edge still carries the
|
|
15
|
+
* old "differs ⇒ update" rule — which rendered `analytics 可更新
|
|
16
|
+
* 0.6.4 → 0.6.2` with a live 安装 button for the whole plugin set. This is
|
|
17
|
+
* the same defensive second pass the badge already applies to the ignored
|
|
18
|
+
* offer: one authoritative decision upstream, one guard where it is shown.
|
|
19
|
+
*
|
|
20
|
+
* The comparator is a strict SemVer 2.0.0 implementation (no dependency —
|
|
21
|
+
* this package ships no runtime dependencies beyond the workspace log +
|
|
22
|
+
* sidebar helpers). Anything that is not clean SemVer on either side makes
|
|
23
|
+
* the comparison `null`, and an unprovable direction fails CLOSED: the item
|
|
24
|
+
* reads as current, never as an update.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import type { PluginUpdateItem, PluginUpdateReport } from './api.js'
|
|
29
|
+
|
|
30
|
+
/** Strict SemVer 2.0.0: MAJOR.MINOR.PATCH[-prerelease][+build]. */
|
|
31
|
+
const SEMVER_RE =
|
|
32
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
|
33
|
+
|
|
34
|
+
interface ParsedVersion {
|
|
35
|
+
major: number
|
|
36
|
+
minor: number
|
|
37
|
+
patch: number
|
|
38
|
+
/** Prerelease identifiers (empty = a release, which outranks any prerelease). */
|
|
39
|
+
prerelease: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parse a strict SemVer string, or null when it is not one. */
|
|
43
|
+
function parseSemver(version: string): ParsedVersion | null {
|
|
44
|
+
const match = SEMVER_RE.exec(version.trim())
|
|
45
|
+
if (match === null) return null
|
|
46
|
+
return {
|
|
47
|
+
major: Number(match[1]),
|
|
48
|
+
minor: Number(match[2]),
|
|
49
|
+
patch: Number(match[3]),
|
|
50
|
+
prerelease: match[4] === undefined ? [] : match[4].split('.'),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when one prerelease identifier is purely numeric. */
|
|
55
|
+
function isNumericIdentifier(id: string): boolean {
|
|
56
|
+
return /^\d+$/.test(id)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compare two prerelease identifier lists per SemVer §11:
|
|
61
|
+
* numeric identifiers compare numerically and rank below alphanumeric ones;
|
|
62
|
+
* a larger set of identifiers ranks higher when all shared ones are equal.
|
|
63
|
+
*/
|
|
64
|
+
function comparePrerelease(a: string[], b: string[]): number {
|
|
65
|
+
// No prerelease outranks any prerelease.
|
|
66
|
+
if (a.length === 0 && b.length === 0) return 0
|
|
67
|
+
if (a.length === 0) return 1
|
|
68
|
+
if (b.length === 0) return -1
|
|
69
|
+
const shared = Math.min(a.length, b.length)
|
|
70
|
+
for (let index = 0; index < shared; index += 1) {
|
|
71
|
+
const left = a[index]
|
|
72
|
+
const right = b[index]
|
|
73
|
+
if (left === undefined || right === undefined) continue
|
|
74
|
+
if (left === right) continue
|
|
75
|
+
const leftNumeric = isNumericIdentifier(left)
|
|
76
|
+
const rightNumeric = isNumericIdentifier(right)
|
|
77
|
+
if (leftNumeric && rightNumeric) {
|
|
78
|
+
return Number(left) < Number(right) ? -1 : 1
|
|
79
|
+
}
|
|
80
|
+
if (leftNumeric !== rightNumeric) {
|
|
81
|
+
// Numeric identifiers always have lower precedence than alphanumeric.
|
|
82
|
+
return leftNumeric ? -1 : 1
|
|
83
|
+
}
|
|
84
|
+
return left < right ? -1 : 1
|
|
85
|
+
}
|
|
86
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compare two strict SemVer strings: negative when `a < b`, positive when
|
|
91
|
+
* `a > b`, 0 when equal, and null when either side is not SemVer (build
|
|
92
|
+
* metadata is ignored in precedence, per spec).
|
|
93
|
+
*/
|
|
94
|
+
export function compareSemver(a: string, b: string): number | null {
|
|
95
|
+
const left = parseSemver(a)
|
|
96
|
+
const right = parseSemver(b)
|
|
97
|
+
if (left === null || right === null) return null
|
|
98
|
+
if (left.major !== right.major) return left.major < right.major ? -1 : 1
|
|
99
|
+
if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1
|
|
100
|
+
if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1
|
|
101
|
+
return comparePrerelease(left.prerelease, right.prerelease)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether the offered release is an actionable upgrade over the installed
|
|
106
|
+
* version: an absent plugin (nothing installed) is always installable, and
|
|
107
|
+
* an installed one only when the offered SemVer is strictly greater. An
|
|
108
|
+
* unprovable direction (non-SemVer on either side) is NOT an upgrade.
|
|
109
|
+
*/
|
|
110
|
+
export function isVersionUpgrade(currentVersion: string | null, availableVersion: string): boolean {
|
|
111
|
+
if (currentVersion === null) return true
|
|
112
|
+
const order = compareSemver(availableVersion, currentVersion)
|
|
113
|
+
return order !== null && order > 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Re-evaluate one report item against the direction rule.
|
|
118
|
+
*
|
|
119
|
+
* A server-reported `update` that is not a strict SemVer upgrade is demoted
|
|
120
|
+
* to `current`: the plugin is at (or ahead of) the offered release, there is
|
|
121
|
+
* nothing to install, and no install control may be offered for it. Every
|
|
122
|
+
* other state passes through untouched — `install` (plugin absent locally)
|
|
123
|
+
* has no local version to compare, and `blocked`/`ignored`/`current` are
|
|
124
|
+
* already non-actionable. Unknown server fields are preserved verbatim: the
|
|
125
|
+
* client stays a thin reader of the hq-edge report shape.
|
|
126
|
+
*/
|
|
127
|
+
export function normalizePluginUpdateItem<T extends PluginUpdateItem>(item: T): T {
|
|
128
|
+
if (item.state !== 'update') return item
|
|
129
|
+
if (isVersionUpgrade(item.currentVersion, item.availableVersion)) return item
|
|
130
|
+
return { ...item, state: 'current' }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** States that carry an install action (the badge/actionable definition). */
|
|
134
|
+
function isActionableState(item: PluginUpdateItem): boolean {
|
|
135
|
+
return item.state === 'update' || item.state === 'install'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Apply {@link normalizePluginUpdateItem} to a whole report and recompute
|
|
140
|
+
* `actionableCount` from the corrected states, so the server's count can
|
|
141
|
+
* never disagree with what the panel and the sidebar badge render.
|
|
142
|
+
*/
|
|
143
|
+
export function normalizePluginUpdateReport<T extends PluginUpdateReport>(report: T): T {
|
|
144
|
+
if (!Array.isArray(report.items)) return report
|
|
145
|
+
const items = report.items.map(normalizePluginUpdateItem)
|
|
146
|
+
return {
|
|
147
|
+
...report,
|
|
148
|
+
items,
|
|
149
|
+
actionableCount: items.filter(isActionableState).length,
|
|
150
|
+
}
|
|
151
|
+
}
|