dsh-global-todo 0.12.9 → 0.12.11

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,3 +1,27 @@
1
+ ## [0.12.11] — 2026-10-21
2
+
3
+ One more silent fallback, found while re-reading the settings path.
4
+
5
+ - **`{idleMinutes: 30.5}` was accepted and then ignored.** `normalizeSettings` only keeps
6
+ integers for the minute fields and otherwise falls back to the default, so the request looked
7
+ like it worked while the value never changed. The settings route now rejects a fractional
8
+ minute with `400 invalid-body`, like every other out-of-range value.
9
+
10
+ ## [0.12.10] — 2026-10-21
11
+
12
+ Two follow-ups from #3, plus the notification channel this repository actually has.
13
+
14
+ - **`/health` names what waits for review.** `review.count` said something was waiting;
15
+ `review.items` says which (id, seq, title, doneAt, newest first) — checking the state no longer
16
+ means opening the panel and clicking through the list.
17
+ - **The panel says when the running version is older than the installed one.** `stale` was added to
18
+ `/health` in 0.12.9; the panel now shows a one-time notice naming both versions and saying to
19
+ restart `dsh web`. This is the trap where the client is new, the module graph is old, and the
20
+ panel calls routes that 404.
21
+ - **`tools/notify.mjs`** — pushes release / CI-failed / new-issue messages to WeChat through the
22
+ host's delivery library, which owns the freshness window and the outbox. `--dry-run` composes and
23
+ reports the window without sending; a closed window queues instead of failing.
24
+
1
25
  ## [0.12.9] — 2026-10-21
2
26
 
