@dan-ai-studio/dshopencodego 0.1.91 → 0.1.93

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/README.en.md CHANGED
@@ -82,6 +82,8 @@ Why: Electron owns the Desktop profile, and **the CLI cannot boot or mutate it**
82
82
 
83
83
  The version constraint matches the CLI: the DSH bundled with the Desktop app must stay inside the declared and verified band (`>=0.2.0-rc.1 <0.3.0`, see "Compatibility"). If the plugin ever breaks startup, the Desktop's native recovery dialog can **disable third-party plugins, back up `cordis.patch.yml`, and restart** in one action — it will not lock the app out.
84
84
 
85
+ The settings page also carries a **Desktop app** row: **Check download address** reads the official feed at `https://download.deepseek.com/dsh-desk/feeds/win-x64/nightly.yml` and shows the newest installer's version, publication date, size, staged-rollout share, SHA-512, and address — the address can be copied or opened in a browser. This **only reads the feed and shows the address**: the plugin never downloads, verifies, or installs anything. The Host caches the reading for six hours, and an unreachable feed says so instead of pretending nothing was published.
86
+
85
87
  ## Configuration
86
88
 
87
89
  Configuration lives in the profile's `cordis.patch.yml`:
@@ -258,7 +260,7 @@ The verdict:
258
260
  Things to keep in mind when maintaining this:
259
261
 
260
262
  - **Required versus optional peers**: nine of the twenty-one are marked `optional` (`dsh-api-remotes`, `dsh-client-locale`, `dsh-client-store`, `dsh-client-ui-layout`, `dsh-client-ui-model-selection`, `dsh-client-ui-primitives`, `dsh-client-ui-settings`, `dsh-client-ui-sidebar`, `dsh-client-ui-slots`). The other twelve are required at runtime: `cordis`, `dsh-attachment`, `dsh-brand`, `dsh-client-ui-conversation`, `dsh-client-ui-renderer`, `dsh-credentials`, `dsh-fs`, `dsh-launch-environment`, `dsh-llm`, `dsh-timeout`, `dsh-typert-protocol`, and `dsh-typert-registry`. None of the twenty-one takes part in the load verdict today (see above: `engines.dsh` decides alone); their value is declaring the dependency for the package manager and for readers. `dsh-llm` is the heaviest coupling — more than twenty imports across the source.
