@goodandready/dsh-opencode-provider 0.5.24 → 0.5.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.25 — release 2: stale data
4
+
5
+ - **#87 — stale usage**: a failed usage refresh now returns the last successful report with `stale: true` and `lastSuccessAt` instead of discarding it. The quota sidebar/panel and Settings label the stale snapshot; an explicit refresh no longer loses the previous reading.
6
+ - **#88 — stale analytics**: a failed analytics rescan keeps the previous report and heatmap visible with a stale notice and a retry button instead of blanking the panel; successful rescans clear the stale flag.
7
+ - 127 tests.
8
+
3
9
  ## 0.5.24 — release 1: account safety
4
10
 
5
11
  - **#89 — managed credential cleanup**: keys saved through Settings or imported from `auth.json` are recorded in `~/.dsh/storages/dsh-opencode/managed-credentials.json` (refs only, never secrets). Removing an account asks for confirmation and deletes only plugin-managed credentials after the settings save succeeds; external environment/`auth.json` references are left intact. Cleanup failures are reported separately from the account removal.
package/README.md CHANGED
@@ -132,12 +132,12 @@ npm pack
132
132
  Install the **packed tarball** into a profile:
133
133
 
134
134
  ```bash
135
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.24.tgz
135
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
136
136
  # DSH Desktop:
137
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.24.tgz
137
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
138
138
 
139
139
  # Or install the published package:
140
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.24
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
141
141
  ```
142
142
 
143
143
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
package/README.ru.md CHANGED
@@ -131,12 +131,12 @@ npm pack
131
131
  Установка из **упакованного tarball**:
132
132
 
133
133
  ```bash
134
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.24.tgz
134
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
135
135
  # DSH Desktop:
136
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.24.tgz
136
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
137
137
 
138
138
  # Либо установка опубликованного пакета из npm:
139
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.24
139
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
140
140
  ```
141
141
 
142
142
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
package/README.zh.md CHANGED
@@ -126,12 +126,12 @@ npm pack
126
126
  通过**打包文件 (.tgz)**安装:
127
127
 
128
128
  ```bash
129
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.24.tgz
129
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
130
130
  # DSH 桌面端:
131
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.24.tgz
131
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
132
132
 
133
133
  # 或从 npm 安装已发布版本:
134
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.24
134
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
135
135
  ```
136
136
 
