dsh-global-todo 0.0.0-stage → 0.3.1

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 ADDED
@@ -0,0 +1,484 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.1] — 2026-10-17
11
+
12
+ A compatibility release rather than a feature one: DeepSeek Harness moved to `0.2.1-rc.2`, and
13
+ the plugin needed no code change to follow it — but three of its own notes described the old
14
+ Host, and a note that is out of date is what the next change gets planned around.
15
+
16
+ ### Changed
17
+
18
+ - **Verified against DSH `0.2.1-rc.2`, and the picker notes now say what is true.** Nothing in
19
+ the plugin had to change: the tool schemas still pass the Host's own validator, every service
20
+ it injects is still there, and the client half touches only `slots` / `locale` / `effect` /
21
+ `uiWorkspace`, whose contract did not move. What moved is the Host — 0.2.1 mounts its own
22
+ directory picker, so the panel's `选择` is now the shell's native dialog and the built-in
23
+ browser is a fallback rather than the normal case. Three comments and one notice said
24
+ otherwise and have been corrected: a note that still claims a capability is missing is worse
25
+ than no note, because the next reader plans around it.
26
+
27
+ ## [0.3.0] — 2026-10-17
28
+
29
+ The round after 0.2.0: `/health` grew counters and a JSON export/import round trip, an agent
30
+ gained an honest way to reopen a todo it had finished by mistake, and automatic collect learned
31
+ both when to shut up — one question per message a person wrote — and where never to speak at all,
32
+ a workspace somebody exempted. The quiet auto-close from the middle of that round was removed
33
+ again the same night it shipped; the reason is written down in `DESIGN.md`.
34
+
35
+ ### Added
36
+
37
+ - **`/health` now reports what the plugin has been doing.** In-process counters for the list
38
+ (`created`, `removed`, `restored`, `imports`, `noted`, `changes`, `size`, `peak`) and for
39
+ hand-offs (`attempts`, `ok`, `failed`, `byMode`, `successRate`), plus a per-tool call count that
40
+ includes failures. The existing fields are untouched; `metrics` is `null` when the plugin is
41
+ mounted without a store. Deliberately **in-memory**: it describes this process, and a counter
42
+ that survives a restart is a second source of truth for facts the store already owns.
43
+ - **The list can be exported and imported as JSON.** `GET /export` returns the store's own
44
+ document — not the read view, so the runtime annotations (`missing`, `sessionDir`) are absent —
45
+ and `POST /import` accepts it back with `replace` or `merge` semantics (`merge` overwrites by id
46
+ and appends new ones). A single unreadable record fails the whole request with a 400 rather than
47
+ writing half of it. The panel gained 导出 / 导入 buttons; the import path defaults to `merge` so
48
+ choosing the wrong file cannot empty the list.
49
+ - The tool count in the docs now matches the tree: **ten** `global_todo_*` tools
50
+ (`add`, `capture`, `list`, `get`, `update`, `link`, `note`, `done`, `reopen`, `remove`), the
51
+ same list `/health.tools.registered` reports. `ROADMAP.md` was re-audited against the code and
52
+ now separates what has landed (with `file:line` citations) from what has not.
53
+ - **`global_todo_reopen` gives a wrongly-finished todo an honest way back.** It does exactly
54
+ one thing — `done` / `cancelled` → `open` — records the act as the agent's (with an optional
55
+ `reason` in the activity trail) and **refuses** an item that was never finished, instead of
56
+ reporting a repair that did not happen. Until now the only agent-side path was `status` on
57
+ `global_todo_done`, i.e. a capability filed under the tool whose name says the opposite, so a
58
+ data repair reached for the HTTP route instead — and was filed as the person's work.
59
+ - **A workspace can be exempted from automatic collect.** The `Exempt` action on a conversation's
60
+ group header takes that conversation's whole **working directory** out of scope, and
61
+ `/health.settings` reports the list so the panel can show which places are out. It is keyed on
62
+ the directory rather than on a session id because the conversations that need it are the ones a
63
+ machine drives — a chat companion's room — and such a session is recreated with a new id when it
64
+ restarts, so an id-keyed exemption would quietly stop applying. Subdirectories count; a shared
65
+ prefix does not, and a conversation whose directory nobody reported is not treated as exempt.
66
+ The directory is remembered from the conversation's own events rather than looked up when the
67
+ timer runs, because these conversations are idle most of the time and an idle conversation is
68
+ one the Host may have unloaded — where a live lookup answers `''`, and the exemption would stop
69
+ applying to exactly the conversations it exists for.
70
+
71
+ ### Fixed
72
+
73
+ - **The panel's `/update` and `/status` routes no longer need a lie to accept a write.** Both
74
+ hardcoded `by: 'human'`, so a script repairing data on an agent's behalf filed the change as
75
+ the person's own — the four reopens from the mis-close repair are recorded that way, and the
76
+ only thing saying otherwise was a sentence in a note. Both routes now take an explicit
77
+ `by: 'agent'` and still default to `human`; any other value is a `400`, because quietly
78
+ folding a typo into `human` is the same lie with better manners.
79
+
80
+ - **Automatic collect no longer asks the same conversation over and over.** The question the plugin
81
+ sends *is* a message in the conversation, so it restarted the plugin's own quiet clock, and the
82
+ answer to it ended a clean turn — which put the same conversation straight back in the queue. With
83
+ `cooldownMinutes` on the same half hour as `idleMinutes`, the cooldown had always expired by the
84
+ time the quiet window opened, so one conversation was asked every half hour, all night, with
85
+ nobody there. A conversation is now asked **once per message a person wrote in it**: only
86
+ `source.kind === 'user'` re-arms it, and the plugin's own traffic never does. A restarted Host has
87
+ forgotten the timestamps and so stays quiet, which is the direction to fail in.
88
+
89
+ - **A quiet conversation no longer has its open todos closed for it.** Yesterday's helper read
90
+ "this conversation has gone quiet" as "its work is finished" and closed every open todo that
91
+ conversation had written, under a template summary. On its first night it closed eight that were
92
+ not finished — one waiting on a person, one waiting on an event that had not happened yet — and
93
+ the only thing that told them apart was that summary, because there was no evidence behind it.
94
+ The signal is not weak, it points the wrong way: a conversation falls quiet *because* it is
95
+ waiting, so the items that matter most are the ones it gets wrong. Only paths that carry evidence
96
+ close a todo now — the agent's own `global_todo_done`, the hand-off watcher, or a person in the
97
+ panel — and the module that held the heuristic is gone.
98
+
99
+ ## [0.2.0] — 2026-10-15
100
+
101
+ The first published release. 0.1.0 was cut in the repository but never reached a
102
+ registry, so this is the version to install.
103
+
104
+ ### Fixed
105
+
106
+ - **A settings write that landed during startup was silently undone.** The Host reads
107
+ `settings.json` without awaiting the read — it cannot block its own mount on a file — and that
108
+ read replaces the whole settings state. A panel toggle, or a cooldown booking from an
109
+ automatic ask, that arrived while the read was still in flight was therefore overwritten by
110
+ the file that read had just loaded. Writes now wait for the startup read to finish.
111
+ - **The diagnostics log is trimmed without ever being seen empty.** The bound was enforced by
112
+ rewriting `client-errors.log` in place, and `writeFile` truncates its target before it writes,
113
+ so a reader that landed in that window got 0 bytes — the CI reader that expected the newest
114
+ report got `''`. The trim now writes a sibling file and renames it over the log, which is
115
+ atomic, and a failed trim leaves no litter beside the log.
116
+
117
+ ### Fixed
118
+
119
+ - **The hand-off picker now mirrors the shell's own visibility rule.** It was offering
120
+ conversations the sidebar deliberately hides: **subagent children** (`origin === 'subagent'`),
121
+ **blank sessions** that are not the one in front of the user, and archived sessions whose
122
+ archived set came from a source the picker read too narrowly. The rule is copied from
123
+ `sessionVisible` in the shipped workspace package — the only authority on it — including its one
124
+ exception: a blank session *is* offered while it is the current one. The picker also reports the
125
+ shape of the session snapshot it sees to `/health.clientErrors` once per mount, because a preview
126
+ fixture written by the same hand as the filter can only ever confirm itself.
127
+
128
+ - **A long working directory no longer breaks its own label.** `工作目录:` is a flex child of the
129
+ directory pill, so when the row overflowed it shrank and wrapped mid-word — and the third
130
+ button (`浏览`) was enough to push it over. Only the path yields now: it already ellipsises,
131
+ while the label, the hint and the buttons keep their size.
132
+
133
+ ### Added
134
+
135
+ - **A folder browser, because the shell's picker is not always mounted.** `uiWorkspace.pickDirectory()`
136
+ delegates to a directory-picking Remote namespace that this deployment does not have, so it
137
+ throws instead of opening anything (the panel had been reduced to asking you to type a path).
138
+ The Host now serves `GET /api/global-todo/dirs` — visible sub-folders, sorted, capped, with the
139
+ parent — and `选择` walks it as a real picker (`浏览`: current path, `上一级`, folder buttons,
140
+ `用这个目录`). The path field stays for pasting, and the failure is still reported to
141
+ `/health.clientErrors`.
142
+
143
+ ### Changed
144
+
145
+ - **A quiet conversation has its finished work ticked off for it.** Asking an agent to close what
146
+ it finished works most of the time, which is not the same as a tidy list — so when a conversation
147
+ that *wrote* todos has been quiet for the idle window after a clean turn, the Host closes the ones
148
+ it owns, with a summary saying why ("插件按「做完就划掉」结束了它写下的这条"), and it is reversible.
149
+ It touches only that conversation's own todos, and it is deliberately independent of the
150
+ automatic-collect switch: tidying is not the same feature as asking for more.
151
+ - **`/health` reports `idleMinutes`** (and the settings write echoes it). The rule in force has to
152
+ be observable — a check that could not see it reported "not deployed" for code that was running.
153
+
154
+
155
+ - **Automatic collect now waits for quiet, instead of asking at every turn end.** That was the
156
+ design mistake behind "too frequent": a session being worked in ends turns every few minutes, so
157
+ the question arrived mid-work, repeatedly. A clean turn now only *marks* the conversation as a
158
+ candidate and records **when it went quiet**; a one-minute timer asks when nothing has been said
159
+ for `idleMinutes` (default **30**) — and any new message, or the start of a new turn, restarts
160
+ that clock. Asking is also still gated on the conversation being the one the person is in, the
161
+ turn having ended cleanly, and the per-session cooldown. Not knowing how long it has been quiet
162
+ counts as "not yet" (fail closed).
163
+ - **Captured items are refined and merged rather than transcribed.** The capture prompt now asks
164
+ for one entry per matter, merges scattered points into a single entry's description, targets
165
+ "1–3 entries, not 8", and refuses to record what the list already has. The capture tool enforces
166
+ the last part: an item whose normalised title matches an unfinished entry is skipped with
167
+ 「清单里已有同名的未完成条目」 instead of being registered again — which is how the list kept
168
+ filling with near-duplicates.
169
+
170
+
171
+ - **A calmer panel.** The header had grown two rows of controls that said the same things
172
+ twice: the three count chips *were* the status filter, and the add form sat open above the
173
+ list forever. Now there are three rows with one job each — **title + summary + `添加`**,
174
+ **filters · grouping · search** in a single row, then a quiet **context line** (working
175
+ directory, hand-off target, `选择`, `自动获取`). Counts moved into the summary line as plain
176
+ text, every "pick one of these" control uses one segmented style (chips are now only facts
177
+ about a todo: tags, levels, notes), and the add form opens from `添加` instead of occupying
178
+ the top of the list.
179
+
180
+ - **The test suite waits for results, not for the clock.** The remaining fixed sleeps
181
+ (`settle()`, `setTimeout`) are gone: the auto-collect, restart and dispatch tests now poll for
182
+ the state their assertions are about, with a bounded timeout that names what it waited for. A
183
+ sleep is a test that reports on the machine, and one of them was already losing that race on
184
+ the slower CI runner.
185
+ - **CI runs `actions/checkout@v7` and `actions/setup-node@v7`**, which are already on Node 24,
186
+ so the Node 20 deprecation warnings are gone. Automatic npm caching is off explicitly: this
187
+ plugin has no dependencies and no lockfile to cache.
188
+
189
+ - **The repository is publishable as it stands.** Two things stood between this tree and a
190
+ public one. The machine-path guard in `scripts/check.mjs` only looked at the shipped halves,
191
+ so a preview fixture, a test constant and a screenshot caption each carried the author's real
192
+ home directory while the guard still reported `OK`; it now covers every committable file and
193
+ allows exactly one placeholder, `/home/me/…`. And `publish.yml` asked Node 22.19 for npm
194
+ 10.9.3, which cannot do OIDC at all — trusted publishing needs npm 11.5.1 or later — so the
195
+ job installs that floor before it publishes. The internal research reports left `docs/`, and
196
+ the fixtures and screenshots were regenerated from the same neutral data.
197
+
198
+ ## [0.1.0] — 2026-10-14
199
+
200
+ The first release: one JSON list a person and an agent both write to, eight agent tools, a
201
+ panel that hands work over and records how it finished.
202
+
203
+ ### Fixed
204
+
205
+ - **「选择」工作目录不再以一条无解的错误结束.** `uiWorkspace.pickDirectory()` exists, but it
206
+ delegates to a directory-picking **Remote namespace** that is not mounted in every
207
+ deployment, where calling it throws a bare
208
+ `Cannot read properties of undefined (reading 'directoryPicker')`. The panel now falls back
209
+ to a **path field** (`手动填` → paste an absolute path → `使用`), says plainly why, and reports
210
+ the throw to the Host diagnostics so it is visible in `/health.clientErrors` instead of only
211
+ in someone's browser.
212
+ - **Both capture actions and the automatic-collect switch reported an error even though they
213
+ worked.** The handlers ended with `void refresh()` — a function that does not exist — so a
214
+ successful request was immediately followed by a `ReferenceError` that the handler's own catch
215
+ turned into an error banner. The write had already landed, which is also why the switch appeared
216
+ impossible to turn off: the panel's copy of the Host settings was only read at mount, so the chip
217
+ never moved and the next click sent the same value again. Health is now refreshed on the poll and
218
+ updated optimistically on write, and `void refresh()` is gone.
219
+ - **A guard against that class of bug**: `tools/scan-client-calls.mjs` scans the shipped client for
220
+ calls to functions nobody defined (comments and strings removed first; destructuring, parameters
221
+ and method shorthand recognised), wired into `npm run check` and covered by its own test. Every
222
+ other check passes such a mistake, and the preview harness cannot see it — an error caught inside
223
+ a handler only appears on a real re-render.
224
+ - **The toolbar's own "collect from this chat" button is gone.** The action lives on each
225
+ conversation's group header, where its subject is unambiguous; a second copy in the toolbar only
226
+ gave the wrong one to click.
227
+ - **The Host's own tooling is no longer locked out by the connection fence.** Deferring to the
228
+ fence fixed LAN access but made *every* credential-less request a 401, because the fence
229
+ authenticates browsers — so `curl`, scripts on the Host and other agents lost access to a list
230
+ they are the main writers of. The fence now judges **browsers** (anything carrying a cookie or
231
+ an `Origin`), while a local, credential-less caller is judged by the loopback rule — the guard
232
+ that existed before the fence did. A fence that throws still fails closed, and a cross-site
233
+ request is never rescued by the carve-out. `/health` reports `fence.localScriptsAdmitted`.
234
+
235
+ - **The panel works from the LAN and from a tunnel again.** The Host's connection fence was read
236
+ once at mount time, when it can still be absent, so every request fell through to the plugin's
237
+ own loopback check. That check is stricter than the Host's trusted-host list: a browser anywhere
238
+ but the Host machine got `403 只接受本机请求` on **every** call — list, picker and both capture
239
+ actions — while `curl` from the Host looked perfectly healthy. The fence is now resolved per
240
+ request, its verdict is honoured verbatim, and `/health` reports which fence is actually in
241
+ force so a silent downgrade cannot hide again.
242
+
243
+ ### Added
244
+
245
+ - **`全选` in selection mode.** Tick everything the panel is currently showing (collapsed groups
246
+ included — their todos are still what the user means), and the same control becomes
247
+ `取消全选` once everything is selected.
248
+
249
+ - **Batch assignment (「一键分配」)**. Turn on 「选择」 in the toolbar, tick a few todos, and one
250
+ click sends each of them to the best target available for it — **the conversation that wrote
251
+ it**, then a conversation in the **same workspace** (a candidate the panel works out from its
252
+ own recency and archive data, which the Host cannot see), then a **target you choose** for the
253
+ rest. Those are an attempt order, not a preference: a target that cannot be loaded is the
254
+ reason the next one exists. Whatever still has nowhere to go is reported back and offered a
255
+ picker instead of being sent somewhere nobody chose, and one failure does not abort the batch.
256
+ - **An automatic collect switch** (panel chip `自动获取`, `POST /api/global-todo/settings`). When
257
+ it is on, a conversation you are in that finishes a clean turn and has not been asked for a
258
+ while is asked once whether anything needs following up. It is off until a person turns it on,
259
+ the switch lives on the Host (so it works with the panel closed and is reported by `/health`),
260
+ and every reason *not* to ask is explicit: an aborted/failed turn, a conversation nobody is in,
261
+ a session asked inside the cooldown, or the feature being off. The cooldown is booked before
262
+ the question is asked, so a repeated event or a failed request cannot turn into a loop.
263
+ - **Stronger proactive guidance in the system prompt.** The section now lists the concrete
264
+ triggers for recording something on one's own initiative (a cross-conversation follow-up, "I'll
265
+ do X then Y", a leftover step at the end of a turn), says plainly that asking the user whether
266
+ to record it is wrong, and points at `global_todo_note` for reporting a blocker instead of
267
+ rewriting the description.
268
+
269
+ ### Changed
270
+
271
+ - **"Collect from this chat" lives on the conversation it collects from.** A global toolbar
272
+ button could not say *which* conversation it meant, so the action now sits on each
273
+ conversation's group header in the list (`抓取`), and asking a group to collect files the
274
+ results under that conversation. The toolbar keeps a fallback, shown *only* when the
275
+ conversation in front of the user has no group yet — otherwise there would be no way to
276
+ collect from it.
277
+ - **Collecting from a conversation that is not running now resumes it** (the same rule a
278
+ hand-off follows). It used to look the session up in the live registry and die on
279
+ `undefined` for any conversation that was not currently loaded.
280
+
281
+ ### Added
282
+
283
+ - **The hand-off picker shows the conversations you actually use, newest first.** Its order is
284
+ the sidebar's own recency key (`updatedAt`), so the conversation you were last in is at the
285
+ top and stale ones sink, and **archived conversations are left out entirely** — an archived
286
+ conversation is not an answer to "where should this work go". Conversations that share a
287
+ title get a short id suffix so two options can be told apart, and if the current target is no
288
+ longer listed (archived since it was chosen) it is shown as `(已归档)` rather than silently
289
+ displaying a different conversation as selected.
290
+ - **Progress notes** (`global_todo_note`, `POST /api/global-todo/note`): an append-only trail of
291
+ who said what and when, capped per todo. An agent reporting "here is where I am stuck" no
292
+ longer has to rewrite the user's description, and — like the other verdict tools — writing a
293
+ note makes automatic completion stand down rather than contradict a stated blocker. The row
294
+ shows the count, the detail shows the trail and a box to add one.
295
+ - **Dead attachments are flagged where you see them.** File attachments that no longer exist
296
+ are marked red on the row and labelled in the detail. The flag is computed on read and never
297
+ stored, so a file that comes back stops being flagged and a stale flag cannot outlive the
298
+ problem.
299
+ - **Search syntax**: `tag:发布` / `#运维`, `is:overdue|active|due|open|doing|done`, `has:link`,
300
+ `has:note`, `level:p0|p1|p2|p3`, plus free words, which are combined with AND and also match
301
+ note text. A plain search still behaves exactly as before.
302
+ - **A new conversation is placed where the work lives.** A hand-off that opens a session now
303
+ joins the workspace owning the conversation that wrote the todo (or the one it was handed
304
+ from), and starts in that workspace's directory — so the agent and the sidebar agree
305
+ instead of the session landing beside unrelated ones. The panel also lists the workspaces,
306
+ so a new conversation can be aimed at one explicitly (`+ new conversation · <workspace>`);
307
+ a named workspace outranks the inherited one. Only when neither is known does the working
308
+ directory decide.
309
+ - **A pre-commit credential audit inside `npm run check`**: the rule the project used to
310
+ apply only to shipped files now runs over every file a commit would carry (51 of them,
311
+ with the same exclusions `.gitignore` applies), and it asserts that the local artifacts
312
+ (`node_modules/`, `preview/`, `todos.json`, backups) cannot reach a commit at all. A
313
+ credential leaked in a test or a doc is exactly as leaked as one in `lib/`.
314
+ - **`npm run check:live`** (`tools/live-check.mjs`): verifies the wired Host rather than
315
+ doubles — health surfaces, a delete → undo round trip compared field by field, and the
316
+ `sessionAlive`/`sessionDir` a row needs to show whether its conversation is running. It
317
+ cleans up its temporary record even when it fails, and stays out of `verify` because it
318
+ needs a reachable Host.
319
+ - **Levels P0–P3.** `urgent` joins the existing three levels (`P0 紧急 / P1 高 / P2 常规 /
320
+ P3 低`), so no migration is needed: `high`/`normal`/`low` stay valid and `normal` stays the
321
+ default. The badge marks P0 in the error colour to separate it from P1.
322
+ - **`POST /api/global-todo/capture` and the panel's 「从本对话抓取」 button**: the panel
323
+ injects a capture request into the conversation the user is in, which then records its
324
+ follow-ups with `global_todo_capture`. It is a request for work, not a hand-off: nothing is
325
+ recorded against a todo and no completion is expected.
326
+ - **Grouping dimensions.** The panel groups by `conversation` (default), `time` or `level`.
327
+ Conversation mode merges everything one agent conversation submitted into a single entry
328
+ that carries the count, collapsing it until asked — a list that several 「抓一下待办」 runs
329
+ filled up stops reading as a wall of rows. Items with no source conversation gather under
330
+ 「手动添加」, and the row level (high / normal / low) is visible as a badge and as the
331
+ highest level of a conversation group.
332
+ - **A render check** (`npm run check:render`, part of `verify`): the client half has no unit
333
+ seam, so the generated panel HTML is asserted instead — merged conversations stay one
334
+ entry, single-item ones keep their row, levels and all three grouping buttons render, and
335
+ no literal colour leaks into the markup.
336
+ - **Automatic completion.** A handed-off session that closes its turn cleanly finishes its
337
+ todo by itself: the closing assistant message becomes `completion.summary` and the files
338
+ that turn changed become `completion.artifacts`. Two guards keep it honest — only a
339
+ session this plugin just dispatched to is eligible, and `error` / `aborted` /
340
+ `interrupted` turns leave the todo open. `lib/completion.js` watches `session/event`
341
+ (`assistant/message`, `workspace/changes`, `turn/end`) and resolves file lists through
342
+ the `workspaceChanges` recorder when an event does not carry them.
343
+ - `global_todo_done` accepts `summary` and `artifacts`, so an agent can write the record
344
+ itself; the completion prompt asks for them, and "don't mark it done if you couldn't" is
345
+ stated in the hand-off message.
346
+ - The panel renders the completion: 完成于 on the row, artifact chips next to the
347
+ attachments, and a completion section in the detail.
348
+ - `global_todo_capture`: record a batch of todos in one call, with per-entry validation so
349
+ one bad attachment does not lose the rest.
350
+ - Proactive-capture guidance in the system prompt: when the agent should record something
351
+ on its own initiative, when it must not, and 「抓一下待办」 as the phrase that asks it to
352
+ scan the conversation and capture a batch. The boundary is stated in the prompt too:
353
+ record, never start work.
354
+ - `POST /api/global-todo/restore` and the panel's undo: a deleted todo comes back
355
+ verbatim — same id, creation time, attachments and history — and a record whose id
356
+ is taken is refused rather than merged.
357
+ - `sessionAlive` on every listed todo, derived from the live agent registry on read, so
358
+ the panel can say 「会话在跑」 instead of 「已绑定会话」.
359
+ - `npm run preview`: renders the panel (and an opened detail view) to static HTML with
360
+ no Host and no dependencies, for UI work and review.
361
+
362
+ ### Fixed
363
+
364
+ - **The receipt's jump button did nothing.** Client services arrive through
365
+ `ctx.inject([...])`, not `ctx.get(...)`: the directory marks `uiWorkspace` as a hard
366
+ dependency for exactly that reason. It is now obtained by injection, and the button is
367
+ only rendered when `openSession` really exists — a button that does nothing is worse than
368
+ no button.
369
+ - **A hand-off still landed in the wrong conversation.** Inference about "the conversation
370
+ you are in" was wrong twice, so it is no longer the mechanism: the toolbar now lists the
371
+ conversations and the user picks one (`交给:[ 对话名 ▾ ]`). An explicit pick is sent with
372
+ `authoritative: true`, which the Host honours verbatim; the list marks the conversation the
373
+ Host would have chosen on its own, `自动` keeps the old behaviour, and a pick is remembered.
374
+ - **A hand-off pulled the next hand-off with it.** The "conversation you were last in" came
375
+ from `api-session/activity`, which reports *any* user-authored durable message — and a
376
+ hand-off is one: the plugin injects a user-role message into its target. So every dispatch
377
+ moved "the current conversation" to the conversation it had just dispatched to, and the
378
+ next click followed the previous click instead of the person. The tracker now reads the
379
+ session log and keeps only `user/message` events whose `source.kind` is `user`, so model
380
+ turns, tool results and plugin injections cannot move it. The id is also kept next to the
381
+ data (`recent-session.json`), because the tracker lives in memory and this plugin restarts
382
+ with the Host: an empty tracker falls back to the panel's stale guess, which is how a
383
+ hand-off reached the wrong conversation. Covered by two smoke tests — the message sequence,
384
+ and a restart that must not forget.
385
+ - **A hand-off could start work silently in another conversation.** The panel said nothing
386
+ after a successful dispatch, so a person staring at the todo list saw "no reaction" while
387
+ the agent in a different conversation was already working. The panel now names the target
388
+ and offers to open it, and the pre-click label uses the conversation the Host will actually
389
+ pick (`/health`'s recent-session answer) instead of the panel's own guess.
390
+ - **The panel's guess about "the conversation on screen" outranked the real one.** While the
391
+ panel is the main view, its idea of the current session is whatever the view retained
392
+ earlier — hours stale in practice, and it sent work to the wrong conversation. The activity
393
+ feed (a real user message) now wins; a caller that genuinely knows — a button rendered
394
+ inside a conversation — opts in with `authoritative: true`.
395
+ - **Auto-completion recorded an empty artifact list.** `workspace/changes` is not appended
396
+ with `turn/end`: the recorder enqueues a turn-end snapshot and diff when the turn closes
397
+ and announces the result in a later event. Completing on `turn/end` therefore always saw
398
+ no files. The record is now written when that summary arrives, with a bounded wait so a
399
+ turn that changed nothing still completes, and a new turn settles the previous wait.
400
+ - **A stated blocker was overwritten by an automatic "done".** Auto-completion now backs off
401
+ when the agent used a verdict tool of its own (`global_todo_done`, `global_todo_update`,
402
+ `global_todo_remove`) during the turn: an agent that says "this is blocked, here is why"
403
+ must not be contradicted by a heuristic. Read-only tools (`list`, `get`) do not count.
404
+ - **「交给 Agent」 did not mean "hand it to the conversation I am in".** The default was
405
+ `auto`, which reused the session an earlier click had created — so a second click sent the
406
+ work back to that side conversation instead of the chat on screen, and the first click
407
+ opened a session nobody asked for. The panel now defaults to `current`, shows the target
408
+ before the click, and the Host resolves "current" from the `api-session/activity` feed
409
+ (the conversation the user last wrote in), falling back to the conversation the todo was
410
+ written from. `auto` now means bound → current → new, and an unresolvable target fails
411
+ with a reason instead of silently creating a conversation.
412
+ - **A hand-off created sessions with no working directory.** `meta.cwd` was only set when
413
+ `dispatchDir` was configured, so the Host filed the new sessions under `_no-cwd` and the
414
+ agent inside them could do nothing. The directory is now resolved in order — what the
415
+ caller names, the directory the todo was written from, the bound conversation's
416
+ directory, then `dispatchDir` — and a hand-off with none of them is refused with an
417
+ actionable message instead of quietly creating a dead session.
418
+ - A live conversation with no working directory is flagged on its row, and `auto` no longer
419
+ reuses one: it opens a fresh session in a resolved directory instead, so a single click
420
+ recovers a todo that was handed to a dead-end conversation.
421
+
422
+ ### Added
423
+
424
+ - Every todo written through a tool records `originDir` and `originSessionId`, read from
425
+ the calling agent in `execute(args, exec)`.
426
+ - The panel's 工作目录 control: follows the conversation you are in (read from the shell's
427
+ `useSessions` state), or a directory you pick with the Host's picker — remembered across
428
+ sessions and overridable per dispatch.
429
+ - **A hot reload kept the previous stylesheet.** The panel's `<style>` tag was reused
430
+ when it already existed, so new markup ran against the old rules: class names the old
431
+ stylesheet never knew rendered with no styles at all — a panel that looked "unchanged"
432
+ while its code was already new. The tag is now replaced (and stamped with the build
433
+ revision), with a bundle test for the reload path.
434
+ - **The agent tools were missing entirely.** Their output schemas used
435
+ `type: ['string', 'null']`, which the Host's schema subset does not accept, so
436
+ registration was rejected for all seven tools. Nullability is now `oneOf`, and
437
+ `test/schema.test.mjs` plus the Host-validator step in `npm run check` keep the
438
+ class of failure out.
439
+ - The `/health` payload reports the tool registration state, which is what surfaced
440
+ the failure in a live Host.
441
+
442
+ ### Changed
443
+
444
+ - Rows now keep `▶ 交给 Agent` visible at all times (hiding the only way to hand work to an
445
+ agent behind a hover made a lone todo look inert), list the first attachments as chips
446
+ (a link opens, a document copies its path, `+N` for the rest), and separate the meta line
447
+ with `·`.
448
+ - Row cards use the raised surface and a heavier title, so the list keeps its structure in
449
+ a light theme; the row action is an outline button instead of a filled one.
450
+ - Panel redesign: urgency grouping with collapsible sections, hover-revealed row actions
451
+ behind a menu, count chips that double as filters, a search field with a `/` hint,
452
+ attachment rows with 打开 / 复制路径, an inline edit section with a dirty-state save,
453
+ and a footer that names the keyboard shortcuts.
454
+ - The word "attachment" now covers addresses and documents consistently in the UI.
455
+
456
+ ### Added
457
+
458
+ - Global todo list persisted as one JSON file (`$DSH_HOME/global-todo/todos.json`
459
+ by default), written atomically and serialized so the human and the agent can
460
+ never lose each other's change.
461
+ - Creation time on every entry: `createdAt` is set once, never edited, and shown
462
+ in the panel, in the tool output and in every dispatch prompt.
463
+ - Attachments as references — any number of documents (absolute paths) and
464
+ addresses (http/https) per todo, with URL credentials refused; documents stay
465
+ where they are instead of being copied into plugin storage.
466
+ - Seven agent tools: `global_todo_add`, `global_todo_list`, `global_todo_get`,
467
+ `global_todo_update`, `global_todo_link`, `global_todo_done`,
468
+ `global_todo_remove`.
469
+ - Web panel in the sidebar with quick add, filters, text search, a detail view
470
+ showing the activity trail, and inline attachment editing.
471
+ - One-click hand-off to an agent session: `auto` (reuse the bound conversation,
472
+ otherwise start one), `new`, `bound` and `current` modes, with the outcome —
473
+ success or the reason for failure — recorded on the todo.
474
+ - HTTP API (`/api/global-todo/*`) fenced by the host connection fence, with a
475
+ strict loopback fallback and real status codes.
476
+ - Optional-service degradation: with no `webServer`, `tools`, `agents` or
477
+ `agentPresets` service, the plugin loses exactly that surface instead of
478
+ failing to load.
479
+
480
+ [Unreleased]: https://github.com/LUgABeEEE/dsh-global-todo/compare/v0.3.1...HEAD
481
+ [0.3.1]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.3.1
482
+ [0.3.0]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.3.0
483
+ [0.2.0]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.2.0
484
+ [0.1.0]: https://github.com/LUgABeEEE/dsh-global-todo/releases/tag/v0.1.0
@@ -0,0 +1,58 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a
6
+ harassment-free experience for everyone, regardless of age, body size, visible or invisible
7
+ disability, ethnicity, sex characteristics, gender identity and expression, level of
8
+ experience, education, socio-economic status, nationality, personal appearance, race,
9
+ religion, or sexual identity and orientation.
10
+
11
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse,
12
+ inclusive, and healthy community.
13
+
14
+ ## Our standards
15
+
16
+ Examples of behavior that contributes to a positive environment:
17
+
18
+ - Demonstrating empathy and kindness toward other people
19
+ - Being respectful of differing opinions, viewpoints, and experiences
20
+ - Giving and gracefully accepting constructive feedback
21
+ - Accepting responsibility and apologizing to those affected by our mistakes, and learning
22
+ from the experience
23
+ - Focusing on what is best not just for us as individuals, but for the overall community
24
+
25
+ Examples of unacceptable behavior:
26
+
27
+ - The use of sexualized language or imagery, and sexual attention or advances of any kind
28
+ - Trolling, insulting or derogatory comments, and personal or political attacks
29
+ - Public or private harassment
30
+ - Publishing others' private information, such as a physical or email address, without
31
+ their explicit permission
32
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
33
+
34
+ ## Enforcement responsibilities
35
+
36
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable
37
+ behavior and will take appropriate and fair corrective action in response to any behavior
38
+ that they deem inappropriate, threatening, offensive, or harmful.
39
+
40
+ ## Scope
41
+
42
+ This Code of Conduct applies within all community spaces, and also applies when an
43
+ individual is officially representing the community in public spaces.
44
+
45
+ ## Enforcement
46
+
47
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the
48
+ maintainer through the contact route in [SECURITY.md](SECURITY.md). All complaints will be
49
+ reviewed and investigated promptly and fairly, and the privacy and security of the reporter
50
+ will be respected.
51
+
52
+ ## Attribution
53
+
54
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1,
55
+ available at
56
+ <https://www.contributor-covenant.org/version/2/1/code_of_conduct.html>.
57
+
58
+ [homepage]: https://www.contributor-covenant.org
@@ -0,0 +1,101 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time. This project is small on purpose: no build step, no runtime
4
+ dependencies, and a test suite that runs in a couple of seconds. Please keep it that way.
5
+ 中文亦可,Issue 和 PR 用中文写没问题。
6
+
7
+ ## Getting set up
8
+
9
+ ```sh
10
+ git clone https://github.com/LUgABeEEE/dsh-global-todo.git
11
+ cd dsh-global-todo
12
+ npm run verify # static checks + the whole test suite
13
+ dsh plugin --profile web add "$PWD" # optional: try it in a live host
14
+ ```
15
+
16
+ There is nothing to install: the plugin has no runtime and no development dependencies.
17
+ Node 22.19 or newer is all you need.
18
+
19
+ ## The loop
20
+
21
+ 1. **Open an issue first** for anything larger than a bug fix — a new field, a new route, a
22
+ new dispatch mode. Design conversations are cheaper than review cycles.
23
+ 2. Keep the change to one concern.
24
+ 3. Add or extend tests. A behaviour change without a test will be asked for one.
25
+ 4. Run `npm run verify`.
26
+ 5. Open a PR and fill in the template.
27
+
28
+ ## What the tests are for
29
+
30
+ | File | Protects |
31
+ |---|---|
32
+ | `test/links.test.mjs` | attachment rules, including the credential refusal |
33
+ | `test/model.test.mjs` | the entity: validation, patches, activity, tolerant loads |
34
+ | `test/store.test.mjs` | atomic writes, the write lock, corrupt-file behaviour |
35
+ | `test/service.test.mjs` | filters, sorting, the shared write path |
36
+ | `test/tools.test.mjs` | tool schemas and every tool's success and failure shape |
37
+ | `test/routes.test.mjs` | HTTP status codes, the fence, body limits |
38
+ | `test/dispatch.test.mjs` | all four dispatch modes, preset mounting, recorded failures |
39
+ | `test/bundle.test.mjs` | the browser half: registration, slots, theme tokens, route contract |
40
+ | `test/manifest.test.mjs` | `package.json`, the patch row, exports, version agreement |
41
+ | `test/schema.test.mjs` | the Host's tool-schema subset — a schema outside it is rejected at registration, silently |
42
+
43
+ ## House rules
44
+
45
+ - **Plain ESM, no dependencies.** No bundler, no TypeScript, no `@deepseek-ai/*` import in
46
+ `lib/`. Raw JSON Schema for tool parameters; raw `webServer.register` for routes. This is
47
+ what keeps the plugin loading in lean compositions and across `0.2.x` releases.
48
+ - **Comments explain why, not what.** A non-obvious guard should say which failure it
49
+ prevents. Restating a line of code in prose is noise.
50
+ - **User-facing strings need both languages.** Client dictionaries live in `lib/client.js`
51
+ (`zh` and `en`); host-side messages are Chinese, matching the rest of the ecosystem.
52
+ - **Errors are typed.** Raise `TodoError` with a code from `lib/errors.js` rather than
53
+ throwing a bare `Error`; the HTTP layer maps codes to statuses and the tests assert them.
54
+ - **Never write outside your own surface.** The client half contributes through slots and
55
+ its own stylesheet tag; the host half registers routes, tools and one prompt section.
56
+ - **No secrets, ever.** The store is local and the plugin stores no credentials. The URL
57
+ rule that refuses `user:password@` exists for this reason — do not relax it.
58
+
59
+ ## Releasing
60
+
61
+ Facts checked on 2026-10-14, so nobody has to re-derive them:
62
+
63
+ - the npm name `dsh-global-todo` is **still free** (`registry.npmjs.org/dsh-global-todo` → 404);
64
+ - the repository `LUgABeEEE/dsh-global-todo` exists and holds the first commit
65
+ (`2a6f716`), so the `repository` field in `package.json` now points at something real;
66
+ CI has run green on both matrix legs (node 22.19.0 and 24.x).
67
+
68
+ The order that avoids publishing a package pointing at nothing:
69
+
70
+ 1. **`npm run verify`** — static checks plus the whole suite. This is also what CI runs.
71
+ 2. **First commit.** The repository has none yet, so there is no history to preserve:
72
+ `git init` and commit with the identity used across this machine —
73
+ `git -c user.name=LUgABeEEE -c user.email=LUgABeEEE@users.noreply.github.com commit`.
74
+ Before committing, confirm nothing secret is staged:
75
+ `git diff --cached | rg -i "apiKey|password|token|PRIVATE KEY"`.
76
+ `npm run verify` already performs the equivalent audit over every committable file, so a
77
+ green run is the evidence — and it also asserts that `.gitignore` keeps the local
78
+ artifacts (`node_modules/`, `preview/`, `todos.json`, backups) out of the commit.
79
+ 3. **Push and watch CI for real.** The workflow matrix is Node 22.19 and 24; it has only
80
+ ever been simulated locally, so the first green run is the first real evidence.
81
+ 4. **Publish with Trusted Publishing (OIDC).** `.github/workflows/publish.yml` is ready: it
82
+ verifies, then publishes with provenance when a `v*` tag is pushed or the workflow is
83
+ dispatched by hand. Before its first run, the npm side needs the binding — on the package
84
+ settings page add a trusted publisher with:
85
+ - repository `LUgABeEEE/dsh-global-todo`
86
+ - workflow file `publish.yml`
87
+ - environment: none (or one you create and also name here)
88
+
89
+ No long-lived token is involved, which is the point: nothing to leak from a laptop. Check
90
+ what would ship with `npm pack --dry-run`.
91
+ 5. **Cut the version.** Move CHANGELOG's `Unreleased` block under
92
+ `## 0.1.0 — <date>`, keep `package.json`'s `version` and `VERSION` in `lib/index.js`
93
+ equal (a test enforces it), and tag the commit.
94
+ 6. **Ask for a listing.** The plugin index (`awesome-dsh-plugin`) takes a PR: one line plus
95
+ a link, with `docs/screenshots/panel.png` as the picture.
96
+
97
+ ## Reporting a bug
98
+
99
+ Include the plugin version (Settings → Plugins, or `GET /api/global-todo/health`), the DSH
100
+ version, what you did, what happened, and what you expected. A redacted snippet of
101
+ `todos.json` helps when the problem is about a record.