261
- - **Declared range ≠ verified range**: the declaration is `>=0.2.0-rc.1 <0.3.0-0`. What was actually verified here, in order, is `0.2.0-rc.1` (the dev-dependency baseline: gate verdict + typecheck + full suite), `0.2.0-rc.2` (the same plus a `prepareCall` generation freeze), `0.2.1-alpha.3` (the first target release), `0.2.1-alpha.4` (T-24), `0.2.1-alpha.5` (T-26), and `0.2.1-rc.2` (T-31, where the runtime moved to cordis `4.0.5`; the upstream contract layer had zero source changes). Other versions inside the range were not verified one by one.
263
+ - **Declared range ≠ verified range**: the declaration is `>=0.2.0-rc.1 <0.3.0-0`. What was actually verified here, in order, is `0.2.0-rc.1` (the dev-dependency baseline: gate verdict + typecheck + full suite), `0.2.0-rc.2` (the same plus a `prepareCall` generation freeze), `0.2.1-alpha.3` (the first target release), `0.2.1-alpha.4` (T-24), `0.2.1-alpha.5` (T-26), `0.2.1-rc.2` (T-31, where the runtime moved to cordis `4.0.5`; the upstream contract layer had zero source changes), `0.2.2-rc.1` (cordis `4.1.0`), `0.2.2-rc.2` (`LlmError`'s `code` narrowed to its built-in union), and `0.2.2-rc.3` (the current dev-dependency baseline). Other versions inside the range were not verified one by one.
262
264
  - **The upper bound is a tracking line**: `<0.3.0-0` means that the moment DSH ships `0.3.0-0` (the first prerelease on the `0.3.0` line), this plugin must ship a new release in the same window or every user loses the plugin. `tests/peer-coverage.spec.ts` asserts both ends — the lower bound accepts the installed version, the upper bound rejects `0.3.0-0` — so a drift here fails locally instead of failing on a user's machine. After changing a range, `npm test` is the verification (local mock gateway, no network, no tokens) — ranges follow what the interfaces declare, and are never "measured" with live probes.
263
265
  - **Peer coverage is now guarded both ways**: `dsh-typert-registry`, `dsh-client-ui-conversation`, and `dsh-client-ui-renderer` were imported by the source but absent from `peerDependencies`, invisible to the gate; they are declared now, and `tests/peer-coverage.spec.ts` asserts both directions — anything the source imports must be declared, and anything declared but unreached must be removed (`dsh-settings` went that way: the settings form is actually provided by `dsh-client-ui-settings`).
264
266
  - **Exemptions are per profile**: only a profile that ran `dsh plugin allow-version` has one, and no record means the plugin must stay inside the declared range. `dsh plugin version-exemptions` lists a profile's exemptions.
@@ -291,7 +293,7 @@ npm run seed # refresh the bundled catalog seed before a release (see "Re
291
293
 
292
294
  **Order matters**: `npm run build` must come before `npm test` — `tests/pack-contents.spec.ts` walks every `exports` target, and whether `lib/` exists decides whether that check runs or is silently skipped (it does not fail, it just stops protecting you). `package.json` no longer carries `prepare`, so `npm ci` will not build for you; all three CI workflows run `npm run build` explicitly.
293
295
 
294
- The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **61 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
296
+ The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **64 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
295
297
 
296
298
  ## Releasing (maintainers)
297
299
 
package/README.md CHANGED
@@ -82,6 +82,8 @@ DSH 桌面端是 Electron 壳 + 完整的 dsh Web 应用,本插件对它同样
82
82
 
83
83
  版本约束与 CLI 一致:桌面端内置的 DSH 要落在已声明并验证过的版本带内(`>=0.2.0-rc.1 <0.3.0`,见「兼容性」)。若插件导致启动失败,桌面端的原生恢复对话框可以一键**禁用第三方插件、备份 `cordis.patch.yml` 后重启**,不会把应用锁死。
84
84
 
85
+ 设置页里另有一栏**「桌面版」**:点「检查下载地址」会读取官方发布源 `https://download.deepseek.com/dsh-desk/feeds/win-x64/nightly.yml`,显示当前最新安装包的版本、发布时间、大小、灰度比例、SHA-512 与下载地址,地址可一键复制或用浏览器打开。这里**只读发布源、只显示地址**——插件不会下载、校验或安装任何东西。结果在 Host 侧缓存 6 小时,发布源没响应时会明确说「下载站没有响应」,而不是假装「没有新版本」。
86
+
85
87
  ## 配置
86
88
 
87
89
  配置写在 profile 的 `cordis.patch.yml`:
@@ -257,7 +259,7 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
257
259
  维护时需要注意:
258
260
 
259
261
  - **必选与可选之分**:21 个 peer(20 个 `dsh-*` 加框架 `cordis`)中 9 个标了 `optional`(`dsh-api-remotes`、`dsh-client-locale`、`dsh-client-store`、`dsh-client-ui-layout`、`dsh-client-ui-model-selection`、`dsh-client-ui-primitives`、`dsh-client-ui-settings`、`dsh-client-ui-sidebar`、`dsh-client-ui-slots`);其余 12 个是运行时必需的——`cordis`、`dsh-attachment`、`dsh-brand`、`dsh-client-ui-conversation`、`dsh-client-ui-renderer`、`dsh-credentials`、`dsh-fs`、`dsh-launch-environment`、`dsh-llm`、`dsh-timeout`、`dsh-typert-protocol`、`dsh-typert-registry`。这 21 个当前都**不参与加载判定**(见上:`engines.dsh` 单独决定),它们的价值在于声明依赖并供包管理器与读者使用。其中 `dsh-llm` 是最重的一处耦合(源码 import 二十余处)。
260
- - **名义范围 ≠ 实测范围**:声明范围是 `>=0.2.0-rc.1 <0.3.0-0`,本项目实际验证过的基线依次是 `0.2.0-rc.1`(开发依赖基线:门禁判定 + typecheck + 全量用例)、`0.2.0-rc.2`(同上 + `prepareCall` 代次冻结)、`0.2.1-alpha.3`(首个目标版本)、`0.2.1-alpha.4`(T-24)、`0.2.1-alpha.5`(T-26)与 `0.2.1-rc.2`(T-31,运行时升到 cordis `4.0.5`;上游契约层源码零变更)。范围内的其他版本没有逐个验证过。
262
+ - **名义范围 ≠ 实测范围**:声明范围是 `>=0.2.0-rc.1 <0.3.0-0`,本项目实际验证过的基线依次是 `0.2.0-rc.1`(开发依赖基线:门禁判定 + typecheck + 全量用例)、`0.2.0-rc.2`(同上 + `prepareCall` 代次冻结)、`0.2.1-alpha.3`(首个目标版本)、`0.2.1-alpha.4`(T-24)、`0.2.1-alpha.5`(T-26)、`0.2.1-rc.2`(T-31,运行时升到 cordis `4.0.5`;上游契约层源码零变更)、`0.2.2-rc.1`(cordis `4.1.0`)、`0.2.2-rc.2`(`LlmError` 的 `code` 收窄为内置联合类型)与 `0.2.2-rc.3`(当前开发依赖基线)。范围内的其他版本没有逐个验证过。
261
263
  - **上界就是跟版线**:`<0.3.0-0` 意味着 DSH 一旦发布 `0.3.0-0`(`0.3.0` 线的第一个预发布版),本插件必须同批发新版,否则所有用户加载失败。`tests/peer-coverage.spec.ts` 会同时断言下界接受已安装版本、上界拒绝 `0.3.0-0`,所以这条线漂移会在本地测试里先失败,而不是等用户加载失败。调整范围后用 `npm test` 验证即可(本地 mock 网关,零网络零 token)——范围以接口声明为准,不要用线上探测来"测"出一个范围。
262
264
  - **peer 覆盖已双向守住**:`dsh-typert-registry`、`dsh-client-ui-conversation`、`dsh-client-ui-renderer` 曾被源码 import 却未声明、门禁管不到,现已补进 `peerDependencies`;`tests/peer-coverage.spec.ts` 双向断言——源码用到的必须声明,声明了却没人用的必须删(`dsh-settings` 就这样被移除:设置表单实际由 `dsh-client-ui-settings` 提供)。
263
265
  - **豁免是按 profile 记的**:只有显式执行过 `dsh plugin allow-version` 的 profile 才享有豁免;没有记录就代表必须落在声明范围内。用 `dsh plugin version-exemptions` 可查看当前 profile 的豁免。
@@ -290,7 +292,7 @@ npm run seed # 发版前刷新捆绑的目录种子(见「发布」)
290
292
 
291
293
  **顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
292
294
 
293
- 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **61 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
295
+ 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **64 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
294
296
 
295
297
  ## 发布(维护者)
296
298
 
package/lib/client.js CHANGED
@@ -717,7 +717,23 @@ var en = {
717
717
  updateInspectRefused: "The published target could not be verified, so the installed version was left untouched:",
718
718
  updateStopProfile: "The running version cannot be removed without a restart; restart DSH first, then retry.",
719
719
  updateIncompatible: "The new version is incompatible with this DSH build; finish the update from the plugin manager page (a version exemption needs your explicit approval).",
720
- updatePendingBuilds: "Installation needs build-script approval; approve it on the plugin manager page, then retry."
720
+ updatePendingBuilds: "Installation needs build-script approval; approve it on the plugin manager page, then retry.",
721
+ desktopTitle: "Desktop app",
722
+ desktopHint: "The newest installer published for this platform. The address is shown, never downloaded \u2014 copy it or open it in a browser.",
723
+ desktopCheck: "Check download address",
724
+ desktopChecking: "Checking the download host\u2026",
725
+ desktopVersion: "Newest build:",
726
+ desktopPublished: "published",
727
+ desktopSize: "size",
728
+ desktopStaging: "staged rollout",
729
+ desktopCopy: "Copy address",
730
+ desktopCopied: "Copied",
731
+ desktopCopyFailed: "The address could not be copied; select it and copy by hand.",
732
+ desktopOpen: "Open download",
733
+ desktopUnavailable: "The download host did not answer, so no installer address was read; choose Check download address to retry.",
734
+ desktopFailed: "The check failed:",
735
+ desktopRemoteMissing: "The desktop download service is not ready yet; choose Check download address to retry.",
736
+ desktopNoRelease: "The download host answered, but published no installer for this platform."
721
737
  };
722
738
  var zh = {
723
739
  nav: "OpenCode Go",
@@ -929,7 +945,23 @@ var zh = {
929
945
  updateInspectRefused: "\u65E0\u6CD5\u9A8C\u8BC1\u53D1\u5E03\u7684\u76EE\u6807\uFF0C\u5DF2\u4FDD\u7559\u5F53\u524D\u7248\u672C\u672A\u505A\u6539\u52A8\uFF1A",
930
946
  updateStopProfile: "\u8FD0\u884C\u4E2D\u7684\u7248\u672C\u9700\u91CD\u542F\u540E\u624D\u80FD\u79FB\u9664\uFF1B\u8BF7\u5148\u91CD\u542F DSH \u518D\u91CD\u8BD5\u3002",
931
947
  updateIncompatible: "\u65B0\u7248\u672C\u4E0E\u5F53\u524D DSH \u4E0D\u517C\u5BB9\uFF0C\u8BF7\u524D\u5F80\u63D2\u4EF6\u7BA1\u7406\u9875\u5B8C\u6210\u66F4\u65B0\uFF08\u7248\u672C\u8C41\u514D\u9700\u660E\u786E\u6279\u51C6\uFF09\u3002",
932
- updatePendingBuilds: "\u5B89\u88C5\u9700\u8981\u6279\u51C6\u6784\u5EFA\u811A\u672C\uFF1B\u8BF7\u524D\u5F80\u63D2\u4EF6\u7BA1\u7406\u9875\u6279\u51C6\u540E\u518D\u91CD\u8BD5\u3002"
948
+ updatePendingBuilds: "\u5B89\u88C5\u9700\u8981\u6279\u51C6\u6784\u5EFA\u811A\u672C\uFF1B\u8BF7\u524D\u5F80\u63D2\u4EF6\u7BA1\u7406\u9875\u6279\u51C6\u540E\u518D\u91CD\u8BD5\u3002",
949
+ desktopTitle: "\u684C\u9762\u7248",
950
+ desktopHint: "\u5F53\u524D\u5E73\u53F0\u6700\u65B0\u53D1\u5E03\u7684\u5B89\u88C5\u5305\u3002\u8FD9\u91CC\u53EA\u663E\u793A\u5730\u5740\uFF0C\u4E0D\u4F1A\u4E0B\u8F7D\u2014\u2014\u53EF\u4EE5\u590D\u5236\uFF0C\u6216\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u3002",
951
+ desktopCheck: "\u68C0\u67E5\u4E0B\u8F7D\u5730\u5740",
952
+ desktopChecking: "\u6B63\u5728\u67E5\u8BE2\u4E0B\u8F7D\u7AD9\u2026",
953
+ desktopVersion: "\u6700\u65B0\u7248\u672C\uFF1A",
954
+ desktopPublished: "\u53D1\u5E03\u4E8E",
955
+ desktopSize: "\u5927\u5C0F",
956
+ desktopStaging: "\u7070\u5EA6\u6BD4\u4F8B",
957
+ desktopCopy: "\u590D\u5236\u5730\u5740",
958
+ desktopCopied: "\u5DF2\u590D\u5236",
959
+ desktopCopyFailed: "\u5730\u5740\u590D\u5236\u5931\u8D25\uFF1B\u8BF7\u624B\u52A8\u9009\u4E2D\u540E\u590D\u5236\u3002",
960
+ desktopOpen: "\u6253\u5F00\u4E0B\u8F7D",
961
+ desktopUnavailable: "\u4E0B\u8F7D\u7AD9\u6CA1\u6709\u54CD\u5E94\uFF0C\u672A\u83B7\u53D6\u5230\u5B89\u88C5\u5305\u5730\u5740\uFF1B\u8BF7\u70B9\u51FB\u201C\u68C0\u67E5\u4E0B\u8F7D\u5730\u5740\u201D\u91CD\u8BD5\u3002",
962
+ desktopFailed: "\u68C0\u67E5\u5931\u8D25\uFF1A",
963
+ desktopRemoteMissing: "\u684C\u9762\u7248\u4E0B\u8F7D\u670D\u52A1\u5C1A\u672A\u5C31\u7EEA\uFF1B\u8BF7\u70B9\u51FB\u201C\u68C0\u67E5\u4E0B\u8F7D\u5730\u5740\u201D\u91CD\u8BD5\u3002",
964
+ desktopNoRelease: "\u4E0B\u8F7D\u7AD9\u6709\u54CD\u5E94\uFF0C\u4F46\u6CA1\u6709\u4E3A\u8BE5\u5E73\u53F0\u53D1\u5E03\u5B89\u88C5\u5305\u3002"
933
965
  };
934
966
 
935
967
  // src/client/Section.tsx
@@ -1113,7 +1145,8 @@ var INITIAL = {
1113
1145
  writable: false,
1114
1146
  saving: false,
1115
1147
  filter: INITIAL_FILTER,
1116
- update: { phase: "idle", status: void 0, message: void 0 }
1148
+ update: { phase: "idle", status: void 0, message: void 0 },
1149
+ desktop: { phase: "idle", reading: void 0, message: void 0, copied: false }
1117
1150
  };
1118
1151
  function settingsOf(value) {
1119
1152
  if (value === null || typeof value !== "object") return void 0;
@@ -1166,6 +1199,7 @@ var SectionController = class _SectionController {
1166
1199
  void this.loadKey();
1167
1200
  void this.loadNetwork();
1168
1201
  void this.checkUpdate(false);
1202
+ void this.checkDesktop(false);
1169
1203
  }
1170
1204
  subscribe = (listener) => {
1171
1205
  this.listeners.add(listener);
@@ -1375,6 +1409,71 @@ var SectionController = class _SectionController {
1375
1409
  });
1376
1410
  }
1377
1411
  }
1412
+ /**
1413
+ * The desktop download Remote.
1414
+ *
1415
+ * Declared in `client/index.ts`'s inject list, so the namespace is mounted by
1416
+ * the time this controller exists; the guard is a defensive assertion, not a
1417
+ * wait, for the same reason the update Remote's is.
1418
+ * @returns the mounted desktop Remote.
1419
+ * @throws {Error} `remote-not-mounted` when the namespace is absent.
1420
+ */
1421
+ desktopRemote() {
1422
+ const remote = this.services.remote.opencodeGoDesktop;
1423
+ if (remote === void 0) throw new Error("remote-not-mounted");
1424
+ return remote;
1425
+ }
1426
+ /**
1427
+ * Ask the Host which installer the desktop download host publishes.
1428
+ *
1429
+ * The Host answers from a cached feed reading and fails silent, so an
1430
+ * unreachable download host still answers "no installer known" rather than
1431
+ * erroring the page. Runs once with the page and again only on explicit
1432
+ * refresh — same shape as the update check, and for the same reason: both
1433
+ * wire methods take zero arguments.
1434
+ */
1435
+ async checkDesktop(force) {
1436
+ const previous = this.state.desktop;
1437
+ this.set({ desktop: { ...previous, phase: "checking", message: void 0, copied: false } });
1438
+ try {
1439
+ const remote = this.desktopRemote();
1440
+ const result = force ? await remote.refreshDesktop() : await remote.readDesktop();
1441
+ if (!result.ok) throw result.error;
1442
+ const reading = result.value;
1443
+ this.set({
1444
+ desktop: {
1445
+ // An unanswered feed is not "nothing published": rendering the two
1446
+ // alike would report a dead download host as an empty channel.
1447
+ phase: !reading.checked ? "failed" : reading.release === void 0 ? "empty" : "ready",
1448
+ reading,
1449
+ message: reading.checked ? void 0 : "feed-unreachable",
1450
+ copied: false
1451
+ }
1452
+ });
1453
+ } catch (error) {
1454
+ this.set({
1455
+ desktop: {
1456
+ phase: "failed",
1457
+ reading: previous.reading,
1458
+ message: error instanceof Error ? error.message : "the download check failed",
1459
+ copied: false
1460
+ }
1461
+ });
1462
+ }
1463
+ }
1464
+ /** Copy the installer address to the clipboard; the address stays on screen either way. */
1465
+ async copyDesktopAddress() {
1466
+ const url = this.state.desktop.reading?.release?.url;
1467
+ if (url === void 0) return;
1468
+ try {
1469
+ const clipboard = globalThis.navigator?.clipboard;
1470
+ if (clipboard === void 0) throw new Error("no clipboard");
1471
+ await clipboard.writeText(url);
1472
+ this.set({ desktop: { ...this.state.desktop, copied: true, message: void 0 } });
1473
+ } catch {
1474
+ this.set({ desktop: { ...this.state.desktop, copied: false, message: "copy-failed" } });
1475
+ }
1476
+ }
1378
1477
  /** Arm the confirm step; the destructive two-step runs only from `applyUpdate`. */
1379
1478
  armUpdate() {
1380
1479
  if (this.state.update.phase === "ready") {
@@ -1804,6 +1903,82 @@ function updateFailureText(message, t) {
1804
1903
  if (message === "pending-builds") return t("updatePendingBuilds");
1805
1904
  return message;
1806
1905
  }
1906
+ function formatBytes(bytes2) {
1907
+ const units = ["B", "KB", "MB", "GB", "TB"];
1908
+ let value = bytes2;
1909
+ let unit = 0;
1910
+ while (value >= 1024 && unit < units.length - 1) {
1911
+ value /= 1024;
1912
+ unit += 1;
1913
+ }
1914
+ const rounded = unit === 0 || value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
1915
+ return `${rounded} ${units[unit] ?? "B"}`;
1916
+ }
1917
+ function formatReleaseDate(value) {
1918
+ const at = new Date(value);
1919
+ if (Number.isNaN(at.getTime())) return value;
1920
+ return at.toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" });
1921
+ }
1922
+ function DesktopBanner({ controller, t, busy, act, desktop }) {
1923
+ if (desktop.phase === "idle") return null;
1924
+ const release = desktop.reading?.release;
1925
+ const check = () => {
1926
+ void act(async () => {
1927
+ await controller.checkDesktop(true);
1928
+ });
1929
+ };
1930
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.block, children: [
1931
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
1932
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("desktopTitle") }),
1933
+ desktop.phase === "checking" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: t("desktopChecking") }),
1934
+ desktop.phase === "ready" && release !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: `${t("desktopVersion")} ${release.version}` }),
1936
+ release.releaseDate !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: `${t("desktopPublished")} ${formatReleaseDate(release.releaseDate)}` }),
1937
+ release.size !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: `${t("desktopSize")} ${formatBytes(release.size)}` }),
1938
+ release.stagingPercentage !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: `${t("desktopStaging")} ${release.stagingPercentage}%` }),
1939
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: section_default.button, disabled: busy, onClick: check, children: t("desktopCheck") })
1940
+ ] }),
1941
+ desktop.phase === "empty" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1942
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: t("desktopNoRelease") }),
1943
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: section_default.button, disabled: busy, onClick: check, children: t("desktopCheck") })
1944
+ ] }),
1945
+ desktop.phase === "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1946
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.warn, children: t("desktopFailed") }),
1947
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.hint, children: desktopFailureText(desktop.message, t) }),
1948
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: section_default.button, disabled: busy, onClick: check, children: t("desktopCheck") })
1949
+ ] })
1950
+ ] }),
1951
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.hint, children: t("desktopHint") }),
1952
+ desktop.phase === "ready" && release !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1953
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
1954
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.mono, children: release.url }),
1955
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1956
+ "button",
1957
+ {
1958
+ type: "button",
1959
+ className: section_default.button,
1960
+ disabled: busy,
1961
+ onClick: () => {
1962
+ void act(async () => {
1963
+ await controller.copyDesktopAddress();
1964
+ });
1965
+ },
1966
+ children: desktop.copied ? t("desktopCopied") : t("desktopCopy")
1967
+ }
1968
+ ),
1969
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: section_default.button, href: release.url, target: "_blank", rel: "noreferrer", children: t("desktopOpen") })
1970
+ ] }),
1971
+ desktop.message === "copy-failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.warn, children: t("desktopCopyFailed") }),
1972
+ release.sha512 !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.hint, children: `SHA-512 ${release.sha512}` })
1973
+ ] })
1974
+ ] });
1975
+ }
1976
+ function desktopFailureText(message, t) {
1977
+ if (message === void 0 || message.length === 0) return "";
1978
+ if (message === "remote-not-mounted") return t("desktopRemoteMissing");
1979
+ if (message === "feed-unreachable") return t("desktopUnavailable");
1980
+ return message;
1981
+ }
1807
1982
  function Section({ controller, t, getLocale, openRequestLog }) {
1808
1983
  const state = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.snapshot, controller.snapshot);