137
137
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
package/lib/client.js CHANGED
@@ -96,6 +96,7 @@ window.__ModuleLoader__.load({
96
96
  runwaySafe: "safe",
97
97
  noUsage: "Quota is unavailable.",
98
98
  usageError: "Quota error",
99
+ usageStale: "Showing the last successful quota reading; the latest refresh failed.",
99
100
  refresh: "Refresh",
100
101
  refreshed: "Refreshed",
101
102
  loading: "Loading…",
@@ -144,6 +145,7 @@ window.__ModuleLoader__.load({
144
145
  heatmapAllAccounts: "All accounts",
145
146
  analyticsHint: "Aggregated from local session metadata by last activity. A session that switched models is counted under its last model; failures are not exposed by the projection. Only plugin routes are counted; legacy opencode sessions belong to the built-in catalog.",
146
147
  analyticsPartial: "This report is partial: the projection scan hit its file limit or skipped unreadable files.",
148
+ analyticsStale: "Showing the previous successful scan; the latest refresh failed.",
147
149
  analyticsEmpty: "No plugin sessions in this window. Legacy opencode sessions belong to the built-in catalog.",
148
150
  showAll: "Show all",
149
151
  modelSearch: "Search models…",
@@ -314,6 +316,7 @@ window.__ModuleLoader__.load({
314
316
  runwaySafe: "安全",
315
317
  noUsage: "配额不可用。",
316
318
  usageError: "配额错误",
319
+ usageStale: "显示上次成功读取的配额;最新刷新失败。",
317
320
  refresh: "刷新",
318
321
  refreshed: "已刷新",
319
322
  loading: "加载中…",
@@ -362,6 +365,7 @@ window.__ModuleLoader__.load({
362
365
  heatmapAllAccounts: "全部账户",
363
366
  analyticsHint: "按最后活动时间聚合本地会话元数据。切换过模型的会话计入最后使用的模型;本地投影不暴露失败。仅统计插件路由;旧 opencode 会话属于内置目录。",
364
367
  analyticsPartial: "此报告不完整:投影扫描达到文件上限或跳过了不可读文件。",
368
+ analyticsStale: "显示上次成功扫描的结果;最新刷新失败。",
365
369
  analyticsEmpty: "此窗口内没有插件会话。旧 opencode 会话属于内置目录。",
366
370
  showAll: "显示全部",
367
371
  modelSearch: "搜索模型…",
@@ -1016,6 +1020,10 @@ window.__ModuleLoader__.load({
1016
1020
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1017
1021
  className: "oc-bars",
1018
1022
  children: [
1023
+ usage.stale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1024
+ className: "oc-hint",
1025
+ children: [t("usageStale"), usage.lastSuccessAt === void 0 ? "" : ` (${new Date(usage.lastSuccessAt).toLocaleTimeString()})`]
1026
+ }) : null,
1019
1027
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
1020
1028
  label: t("fiveHour"),
1021
1029
  window: windows.fiveHour
@@ -1344,6 +1352,7 @@ window.__ModuleLoader__.load({
1344
1352
  const [stateError, setStateError] = (0, react.useState)(null);
1345
1353
  const [analytics, setAnalytics] = (0, react.useState)(null);
1346
1354
  const [analyticsError, setAnalyticsError] = (0, react.useState)(null);
1355
+ const [analyticsStale, setAnalyticsStale] = (0, react.useState)(false);
1347
1356
  const [days, setDays] = (0, react.useState)(7);
1348
1357
  const [busy, setBusy] = (0, react.useState)(false);
1349
1358
  const analyticsSeq = (0, react.useRef)(0);
@@ -1359,14 +1368,16 @@ window.__ModuleLoader__.load({
1359
1368
  const seq = analyticsSeq.current + 1;
1360
1369
  analyticsSeq.current = seq;
1361
1370
  setAnalyticsError(null);
1371
+ setAnalyticsStale(analytics !== null);
1362
1372
  fetchAnalytics(windowDays).then((next) => {
1363
1373
  if (seq !== analyticsSeq.current) return;
1364
1374
  setAnalytics(next);
1365
1375
  setAnalyticsError(null);
1376
+ setAnalyticsStale(false);
1366
1377
  }).catch((caught) => {
1367
1378
  if (seq !== analyticsSeq.current) return;
1368
- setAnalytics(null);
1369
1379
  setAnalyticsError(caught instanceof Error ? caught.message : "analytics failed");
1380
+ setAnalyticsStale(analytics !== null);
1370
1381
  });
1371
1382
  };
1372
1383
  (0, react.useEffect)(() => {
@@ -1583,59 +1594,69 @@ window.__ModuleLoader__.load({
1583
1594
  children: t("intro")
1584
1595
  })]
1585
1596
  }),
1586
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1597
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1587
1598
  className: "oc-card",
1588
- children: analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1599
+ children: [
1589
1600
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1590
1601
  className: "oc-card-title",
1591
1602
  children: t("usageAnalytics")
1592
1603
  }),
1593
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1604
+ analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1594
1605
  className: "oc-hint",
1595
1606
  children: [
1596
1607
  t("failed"),
1597
1608
  ": ",
1598
1609
  analyticsError
1599
1610
  ]
1600
- }),
1601
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1611
+ }) : null,
1612
+ analyticsStale && analytics !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1613
+ className: "oc-hint",
1614
+ children: t("analyticsStale")
1615
+ }) : null,
1616
+ analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1602
1617
  type: "button",
1603
1618
  className: "oc-button",
1604
1619
  onClick: () => loadAnalytics(days),
1605
1620
  children: t("refresh")
1621
+ }) : null,
1622
+ analytics === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1623
+ className: "oc-hint",
1624
+ children: t("loading")
1625
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageAnalyticsView, {
1626
+ analytics,
1627
+ t,
1628
+ days,
1629
+ onDays: setDays
1606
1630
  })
1607
- ] }) : analytics === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1608
- className: "oc-card-title",
1609
- children: t("usageAnalytics")
1610
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1611
- className: "oc-hint",
1612
- children: t("loading")
1613
- })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageAnalyticsView, {
1614
- analytics,
1615
- t,
1616
- days,
1617
- onDays: setDays
1618
- })
1631
+ ]
1619
1632
  }),