3
27
  Everything a Windows tester found in a 41-case sweep, answered (#3).
package/README.md CHANGED
@@ -381,7 +381,7 @@ fence when that service is present, and by a strict loopback check otherwise.
381
381
  | POST | `/api/global-todo/capture` | `{ sessionId? }` — ask the current conversation to collect its follow-ups |
382
382
  | POST | `/api/global-todo/bulk` | `{ ids, status?, addTags?, removeTags? }` — one change across a selection; a stale id lands in `unmatched` instead of failing the batch |
383
383
  | POST | `/api/global-todo/ingest` | `{ items: [{ title, source: { kind }, confidence }], dryRun? }` with `Authorization: Bearer <settings.ingestToken>` — external items; below `ingestConfirmBelow` they wait in the inbox |
384
- | GET | `/api/global-todo/health` | — (store location, counts, dispatch capabilities, plus **tool registration** and **whether the completion watcher attached** — both fail quietly, so both are reported — and `metrics`: dispatch success rate, agent tool calls, list growth; `autoCollect`: why each waiting conversation was or was not asked, counters, and how the settings file loaded; `version` is the running module and `packageVersion`/`stale` say whether the installed package is newer than the running one) |
384
+ | GET | `/api/global-todo/health` | — (store location, counts, dispatch capabilities, plus **tool registration** and **whether the completion watcher attached** — both fail quietly, so both are reported — and `metrics`: dispatch success rate, agent tool calls, list growth; `autoCollect`: why each waiting conversation was or was not asked, counters, and how the settings file loaded; `version` is the running module and `packageVersion`/`stale` say whether the installed package is newer than the running one; `review.items` names the finished todos that still wait for a person, so a check does not have to drive the panel) |
385
385
  | POST | `/api/global-todo/create` | `{ title, description?, links?, steps?, tags?, priority?, dueAt? }` |
386
386
  | POST | `/api/global-todo/update` | `{ id, ...fields, by?: human\|agent }` |
387
387
  | POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
@@ -15,10 +15,10 @@ of trusting this table.
15
15
 
16
16
  | Registry | Form | What it gives you | Verified state |
17
17
  |---|---|---|---|
18
- | **npm** | the package | the install path | `dsh-global-todo@0.11.2` |
18
+ | **npm** | the package | the install path | `dsh-global-todo@0.12.10` |
19
19
  | **1024 Store** ([deepseek1024.com](https://deepseek1024.com)) · [repo](https://github.com/imsai-sh/awesome-deepseek-harness-plugins) | catalog entry, merged [#654](https://github.com/imsai-sh/awesome-deepseek-harness-plugins/pull/654) | searchable, has a public API | 🟡 entry live, but its install line still resolves to `github:LUgABeEEE/dsh-global-todo` — their npm enrichment has not caught up. Install works either way. |
20
20
  | **dsh.pub** · [page](https://dsh.pub/en/plugins/dsh-global-todo/) | published page + `dshpub` CLI | `npx dshpub add LUgABeEEE/dsh-global-todo` | ✅ page live, pinned to a commit |
21
- | **DSH-Plugins-Marketplace** ([bradeGithub](https://github.com/bradeGithub/DSH-Plugins-Marketplace)) | `registry.json`, collected from the `dsh-plugin` topic | one-click install inside DSH | ✅ `v0.11.2`, risk tier `safe` |
21
+ | **DSH-Plugins-Marketplace** ([bradeGithub](https://github.com/bradeGithub/DSH-Plugins-Marketplace)) | `registry.json`, collected from the `dsh-plugin` topic | one-click install inside DSH | ✅ `v0.12.x`, risk tier `safe` |
22
22
  | **awesome-dsh-plugin** ([19.7k★](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)) | one YAML per plugin | the largest directory | ✅ merged [#7498](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/7498) |
23
23
  | **walkinglabs/awesome-deepseek-harness-plugins** | README entry (source-verified list) | review-backed listing | ✅ merged [#114](https://github.com/walkinglabs/awesome-deepseek-harness-plugins/pull/114) |
24
24
 
package/lib/client.js CHANGED
@@ -175,6 +175,9 @@ window.__ModuleLoader__.load({
175
175
  'notice.autoCollectOn': '已开启自动获取:一次人为发言最多自动问一次',
176
176
  'notice.autoCollectOff': '已关闭自动获取',
177
177
  'notice.externalChange': '清单在别处被改过,已载入最新内容',
178
+ // Installed newer than running: the panel would otherwise call routes the old module
179
+ // graph does not have, and look broken until someone happens to restart the Host.
180
+ 'notice.stale': '已安装 {installed},当前运行的是 {running};重启 dsh web 后生效',
178
181
  'reminder.title': '到点提醒({n})',
179
182
  'reminder.note': '这些是到了设定的提醒时间,和「逾期」不是同一批。',
180
183
  'reminder.dismissAll': '全部知道了',
@@ -472,6 +475,7 @@ window.__ModuleLoader__.load({
472
475
  'notice.autoCollectOn': 'Auto-collect on: at most one question per message from you',
473
476
  'notice.autoCollectOff': 'Auto-collect off',
474
477
  'notice.externalChange': 'The list changed somewhere else; the latest content is loaded',
478
+ 'notice.stale': 'Installed {installed}, running {running}; restart dsh web to finish the upgrade',
475
479
  'reminder.title': 'Due now ({n})',
476
480
  'reminder.note': 'These reached their reminder time — a different set from Overdue.',
477
481
  'reminder.dismissAll': 'Dismiss all',
@@ -2715,6 +2719,9 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2715
2719
  // The store counts file changes made outside this process (another machine, a sync
2716
2720
  // client). A new count means the panel's view was stale a moment ago; say so once.
2717
2721
  const externalRef = useRef(null)
2722
+ // Warn once per mount: a version mismatch is a fact about this page load, not an
2723
+ // event worth repeating on every refresh.
2724
+ const staleRef = useRef(false)
2718
2725
  // The wide-screen detail card floats over the panel, so its offsets are measured from
2719
2726
  // the panel's own column rather than derived from the viewport width.
2720
2727
  const innerRef = useRef(null)
@@ -2782,6 +2789,17 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2782
2789
  setTimeout(() => setNotice(null), 8000)
2783
2790
  }
2784
2791
  externalRef.current = external
2792
+ if (data?.stale === true && !staleRef.current) {
2793
+ staleRef.current = true
2794
+ setNotice({
2795
+ text: translate(lang, 'notice.stale', {
2796
+ installed: String(data.packageVersion ?? '?'),
2797
+ running: String(data.version ?? '?'),
2798
+ }),
2799
+ sessionId: '',
2800
+ })
2801
+ setTimeout(() => setNotice(null), 12000)
2802
+ }
2785
2803
  setState((current) => ({ ...current, health: data }))
2786
2804
  } catch {
2787
2805
  /* the health mirror is not worth an error banner */
package/lib/index.js CHANGED
@@ -37,7 +37,7 @@ import { createStore, resolveStorePath } from './store.js'
37
37
  import { createTools } from './tools.js'
38
38
 
39
39
  /** Must equal `package.json`'s version; `npm run check` enforces that. */
40
- export const VERSION = '0.12.9'
40
+ export const VERSION = '0.12.11'
41
41
 
42
42
  /** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
43
43
  export const name = 'dsh-global-todo'
package/lib/routes.js CHANGED
@@ -426,6 +426,9 @@ export function createRoutes(deps) {
426
426
  stale: deps.diskVersion !== undefined && deps.diskVersion !== version,
427
427
  store: await service.describe(),
428
428
  counts,
429
+ // The count says something waits for a person; the items say what, so a check does not
430
+ // have to drive the UI to find out.
431
+ review: { count: counts.review, items: await service.awaitingReview() },
429
432
  // Purely additive, in-memory counters: hand-off success rate, agent tool
430
433
  // calls and how the list grew. `null` when they were never mounted — an
431
434
  // absent metric is not a zero.
@@ -739,17 +742,22 @@ export function createRoutes(deps) {
739
742
  */
740
743
  function assertSettingsRange(body) {
741
744
  const ranges = {
742
- idleMinutes: [5, 24 * 60],
743
- cooldownMinutes: [5, 24 * 60],
744
- ingestConfirmBelow: [0, 1],
745
- reminderLeadMinutes: [0, 24 * 60],
745
+ idleMinutes: [5, 24 * 60, true],
746
+ cooldownMinutes: [5, 24 * 60, true],
747
+ ingestConfirmBelow: [0, 1, false],
748
+ reminderLeadMinutes: [0, 24 * 60, true],
746
749
  }
747
- for (const [field, [low, high]] of Object.entries(ranges)) {
750
+ for (const [field, [low, high, whole]] of Object.entries(ranges)) {
748
751
  const value = body[field]
749
752
  if (value === undefined || value === null) continue
750
753
  if (typeof value !== 'number' || !Number.isFinite(value)) {
751
754
  throw new TodoError('invalid-body', `${field} 必须是数字(${low}–${high})`)
752
755
  }
756
+ // normalizeSettings only accepts integers for the minute fields and otherwise falls back
757
+ // to the default — a silently ignored 30.5 is the same trap as a silent clamp.
758
+ if (whole === true && !Number.isInteger(value)) {
759
+ throw new TodoError('invalid-body', `${field} 必须是整数分钟,收到 ${value}`)
760
+ }
753
761
  if (value < low || value > high) {
754
762
  throw new TodoError('invalid-body', `${field} 必须在 ${low}–${high} 之间,收到 ${value}`)
755
763
  }
package/lib/service.js CHANGED
@@ -821,10 +821,29 @@ export function createService(options) {
821
821
  return counts(todos, now())
822
822
  }
823
823
 
824
+ /**
825
+ * Which finished todos an agent marked done that a person has not confirmed.
826
+ *
827
+ * The count in `/health` says something is waiting; this says what, so a check does not
828
+ * have to open the panel and click through the list to find out.
829
+ *
830
+ * @param {number} [limit] newest first, capped
831
+ * @returns {Promise<{ id: string, seq: number, title: string, doneAt: number }[]>}
832
+ */
833
+ async function awaitingReview(limit = 20) {
834
+ const todos = await store.all()
835
+ return todos
836
+ .filter((todo) => todo.status === 'done' && todo.review?.pending === true)
837
+ .sort((left, right) => (right.doneAt ?? right.updatedAt ?? 0) - (left.doneAt ?? left.updatedAt ?? 0))
838
+ .slice(0, limit)
839
+ .map((todo) => ({ id: todo.id, seq: todo.seq, title: todo.title, doneAt: todo.doneAt ?? todo.updatedAt ?? 0 }))
840
+ }
841
+
824
842
  return {
825
843
  list,
826
844
  listAll,
827
845
  get,
846
+ awaitingReview,
828
847
  create,
829
848
  update,
830
849
  setStatus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-global-todo",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "icon": "./docs/icon.svg",
5
5
  "description": "Cross-session todo list for DeepSeek Harness: one JSON file a person and an agent both write to, with step plans that ship as a numbered hand-off, session read-back, and a human confirmation before an agent's \"done\" counts.",
6
6
  "type": "module",
@@ -73,6 +73,7 @@
73
73
  "check": "node scripts/check.mjs",
74
74
  "doctor": "node tools/doctor.mjs",
75
75
  "check:windows": "node tools/windows-check.mjs",
76
+ "notify": "node tools/notify.mjs",
76
77
  "test": "node --test \"test/**/*.test.mjs\"",
77
78
  "verify": "npm run check && npm run check:render && npm test",
78
79
  "preview": "node tools/preview.mjs",