1809
1984
  const [keyDraft, setKeyDraft] = (0, import_react.useState)("");
@@ -1834,6 +2009,7 @@ function Section({ controller, t, getLocale, openRequestLog }) {
1834
2009
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: section_default.title, children: t("nav") }),
1835
2010
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.hint, children: t("intro") }),
1836
2011
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UpdateBanner, { controller, t, busy, act, update: state.update }),
2012
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DesktopBanner, { controller, t, busy, act, desktop: state.desktop }),
1837
2013
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.block, children: [
1838
2014
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
1839
2015
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("keyTitle") }),
@@ -3292,7 +3468,7 @@ function apply(ctx) {
3292
3468
  void dispose?.();
3293
3469
  };
3294
3470
  }, "dshopencodego: usage Remote");
3295
- ctx.inject(["configForms", "remote.opencodeGoCatalog", "remote.opencodeGoUpdate", "remote.opencodeGoNetwork", "remote.opencodeGoRequestLog", "remote.credentials"], (scope) => {
3471
+ ctx.inject(["configForms", "remote.opencodeGoCatalog", "remote.opencodeGoUpdate", "remote.opencodeGoNetwork", "remote.opencodeGoDesktop", "remote.opencodeGoRequestLog", "remote.credentials"], (scope) => {
3296
3472
  const forms = scope.get("configForms");
3297
3473
  const pluginManager = {};
3298
3474
  const profile = forms?.get("dshopencodego");