1620
1633
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1621
1634
  className: "oc-card",
1622
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1623
- className: "oc-card-title",
1624
- children: t("requestHeatmap")
1625
- }), analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1626
- className: "oc-hint",
1627
- children: [
1628
- t("failed"),
1629
- ": ",
1630
- analyticsError
1631
- ]
1632
- }) : analytics === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1633
- className: "oc-hint",
1634
- children: t("loading")
1635
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequestHeatmapView, {
1636
- heatmap: analytics.heatmap,
1637
- t
1638
- })]
1635
+ children: [
1636
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1637
+ className: "oc-card-title",
1638
+ children: t("requestHeatmap")
1639
+ }),
1640
+ analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1641
+ className: "oc-hint",
1642
+ children: [
1643
+ t("failed"),
1644
+ ": ",
1645
+ analyticsError
1646
+ ]
1647
+ }) : null,
1648
+ analyticsStale && analytics !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1649
+ className: "oc-hint",
1650
+ children: t("analyticsStale")
1651
+ }) : null,
1652
+ analytics === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1653
+ className: "oc-hint",
1654
+ children: t("loading")
1655
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequestHeatmapView, {
1656
+ heatmap: analytics.heatmap,
1657
+ t
1658
+ })
1659
+ ]
1639
1660
  })
1640
1661
  ]
1641
1662
  });
@@ -2334,6 +2355,10 @@ window.__ModuleLoader__.load({
2334
2355
  ]
2335
2356
  });
2336
2357
  })(),
2358
+ defaultAccount?.usage?.stale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2359
+ className: "oc-hint",
2360
+ children: t("usageStale")
2361
+ }) : null,
2337
2362
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2338
2363
  className: "oc-row",
2339
2364
  children: [
@@ -2435,8 +2460,8 @@ window.__ModuleLoader__.load({
2435
2460
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
2436
2461
  className: "oc-card-title",
2437
2462
  children: [t("serviceStatus"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2438
- className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
2439
- children: state.service.usageCheckedAt === void 0 ? t("notChecked") : state.service.usageError === void 0 ? t("serviceOk") : t("serviceDegraded")
2463
+ className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 && state.service.usageStale !== true ? "oc-badge-ok" : "oc-badge-bad"}`,
2464
+ children: state.service.usageCheckedAt === void 0 ? t("notChecked") : state.service.usageError === void 0 && state.service.usageStale !== true ? t("serviceOk") : t("serviceDegraded")
2440
2465
  })]
2441
2466
  }),
2442
2467
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2447,7 +2472,7 @@ window.__ModuleLoader__.load({
2447
2472
  children: t("usageEndpoint")
2448
2473
  }),
2449
2474
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2450
- className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
2475
+ className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 && state.service.usageStale !== true ? "oc-badge-ok" : "oc-badge-bad"}`,
2451
2476
  children: state.service.usageCheckedAt === void 0 ? t("notChecked") : t("checkedAt").replace("{time}", new Date(state.service.usageCheckedAt).toLocaleTimeString())
2452
2477
  }),
2453
2478
  state.service.usageLatencyMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2460,6 +2485,10 @@ window.__ModuleLoader__.load({
2460
2485
  className: "oc-hint",
2461
2486
  children: state.service.usageError
2462
2487
  }),
2488
+ state.service.usageStale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2489
+ className: "oc-hint",
2490
+ children: t("usageStale")
2491
+ }) : null,
2463
2492
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2464
2493
  className: "oc-row",
2465
2494
  children: [
@@ -2641,6 +2670,10 @@ window.__ModuleLoader__.load({
2641
2670
  known.usage.error
2642
2671
  ]
2643
2672
  }),
2673
+ known?.usage?.stale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2674
+ className: "oc-hint",
2675
+ children: t("usageStale")
2676
+ }) : null,
2644
2677
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2645
2678
  className: "oc-row",
2646
2679
  children: [
package/lib/index.d.ts CHANGED
@@ -263,6 +263,9 @@ interface UsageResult {
263
263
  readonly keyStatus?: UsageKeyStatus;
264
264
  readonly latencyMs?: number;
265
265
  readonly checkedAt: number;
266
+ /** True when `report` is the last successful reading after a failed refresh. */
267
+ readonly stale?: boolean;
268
+ readonly lastSuccessAt?: number;
266
269
  }
267
270
  interface UsageWindowPayload {
268
271
  status?: string;
package/lib/index.js CHANGED
@@ -81,7 +81,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
81
81
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
82
82
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
83
83
  const DEFAULT_AUTH_ENTRY = "opencode-go";
84
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.24";
84
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.25";
85
85
  const DEFAULT_CONTEXT_WINDOW = 262144;
86
86
  const DEFAULT_MAX_TOKENS = 32768;
87
87
  const DEFAULT_GENERATE_MAX_TOKENS = 32768;
@@ -5033,7 +5033,8 @@ async function buildState(ctx, deps) {
5033
5033
  service: {
5034
5034
  ...usage?.checkedAt === void 0 ? {} : { usageCheckedAt: usage.checkedAt },
5035
5035
  ...usage?.latencyMs === void 0 ? {} : { usageLatencyMs: usage.latencyMs },
5036
- ...usage?.error === void 0 ? {} : { usageError: usage.error }
5036
+ ...usage?.error === void 0 ? {} : { usageError: usage.error },
5037
+ ...usage?.stale === true ? { usageStale: true } : {}
5037
5038
  },
5038
5039
  routing: deps.adapter.routingTrace(),
5039
5040
  auth: {
@@ -5618,6 +5619,8 @@ function fingerprint(key) {
5618
5619
  /** Cached reader for the (undocumented) Go usage endpoint. Fail-open by design. */
5619
5620
  function createUsageReader(config, resolveKey, fetchImpl = fetch) {
5620
5621
  const cache = /* @__PURE__ */ new Map();
5622
+ /** Last successful reading per key, kept across explicit cache clears. */
5623
+ const lastSuccess = /* @__PURE__ */ new Map();
5621
5624
  const read = async (provider, explicitKey) => {
5622
5625
  if (provider !== "opencode-go-direct" && provider !== "opencode-go") return void 0;
5623
5626
  const current = config();
@@ -5635,7 +5638,7 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
5635
5638
  const cacheKey = `${provider}:${fingerprint(key)}`;
5636
5639
  const cached = cache.get(cacheKey);
5637
5640
  if (cached !== void 0) {
5638
- const ttl = cached.report === void 0 ? FAILURE_TTL_MS : SUCCESS_TTL_MS;
5641
+ const ttl = cached.report === void 0 || cached.stale === true ? FAILURE_TTL_MS : SUCCESS_TTL_MS;
5639
5642
  if (Date.now() - cached.checkedAt < ttl) return cached;
5640
5643
  }
5641
5644
  const result = async () => {
@@ -5693,8 +5696,25 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
5693
5696
  }
5694
5697
  };
5695
5698
  const fresh = await result();
5696
- cache.set(cacheKey, fresh);
5697
- return fresh;
5699
+ const previous = cached?.report !== void 0 ? cached : lastSuccess.get(cacheKey);
5700
+ if (fresh.report === void 0 && previous?.report !== void 0) {
5701
+ const stale = {
5702
+ ...fresh,
5703
+ report: previous.report,
5704
+ stale: true,
5705
+ lastSuccessAt: previous.lastSuccessAt ?? previous.checkedAt
5706
+ };
5707
+ cache.set(cacheKey, stale);
5708
+ return stale;
5709
+ }
5710
+ const next = fresh.report === void 0 ? fresh : {
5711
+ ...fresh,
5712
+ stale: false,
5713
+ lastSuccessAt: fresh.checkedAt
5714
+ };
5715
+ if (next.report !== void 0) lastSuccess.set(cacheKey, next);
5716
+ cache.set(cacheKey, next);
5717
+ return next;
5698
5718
  };
5699
5719
  read.clear = () => {
5700
5720
  cache.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",