dsh-global-todo 0.7.1 → 0.8.0
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 +32 -0
- package/README.md +3 -2
- package/README.zh.md +3 -2
- package/docs/DESIGN.md +19 -0
- package/docs/ROADMAP.md +90 -56
- package/docs/screenshots/panel-detail.png +0 -0
- package/docs/screenshots/panel-inbox.png +0 -0
- package/docs/screenshots/panel-reminders.png +0 -0
- package/docs/screenshots/panel-review.png +0 -0
- package/docs/screenshots/panel-select.png +0 -0
- package/docs/screenshots/panel-settings.png +0 -0
- package/docs/screenshots/panel.png +0 -0
- package/lib/client.js +135 -4
- package/lib/index.js +2 -1
- package/lib/model.js +60 -0
- package/lib/service.js +9 -0
- package/lib/tools.js +33 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## [0.8.0] — 2026-10-20
|
|
2
|
+
|
|
3
|
+
A todo can carry its plan, and the plan is what the agent receives.
|
|
4
|
+
|
|
5
|
+
- **`steps[]` on a todo**: `{ text, done }`, up to 20 steps of 200 characters. Strings are accepted
|
|
6
|
+
wherever steps are (create, agent tools), a blank line is not a step, and a longer plan is
|
|
7
|
+
refused rather than truncated — twenty steps means the todo should have been split.
|
|
8
|
+
- **The dispatch prompt numbers them**, marking the ones already done. Handing over "1. 对齐版本号
|
|
9
|
+
2. 跑 verify 3. 打 tag" is worth more than handing over a paragraph.
|
|
10
|
+
- **The panel shows progress without opening the row** (`步骤 2/4` in the metadata), renders the
|
|
11
|
+
ticks in the detail, and takes one step per line in the add form.
|
|
12
|
+
- **Agent guidance says so too**: multi-round work should be recorded with 3–6 steps, and a
|
|
13
|
+
finished step is one `global_todo_update`.
|
|
14
|
+
- Replacement semantics (like `links`): the array travels whole through `applyPatch`, so there is
|
|
15
|
+
still one validation and one write path.
|
|
16
|
+
|
|
17
|
+
## [0.7.2] — 2026-10-20
|
|
18
|
+
|
|
19
|
+
The roadmap audited against the tree, and the review feature brought up to the same standard
|
|
20
|
+
as the rest of the panel.
|
|
21
|
+
|
|
22
|
+
- **`docs/ROADMAP.md` no longer lies.** Six entries claimed things that already exist (due
|
|
23
|
+
reminders, batch completion/tagging, archive, cross-machine notices, external ingest) — they
|
|
24
|
+
moved to *Landed* with the function that proves each one, and *Next* is now ordered by value
|
|
25
|
+
against effort with a "today" line per item.
|
|
26
|
+
- **`/review` has route tests**: a rejected claim reopens and drops its completion record,
|
|
27
|
+
answering twice is a 400 rather than a silent success, an unknown id is a 404.
|
|
28
|
+
- **The review state is photographed** — a fifth scenario (`panel-review.png`) in the same
|
|
29
|
+
harness as the others, asserted by `check:render` and indexed in both READMEs.
|
|
30
|
+
- **Bulk confirm in the review view**: selecting several finished-by-agent rows and confirming
|
|
31
|
+
them in one go, since assigning is not what that view is for.
|
|
32
|
+
|
|
1
33
|
## [0.7.1] — 2026-10-20
|
|
2
34
|
|
|
3
35
|
Documentation for the one piece of layout that cannot be "cleaned up".
|
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ preview and photographed by the same tool:
|
|
|
43
43
|
| Empty list | [`panel-empty.png`](docs/screenshots/panel-empty.png) |
|
|
44
44
|
| Filter with no match | [`panel-filtered.png`](docs/screenshots/panel-filtered.png) |
|
|
45
45
|
| Inbox (awaiting confirmation) | [`panel-inbox.png`](docs/screenshots/panel-inbox.png) |
|
|
46
|
+
| Awaiting review (agent finished) | [`panel-review.png`](docs/screenshots/panel-review.png) |
|
|
46
47
|
| Due reminder | [`panel-reminders.png`](docs/screenshots/panel-reminders.png) |
|
|
47
48
|
| Selection mode | [`panel-select.png`](docs/screenshots/panel-select.png) |
|
|
48
49
|
|
|
@@ -202,7 +203,7 @@ Your list is not touched by install or uninstall: it lives outside the plugin, a
|
|
|
202
203
|
|
|
203
204
|
| Tool | Purpose |
|
|
204
205
|
|---|---|
|
|
205
|
-
| `global_todo_add` | Record an item — description, attachments, tags, priority, due time |
|
|
206
|
+
| `global_todo_add` | Record an item — description, attachments, steps, tags, priority, due time |
|
|
206
207
|
| `global_todo_note` | Append a progress note — where you are, what blocks you, who must decide |
|
|
207
208
|
| `global_todo_capture` | **Record a batch**: for 「抓一下待办」 or a turn that produced several follow-ups; each entry is validated on its own |
|
|
208
209
|
| `global_todo_list` | Query by state, tag or text; every row carries its creation time |
|
|
@@ -263,7 +264,7 @@ fence when that service is present, and by a strict loopback check otherwise.
|
|
|
263
264
|
| 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 |
|
|
264
265
|
| 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 |
|
|
265
266
|
| 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) |
|
|
266
|
-
| POST | `/api/global-todo/create` | `{ title, description?, links?, tags?, priority?, dueAt? }` |
|
|
267
|
+
| POST | `/api/global-todo/create` | `{ title, description?, links?, steps?, tags?, priority?, dueAt? }` |
|
|
267
268
|
| POST | `/api/global-todo/update` | `{ id, ...fields, by?: human\|agent }` |
|
|
268
269
|
| POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
|
|
269
270
|
| POST | `/api/global-todo/remove` | `{ id }` |
|
package/README.zh.md
CHANGED
|
@@ -39,6 +39,7 @@ DSH 自带的 `todo_write` 属于**某一个 agent 会话**:整表替换、三
|
|
|
39
39
|
| 清单为空 | [`panel-empty.png`](docs/screenshots/panel-empty.png) |
|
|
40
40
|
| 筛选无结果 | [`panel-filtered.png`](docs/screenshots/panel-filtered.png) |
|
|
41
41
|
| 收件箱(待确认) | [`panel-inbox.png`](docs/screenshots/panel-inbox.png) |
|
|
42
|
+
| 待确认(Agent 完成) | [`panel-review.png`](docs/screenshots/panel-review.png) |
|
|
42
43
|
| 到点提醒 | [`panel-reminders.png`](docs/screenshots/panel-reminders.png) |
|
|
43
44
|
| 批量选择 | [`panel-select.png`](docs/screenshots/panel-select.png) |
|
|
44
45
|
|
|
@@ -127,7 +128,7 @@ dsh plugin --profile web remove dsh-global-todo
|
|
|
127
128
|
|
|
128
129
|
| 工具 | 用途 |
|
|
129
130
|
|---|---|
|
|
130
|
-
| `global_todo_add` |
|
|
131
|
+
| `global_todo_add` | 登记一条:描述、附件、步骤、标签、优先级、截止时间;同时记下「在哪个工作目录、哪个会话里写的」 |
|
|
131
132
|
| `global_todo_note` | 追加一条**进展记录**(做到哪了、卡在哪、需要谁做什么);不改写你的描述 |
|
|
132
133
|
| `global_todo_capture` | **一次登记多条**(批量抓取):用户说「抓一下待办」,或你一轮里发现好几件要跟进的;逐条校验,坏的那条只跳过它自己 |
|
|
133
134
|
| `global_todo_list` | 按状态/标签/关键词查询,每条都带上加入时间 |
|
|
@@ -159,7 +160,7 @@ dsh plugin --profile web remove dsh-global-todo
|
|
|
159
160
|
| POST | `/api/global-todo/bulk` | `{ ids, status?, addTags?, removeTags? }`——一次改一整批;失效的 id 进 `unmatched`,不会让整批失败 |
|
|
160
161
|
| POST | `/api/global-todo/ingest` | `{ items: [{ title, source: { kind }, confidence }], dryRun? }`,带 `Authorization: Bearer <settings.ingestToken>`——外部条目;置信度低于 `ingestConfirmBelow` 的先进收件箱 |
|
|
161
162
|
| GET | `/api/global-todo/health` | —(数据位置、统计、派发能力,以及**工具注册状态**与**自动收尾观察者是否挂上**——这两种失败是静默的,所以必须能查;还有 `metrics`:派发成功率、工具调用次数、清单增长;`autoCollect`:每个候选对话**为什么被问/没被问**、计次,以及设置文件是怎么加载进来的) |
|
|
162
|
-
| POST | `/api/global-todo/create` | `{ title, description?, links?, tags?, priority?, dueAt? }` |
|
|
163
|
+
| POST | `/api/global-todo/create` | `{ title, description?, links?, steps?, tags?, priority?, dueAt? }` |
|
|
163
164
|
| POST | `/api/global-todo/update` | `{ id, ...字段, by?: human\|agent }` |
|
|
164
165
|
| POST | `/api/global-todo/status` | `{ id, status, by?: human\|agent }` |
|
|
165
166
|
| POST | `/api/global-todo/remove` | `{ id }` |
|
package/docs/DESIGN.md
CHANGED
|
@@ -57,6 +57,25 @@ $DSH_HOME/global-todo/todos.json ← { version: 1, todos: [ … ] }
|
|
|
57
57
|
version, and a future migration step would run before the first write, keeping the old
|
|
58
58
|
file as a backup.
|
|
59
59
|
|
|
60
|
+
## A plan inside the todo
|
|
61
|
+
|
|
62
|
+
A todo is one line, but the work behind it is often five steps. Rather than grow a second object
|
|
63
|
+
(a "project", a "plan file"), a todo may carry `steps[]` — text plus a tick, order is the plan.
|
|
64
|
+
|
|
65
|
+
- **Why inside the todo:** the dispatch prompt is the product here. Handing an agent "ship the
|
|
66
|
+
plugin" is a paragraph; handing it "1. 对齐版本号 2. 跑 verify 3. 打 tag 4. 补截图" is a plan it
|
|
67
|
+
can execute and report against. The steps are also what a person reviews afterwards.
|
|
68
|
+
- **Shape:** `{ text, done }`, capped at `LIMITS.steps` (20) and 200 characters per step. Strings
|
|
69
|
+
are accepted wherever an array of steps is (create, agent tools) so a caller never has to spell
|
|
70
|
+
out `{ done: false }`; a blank line is a typo, not a step, and a plan longer than the cap is
|
|
71
|
+
refused rather than truncated — twenty steps means the todo should have been split.
|
|
72
|
+
- **Replacement, not patching:** `steps` travels as a whole array through `applyPatch`, the same
|
|
73
|
+
rule as `links`. Toggling one tick is a client-side rewrite of the array, which keeps one write
|
|
74
|
+
path and one validation.
|
|
75
|
+
- **Where it shows:** the row's metadata gets `步骤 2/4` (progress without opening anything), the
|
|
76
|
+
detail renders the ticks, the panel's add form takes one step per line, and the dispatch prompt
|
|
77
|
+
numbers them with `[已完成]` on the ones already behind us.
|
|
78
|
+
|
|
60
79
|
## On a wide window, the detail stands beside the list
|
|
61
80
|
|
|
62
81
|
Expanding a row inline is right on a narrow window and wrong on a wide one: the list jumps, the
|
package/docs/ROADMAP.md
CHANGED
|
@@ -110,95 +110,129 @@ in `skipped[]`, and archived/cancelled rows deliberately do not block a new one.
|
|
|
110
110
|
counts a disagreement once (`externalChanges`, `lastExternalAt` in `lib/store.js`; surfaced by
|
|
111
111
|
`/health.store`). The panel shows a one-line notice instead of letting a synced folder change the
|
|
112
112
|
list silently.
|
|
113
|
-
**Side detail on wide windows** —
|
|
114
|
-
panel's right edge instead of pushing the list down (CSS-only over
|
|
115
|
-
implementation), while narrow windows keep the inline expansion.
|
|
113
|
+
**Side detail on wide windows** — when the panel itself is at least 960px wide, the open row's
|
|
114
|
+
detail becomes a card at the panel's right edge instead of pushing the list down (CSS-only over
|
|
115
|
+
the same markup; one detail implementation), while narrow windows keep the inline expansion. The
|
|
116
|
+
breakpoint follows the panel's measured width, and the card is `position: fixed` with a measured
|
|
117
|
+
anchor — `DESIGN.md` records the two alternatives that were measured and rejected, so this is not
|
|
118
|
+
re-litigated every time someone tries to "clean it up".
|
|
116
119
|
**External ingest with an inbox** — `POST /ingest` takes source-tagged, confidence-scored items
|
|
117
120
|
behind a Bearer token; low-confidence records wait in an `inbox` tag view with 确认 / 丢弃 and a
|
|
118
121
|
seven-day sweep, so an external source cannot silently fill the list.
|
|
122
|
+
**An agent's "done" is a claim** — finishing as an agent sets `review = { pending }`; a person
|
|
123
|
+
confirms or sends it back through `POST /review` (`reviewTodo()` in `lib/model.js`, `review()` in
|
|
124
|
+
`lib/service.js`). Confirming keeps the record finished; rejecting reopens it and drops the
|
|
125
|
+
completion record, because "not actually done" is the only honest reading. The panel shows a
|
|
126
|
+
待确认 chip, a 待确认 filter and 确认完成 / 退回清单; bulk confirm exists in that view.
|
|
127
|
+
`counts.review` drives the chip. Tests: `test/model.test.mjs`, `test/routes.test.mjs`.
|
|
128
|
+
**A design system with guards** — five type sizes (20/14/13/12/11), four control heights, one
|
|
129
|
+
settings inspector, and no JavaScript layout measurement left in the panel. Three guards keep it
|
|
130
|
+
that way: `test/locale.test.mjs` (duplicate keys, zh/en parity, full-width punctuation in English,
|
|
131
|
+
missing `t()` keys), the scenario table in `tools/preview.mjs` (empty, filtered, inbox, review,
|
|
132
|
+
reminder) photographed into `docs/screenshots/` by `npm run screenshots`, and
|
|
133
|
+
`npm run check:render` asserting each state's marker.
|
|
119
134
|
### Partly landed
|
|
120
135
|
|
|
121
|
-
- **
|
|
122
|
-
|
|
123
|
-
`lib/
|
|
124
|
-
|
|
125
|
-
- **
|
|
126
|
-
(`lib/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- **
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
- **Session read-back** — `sessionAlive` is on every listed todo (`sessionAlive` in
|
|
137
|
+
`lib/dispatch.js`, surfaced by `GET /list`). Reading what that session last said, and
|
|
138
|
+
linking to it, is not: `lib/completion.js` reads `assistant/message` only to close a todo
|
|
139
|
+
this plugin dispatched, and stores one summary in its completion record.
|
|
140
|
+
- **What the agents produced** — the per-todo record with a summary and the changed files landed
|
|
141
|
+
(`completeTodo()` in `lib/model.js`, 「完成记录」 in `lib/client.js`, the 已完成 filter and
|
|
142
|
+
group). The cross-todo "what did the sessions finish this week, and what did they touch" view
|
|
143
|
+
did not.
|
|
144
|
+
- **Attachment health** — a dead file link is flagged on read, but there is no 「重新挂载」 next
|
|
145
|
+
to it (`relink` does not exist anywhere in `lib/`), and `annotateLinks()` has no unit test:
|
|
146
|
+
today the render assertion in `tools/preview.mjs` is the only coverage.
|
|
147
|
+
- **Batch** — selection, 一键分配, 完成 / 归档 / 打标签 and 批量确认(待确认视图)are all in.
|
|
148
|
+
Nothing here is missing; it is listed because the panel is where the batch surface lives.
|
|
134
149
|
|
|
135
150
|
## Next
|
|
136
151
|
|
|
137
|
-
|
|
152
|
+
Ordered by value against effort, and audited against the tree on 2026-10-20 — "today"
|
|
153
|
+
means what the code actually does, not what the last plan hoped.
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
`steps[]` on a todo, rendered as checkboxes, and shipped to the agent as a numbered
|
|
141
|
-
plan in the dispatch prompt.
|
|
155
|
+
### 1. ~~Checklist inside a todo~~ — landed in 0.8.0
|
|
142
156
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
`steps[]` (text + tick) ships on the todo, renders as `步骤 2/4` in the row and as ticks in the
|
|
158
|
+
detail, is editable from the panel's add form, and travels in the dispatch prompt as a numbered
|
|
159
|
+
plan. `normalizeSteps()` in `lib/model.js`, `dispatchPrompt()` in `lib/service.js`; tests in
|
|
160
|
+
`test/model.test.mjs` and `test/service.test.mjs`.
|
|
147
161
|
|
|
148
|
-
|
|
162
|
+
What is left of the original idea: making a step addressable on its own (a tool that ticks step 3
|
|
163
|
+
without resending the array) — not worth a tenth tool today.
|
|
149
164
|
|
|
150
|
-
|
|
151
|
-
that session last said — one line of its latest assistant message, and a link to open it
|
|
152
|
-
— so a dispatched todo reports back where the work is.
|
|
165
|
+
### 2. Session read-back — makes hand-off stop being fire-and-forget
|
|
153
166
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- Why it matters: hand-off stops being fire-and-forget.
|
|
167
|
+
`sessionAlive` says a bound conversation still exists. Next: one line of its latest
|
|
168
|
+
assistant message plus a link to open it.
|
|
157
169
|
|
|
158
|
-
|
|
170
|
+
- Today: `lib/completion.js` reads `assistant/message` only to close a todo this plugin
|
|
171
|
+
dispatched; nothing exposes it to the panel.
|
|
172
|
+
- Effort: medium, and it must stay cheap and failure-tolerant: a missing or huge message is
|
|
173
|
+
a normal case, not an error.
|
|
174
|
+
- Why second: the panel currently cannot answer "did the thing I handed over get anywhere",
|
|
175
|
+
which is the question people actually ask.
|
|
159
176
|
|
|
160
|
-
|
|
161
|
-
deployment already has.
|
|
177
|
+
### 3. Markdown / TaskPaper import and export
|
|
162
178
|
|
|
163
|
-
-
|
|
164
|
-
|
|
165
|
-
- Careful: **remind only, never auto-dispatch.** The list belongs to the human; starting
|
|
166
|
-
work stays an explicit act. (Auto-dispatch was rejected in `DESIGN.md` for this
|
|
167
|
-
reason.)
|
|
179
|
+
JSON round-trips byte for byte; the text formats are what let the list live in a repo and
|
|
180
|
+
diff.
|
|
168
181
|
|
|
169
|
-
|
|
182
|
+
- Today: only `GET /export` / `POST /import` over JSON.
|
|
183
|
+
- Effort: medium — a serializer plus a tolerant parser, both with round-trip tests.
|
|
184
|
+
- Why third: cheap portability, and it is the only way to compare this list with another
|
|
185
|
+
tool's.
|
|
170
186
|
|
|
171
|
-
|
|
172
|
-
multi-select that did not land with batch assignment.
|
|
187
|
+
### 4. The weekly agent report
|
|
173
188
|
|
|
174
|
-
-
|
|
189
|
+
Per-todo completion records already carry a summary and the changed files. A cross-todo
|
|
190
|
+
view — "what did the sessions finish this week, and what did they touch" — turns that into
|
|
191
|
+
something reviewable.
|
|
175
192
|
|
|
176
|
-
|
|
193
|
+
- Today: the data exists (`completion.artifacts`), the aggregate does not.
|
|
194
|
+
- Effort: small-medium; the honest version is a date-bounded list filtered on
|
|
195
|
+
`completion.by === 'agent'`, with `review` state beside each row.
|
|
177
196
|
|
|
178
|
-
|
|
179
|
-
tool and diffed in a repo.
|
|
197
|
+
### 5. Image attachments
|
|
180
198
|
|
|
181
|
-
|
|
199
|
+
`kind: 'image'` beside `url` / `file`, so a screenshot can be attached instead of a path
|
|
200
|
+
to one.
|
|
182
201
|
|
|
183
|
-
|
|
202
|
+
- Today: `LINK_KINDS = ['url', 'file']` (`lib/links.js`); the link shape is already prepared.
|
|
203
|
+
- Effort: medium — the store must not inline bytes, so this is a path with a preview and a
|
|
204
|
+
clear "the file is gone" state, which #3-adjacent work already models.
|
|
184
205
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
206
|
+
### 6. Source adapters for the ingest door
|
|
207
|
+
|
|
208
|
+
`POST /ingest` exists and is fenced by a token; nothing pushes to it.
|
|
209
|
+
|
|
210
|
+
- Today: the door, the inbox, the confidence threshold and the seven-day sweep are done.
|
|
211
|
+
There is no mail/notification reader anywhere in the repo.
|
|
212
|
+
- Effort: large, and the mode of the work is different: every adapter is an external
|
|
213
|
+
dependency with its own failure modes. Do it when a real source exists, not before.
|
|
214
|
+
|
|
215
|
+
### 7. Small, well-understood items
|
|
216
|
+
|
|
217
|
+
- `before:` / `after:` date bounds in the search syntax (the parser already walks tokens).
|
|
218
|
+
- 「重新挂载」 next to a dead attachment, plus the `annotateLinks()` unit test it owes.
|
|
219
|
+
- A screen-reader pass over the panel and the group headers; the keyboard path is close.
|
|
220
|
+
|
|
221
|
+
### Landed after this list was written
|
|
222
|
+
|
|
223
|
+
Due reminders (0.5.2), batch completion and tagging (`/bulk`), archive instead of
|
|
224
|
+
delete, cross-machine change notices, external ingest with an inbox — all four used to be
|
|
225
|
+
items here. See `Landed since 0.1` above; the entries were removed rather than left to
|
|
226
|
+
mislead.
|
|
188
227
|
|
|
189
228
|
## Later
|
|
190
229
|
|
|
191
230
|
| Idea | Note |
|
|
192
231
|
|---|---|
|
|
193
|
-
| Image attachments | `kind: 'image'` next to `url` / `file`. The shape is already prepared for it, but `LINK_KINDS` is still `['url', 'file']` (`lib/links.js`). |
|
|
194
232
|
| Official `storageDomain` backend | A second store backend behind the same interface; the file stays the default because it is the feature. Today there is one backend and it says so (`kind: 'json'`, `lib/store.js`). |
|
|
195
233
|
| Virtualised list | Only needed past a few hundred rows; measure first. Nothing in `lib/client.js` virtualises today. |
|
|
196
234
|
| Drag to reorder | Manual order is meaningful to some people; it also needs a stable `position` field and a decision about how it interacts with due-date grouping. |
|
|
197
235
|
| Multi-list / projects | A `list` field with one default list; deferred because "global single list" is the current identity. |
|
|
198
|
-
| Archive instead of delete | Keeps history without cluttering. `POST /remove` deletes for real (`lib/routes.js`); only the panel's short undo brings a record back. |
|
|
199
|
-
| Cross-machine conflict notice | The file is portable (NAS, sync folder). Detect "changed underneath me" and offer a reload instead of a silent overwrite — `store.read` still trusts the file whenever it parses. |
|
|
200
|
-
| Capture from outside the chat | Notifications, mail, announcements — the sources `dsh-todo-list` reads. Needs an ingest path and a policy for deduplication; `POST /capture` only asks a conversation to collect (`lib/routes.js`). |
|
|
201
|
-
| Accessible-only sweep | Full keyboard path is close; a screen-reader pass over the panel and the group headers is still owed. |
|
|
202
236
|
|
|
203
237
|
## Explicitly not planned
|
|
204
238
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -50,7 +50,7 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
* different mechanisms (restart vs. hot reload), so "which one am I looking
|
|
51
51
|
* at" needs an answer that the bundle itself carries.
|
|
52
52
|
*/
|
|
53
|
-
const UI_REV = '
|
|
53
|
+
const UI_REV = 'ui47'
|
|
54
54
|
/** Last language resolved for the shell; module-level so a fetch failure can be worded too. */
|
|
55
55
|
let activeLang = 'zh'
|
|
56
56
|
|
|
@@ -288,6 +288,13 @@ window.__ModuleLoader__.load({
|
|
|
288
288
|
'meta.session.noDirHint': '这个会话没有工作目录,agent 在里面做不了事;用「派发:开新会话」重派一次',
|
|
289
289
|
'meta.dispatchFailed': '上次派发失败',
|
|
290
290
|
'detail.description': '描述',
|
|
291
|
+
'meta.steps': '步骤 {done}/{total}',
|
|
292
|
+
'detail.steps': '步骤',
|
|
293
|
+
'steps.none': '还没有步骤。',
|
|
294
|
+
'field.steps': '第一步…(一行一步)',
|
|
295
|
+
'field.stepsHint': '一行一步;派发时会作为编号计划交给 Agent。',
|
|
296
|
+
'step.done': '标记这一步完成',
|
|
297
|
+
'step.undo': '取消这一步的完成标记',
|
|
291
298
|
'detail.noDescription': '没有描述。',
|
|
292
299
|
'detail.links': '相关文档与地址',
|
|
293
300
|
'detail.completion': '完成记录',
|
|
@@ -561,6 +568,13 @@ window.__ModuleLoader__.load({
|
|
|
561
568
|
'meta.session.noDirHint': 'This conversation has no working directory, so an agent cannot do anything in it; hand it off again with a new conversation.',
|
|
562
569
|
'meta.dispatchFailed': 'last hand-off failed',
|
|
563
570
|
'detail.description': 'Description',
|
|
571
|
+
'meta.steps': 'steps {done}/{total}',
|
|
572
|
+
'detail.steps': 'Steps',
|
|
573
|
+
'steps.none': 'No steps yet.',
|
|
574
|
+
'field.steps': 'First step… (one per line)',
|
|
575
|
+
'field.stepsHint': 'One per line; they ship to the agent as a numbered plan.',
|
|
576
|
+
'step.done': 'Mark this step done',
|
|
577
|
+
'step.undo': 'Clear this step',
|
|
564
578
|
'detail.noDescription': 'No description.',
|
|
565
579
|
'detail.links': 'Documents and links',
|
|
566
580
|
'detail.noLinks': 'No attachments. Attach an absolute document path, or a URL.',
|
|
@@ -1281,6 +1295,16 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1281
1295
|
.gt-links { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
|
1282
1296
|
.gt-link { display: flex; gap: 8px; align-items: center; padding: 4px 0; min-width: 0; }
|
|
1283
1297
|
.gt-link-main { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
|
|
1298
|
+
.gt-textarea-steps { min-height: 76px; }
|
|
1299
|
+
.gt-steps { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none; }
|
|
1300
|
+
.gt-step { display: flex; align-items: flex-start; gap: 8px; padding: 3px 0; }
|
|
1301
|
+
.gt-step-check { flex: 0 0 auto; width: 18px; height: 18px; margin-top: 2px; padding: 0; cursor: pointer;
|
|
1302
|
+
border-radius: 5px; border: 1.4px solid var(--dsw-alias-border-l2); background: transparent;
|
|
1303
|
+
color: transparent; display: inline-flex; align-items: center; justify-content: center; }
|
|
1304
|
+
.gt-step[data-done="1"] .gt-step-check { border-color: var(--dsw-alias-state-success-primary);
|
|
1305
|
+
background: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-bg-base); }
|
|
1306
|
+
.gt-step-text { font-size: 13px; line-height: 20px; word-break: break-word; }
|
|
1307
|
+
.gt-step[data-done="1"] .gt-step-text { color: var(--dsw-alias-label-secondary); text-decoration: line-through; }
|
|
1284
1308
|
.gt-note-item { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
|
|
1285
1309
|
.gt-note-item + .gt-note-item { border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
1286
1310
|
.gt-note-meta { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dsw-alias-label-secondary);
|
|
@@ -1671,6 +1695,12 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1671
1695
|
)
|
|
1672
1696
|
}
|
|
1673
1697
|
meta.push(h('span', { key: 'added', title: new Date(todo.createdAt).toLocaleString() }, ageText(lang, todo.createdAt, now)))
|
|
1698
|
+
// Progress on the plan, when there is one: the row shows where it stands without
|
|
1699
|
+
// opening the detail.
|
|
1700
|
+
if (Array.isArray(todo.steps) && todo.steps.length > 0) {
|
|
1701
|
+
const done = todo.steps.filter((step) => step.done === true).length
|
|
1702
|
+
meta.push(h('span', { key: 'steps' }, t('meta.steps', { done, total: todo.steps.length })))
|
|
1703
|
+
}
|
|
1674
1704
|
if (todo.tags.length > 0) {
|
|
1675
1705
|
const shown = todo.tags.slice(0, 4)
|
|
1676
1706
|
meta.push(
|
|
@@ -2155,6 +2185,40 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2155
2185
|
),
|
|
2156
2186
|
),
|
|
2157
2187
|
),
|
|
2188
|
+
h(
|
|
2189
|
+
'div',
|
|
2190
|
+
{ className: 'gt-section' },
|
|
2191
|
+
h('div', { className: 'gt-section-title' }, t('detail.steps')),
|
|
2192
|
+
(todo.steps ?? []).length === 0
|
|
2193
|
+
? h('div', { className: 'gt-muted' }, t('steps.none'))
|
|
2194
|
+
: h(
|
|
2195
|
+
'ul',
|
|
2196
|
+
{ className: 'gt-steps' },
|
|
2197
|
+
todo.steps.map((step, index) =>
|
|
2198
|
+
h(
|
|
2199
|
+
'li',
|
|
2200
|
+
{ key: `${index}:${step.text}`, className: 'gt-step', 'data-done': step.done === true ? '1' : '0' },
|
|
2201
|
+
h(
|
|
2202
|
+
'button',
|
|
2203
|
+
{
|
|
2204
|
+
type: 'button',
|
|
2205
|
+
className: 'gt-step-check',
|
|
2206
|
+
'aria-pressed': step.done === true,
|
|
2207
|
+
title: step.done === true ? t('step.undo') : t('step.done'),
|
|
2208
|
+
'aria-label': step.done === true ? t('step.undo') : t('step.done'),
|
|
2209
|
+
disabled: busy,
|
|
2210
|
+
onClick: () =>
|
|
2211
|
+
onAction('steps', todo, {
|
|
2212
|
+
steps: todo.steps.map((item, i) => (i === index ? { text: item.text, done: item.done !== true } : { text: item.text, done: item.done === true })),
|
|
2213
|
+
}),
|
|
2214
|
+
},
|
|
2215
|
+
step.done === true ? h(Icon, { name: 'check', size: 11 }) : null,
|
|
2216
|
+
),
|
|
2217
|
+
h('span', { className: 'gt-step-text' }, step.text),
|
|
2218
|
+
),
|
|
2219
|
+
),
|
|
2220
|
+
),
|
|
2221
|
+
),
|
|
2158
2222
|
h(
|
|
2159
2223
|
'div',
|
|
2160
2224
|
{ className: 'gt-section' },
|
|
@@ -2349,7 +2413,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2349
2413
|
const [more, setMore] = useState(false)
|
|
2350
2414
|
const [collapsed, setCollapsed] = useState({})
|
|
2351
2415
|
const [undo, setUndo] = useState(null)
|
|
2352
|
-
const [draft, setDraft] = useState({ title: '', description: '', links: '', tags: '', priority: 'normal', due: '' })
|
|
2416
|
+
const [draft, setDraft] = useState({ title: '', description: '', links: '', steps: '', tags: '', priority: 'normal', due: '' })
|
|
2353
2417
|
// Conversations come from the shell's own sessions service, declared in this
|
|
2354
2418
|
// plugin's `inject` list the way the shipped panels do it
|
|
2355
2419
|
// (`ui-subagent` / `agent-team`: ["sessions", "uiWorkspace", "slots", "locale"]).
|
|
@@ -2750,6 +2814,11 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2750
2814
|
.split(',')
|
|
2751
2815
|
.map((part) => part.trim())
|
|
2752
2816
|
.filter((part) => part !== '')
|
|
2817
|
+
// One step per line: the shape a person already writes a plan in.
|
|
2818
|
+
const steps = (draft.steps ?? '')
|
|
2819
|
+
.split('\n')
|
|
2820
|
+
.map((line) => line.trim())
|
|
2821
|
+
.filter((line) => line !== '')
|
|
2753
2822
|
await run('__new__', () =>
|
|
2754
2823
|
call('create', {
|
|
2755
2824
|
method: 'POST',
|
|
@@ -2757,13 +2826,14 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2757
2826
|
title,
|
|
2758
2827
|
...(draft.description.trim() === '' ? {} : { description: draft.description.trim() }),
|
|
2759
2828
|
...(links.length === 0 ? {} : { links }),
|
|
2829
|
+
...(steps.length === 0 ? {} : { steps }),
|
|
2760
2830
|
...(tags.length === 0 ? {} : { tags }),
|
|
2761
2831
|
...(draft.priority === 'normal' ? {} : { priority: draft.priority }),
|
|
2762
2832
|
...(draft.due === '' ? {} : { dueAt: new Date(draft.due).toISOString() }),
|
|
2763
2833
|
},
|
|
2764
2834
|
}),
|
|
2765
2835
|
)
|
|
2766
|
-
setDraft({ title: '', description: '', links: '', tags: '', priority: 'normal', due: '' })
|
|
2836
|
+
setDraft({ title: '', description: '', links: '', steps: '', tags: '', priority: 'normal', due: '' })
|
|
2767
2837
|
setMore(false)
|
|
2768
2838
|
}
|
|
2769
2839
|
|
|
@@ -3197,6 +3267,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3197
3267
|
if (kind === 'archive') return run(todo.id, () => call('status', { method: 'POST', body: { id: todo.id, status: 'archived' } }))
|
|
3198
3268
|
if (kind === 'unarchive') return run(todo.id, () => call('status', { method: 'POST', body: { id: todo.id, status: 'open' } }))
|
|
3199
3269
|
if (kind === 'save') return run(todo.id, () => call('update', { method: 'POST', body: { id: todo.id, ...payload } }))
|
|
3270
|
+
if (kind === 'steps') return run(todo.id, () => call('update', { method: 'POST', body: { id: todo.id, steps: payload.steps } }))
|
|
3200
3271
|
if (kind === 'note') return run(todo.id, () => call('note', { method: 'POST', body: { id: todo.id, ...payload } }))
|
|
3201
3272
|
if (kind === 'link') return run(todo.id, () => call('link', { method: 'POST', body: { id: todo.id, ...payload } }))
|
|
3202
3273
|
if (kind === 'unlink') return run(todo.id, () => call('link', { method: 'POST', body: { id: todo.id, remove: [payload.index] } }))
|
|
@@ -3320,6 +3391,39 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3320
3391
|
}
|
|
3321
3392
|
}
|
|
3322
3393
|
|
|
3394
|
+
// Reviewing is a per-record decision, so it cannot ride on /bulk: answering is not a
|
|
3395
|
+
// status change, and a rejected claim has to clean up its completion record.
|
|
3396
|
+
const runBulkReview = async () => {
|
|
3397
|
+
const ids = Object.keys(selected)
|
|
3398
|
+
if (ids.length === 0) return
|
|
3399
|
+
try {
|
|
3400
|
+
const results = await Promise.all(
|
|
3401
|
+
ids.map((id) =>
|
|
3402
|
+
call('review', { method: 'POST', body: { id, decision: 'confirm' } }).then(
|
|
3403
|
+
() => true,
|
|
3404
|
+
() => false,
|
|
3405
|
+
),
|
|
3406
|
+
),
|
|
3407
|
+
)
|
|
3408
|
+
const done = results.filter(Boolean).length
|
|
3409
|
+
setNotice({
|
|
3410
|
+
text:
|
|
3411
|
+
done === ids.length
|
|
3412
|
+
? t('notice.bulk', { n: done })
|
|
3413
|
+
: t('notice.bulkPartial', { n: done, m: ids.length - done }),
|
|
3414
|
+
sessionId: '',
|
|
3415
|
+
})
|
|
3416
|
+
setTimeout(() => setNotice(null), 6000)
|
|
3417
|
+
setSelected({})
|
|
3418
|
+
setError(null)
|
|
3419
|
+
void reload()
|
|
3420
|
+
void refreshHealth()
|
|
3421
|
+
} catch (failure) {
|
|
3422
|
+
reportFailure('bulk-review', { message: failure?.message })
|
|
3423
|
+
setError(failure)
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3323
3427
|
const counts = state.counts ?? { total: 0, open: 0, doing: 0, done: 0, cancelled: 0, archived: 0, overdue: 0, inbox: 0 }
|
|
3324
3428
|
const remindersDue = Array.isArray(state.health?.reminders?.due) ? state.health.reminders.due : []
|
|
3325
3429
|
const dueKey = remindersDue.map((item) => `${item.id}:${item.dueAt}`).join('|')
|
|
@@ -3737,6 +3841,19 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3737
3841
|
}),
|
|
3738
3842
|
h('span', { className: 'gt-hint' }, t('field.linksHint')),
|
|
3739
3843
|
),
|
|
3844
|
+
h(
|
|
3845
|
+
'div',
|
|
3846
|
+
{ className: 'gt-field' },
|
|
3847
|
+
h('label', { className: 'gt-field-label', htmlFor: 'gt-new-steps' }, t('detail.steps')),
|
|
3848
|
+
h('textarea', {
|
|
3849
|
+
id: 'gt-new-steps',
|
|
3850
|
+
className: 'gt-textarea gt-textarea-steps',
|
|
3851
|
+
placeholder: t('field.steps'),
|
|
3852
|
+
value: draft.steps,
|
|
3853
|
+
onChange: (event) => setDraft({ ...draft, steps: event.target.value }),
|
|
3854
|
+
}),
|
|
3855
|
+
h('span', { className: 'gt-hint' }, t('field.stepsHint')),
|
|
3856
|
+
),
|
|
3740
3857
|
h(
|
|
3741
3858
|
'div',
|
|
3742
3859
|
{ className: 'gt-field-row' },
|
|
@@ -4062,11 +4179,25 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
4062
4179
|
t('select.clearAll'),
|
|
4063
4180
|
),
|
|
4064
4181
|
h('span', { className: 'gt-spacer' }),
|
|
4182
|
+
// In the review view the batch action is confirming, not assigning: every
|
|
4183
|
+
// record on screen is finished and waiting for a word.
|
|
4184
|
+
filter === 'review'
|
|
4185
|
+
? h(
|
|
4186
|
+
'button',
|
|
4187
|
+
{
|
|
4188
|
+
type: 'button',
|
|
4189
|
+
className: 'gt-btn gt-btn-primary',
|
|
4190
|
+
disabled: Object.keys(selected).length === 0,
|
|
4191
|
+
onClick: () => void runBulkReview(),
|
|
4192
|
+
},
|
|
4193
|
+
t('review.confirm'),
|
|
4194
|
+
)
|
|
4195
|
+
: null,
|
|
4065
4196
|
h(
|
|
4066
4197
|
'button',
|
|
4067
4198
|
{
|
|
4068
4199
|
type: 'button',
|
|
4069
|
-
className: 'gt-btn gt-btn-primary',
|
|
4200
|
+
className: filter === 'review' ? 'gt-btn gt-btn-quiet' : 'gt-btn gt-btn-primary',
|
|
4070
4201
|
disabled: assignBusy || Object.keys(selected).length === 0,
|
|
4071
4202
|
onClick: () => void runAssign(Object.keys(selected)),
|
|
4072
4203
|
},
|
package/lib/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import { createStore, resolveStorePath } from './store.js'
|
|
|
36
36
|
import { createTools } from './tools.js'
|
|
37
37
|
|
|
38
38
|
/** Must equal `package.json`'s version; `npm run check` enforces that. */
|
|
39
|
-
export const VERSION = '0.
|
|
39
|
+
export const VERSION = '0.8.0'
|
|
40
40
|
|
|
41
41
|
/** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
|
|
42
42
|
export const name = 'dsh-global-todo'
|
|
@@ -74,6 +74,7 @@ const GUIDANCE = [
|
|
|
74
74
|
'- 用户说「记一下 / 别忘了 / 回头办」。',
|
|
75
75
|
'**不要记**:你这一步就能做完的、本会话内的执行计划、纯闲聊。宁可少记。',
|
|
76
76
|
'记的时候:标题一句话说清要做什么;把相关文档**绝对路径**或网址挂进 `links`(接手时会作为上下文一起交出去);需要精确时间的写 `dueAt`。',
|
|
77
|
+
'**大活拆步骤**:一件需要多轮才能做完的事,用 `steps` 记下 3–6 步(`global_todo_add` 的 `steps` 参数)——派发时这些步骤会作为编号计划原样交给接手的 Agent,比一段散文有用得多;做完一步就用 `global_todo_update` 把该步的 `done` 置为 true。',
|
|
77
78
|
'记完用一句话说明记了什么(别复述整条描述)。做完用 `global_todo_done` 留痕,**不要删**。',
|
|
78
79
|
'**一轮收尾时顺手划掉**:如果清单里某条就是你刚做完的事,直接 `global_todo_done` 结束它,别留着占地方(插件也会在你安静下来后自动划掉,但你先划更准)。',
|
|
79
80
|
'做不到 / 卡住了:用 `global_todo_note` 追加一条进展,说明卡在哪、需要用户做什么——**不要**改写待办的 description(那是用户自己的文字)。',
|
package/lib/model.js
CHANGED
|
@@ -52,6 +52,10 @@ export const LIMITS = Object.freeze({
|
|
|
52
52
|
activity: 50,
|
|
53
53
|
note: 2000,
|
|
54
54
|
notes: 50,
|
|
55
|
+
// A todo is a unit of work, not a project plan: a list longer than this is a sign the
|
|
56
|
+
// item should be split, and a cap keeps the dispatch prompt from becoming a document.
|
|
57
|
+
steps: 20,
|
|
58
|
+
step: 200,
|
|
55
59
|
})
|
|
56
60
|
|
|
57
61
|
/**
|
|
@@ -201,6 +205,9 @@ export function createTodo(input, options = {}) {
|
|
|
201
205
|
dueAt: normalizeDue(raw.dueAt ?? raw.due),
|
|
202
206
|
tags: normalizeTags(raw.tags),
|
|
203
207
|
links,
|
|
208
|
+
// An optional plan inside the todo: the steps a person or an agent broke it into.
|
|
209
|
+
// Shipped to the agent as a numbered list, so hand-off stops being one paragraph.
|
|
210
|
+
steps: normalizeSteps(raw.steps),
|
|
204
211
|
createdAt: now,
|
|
205
212
|
updatedAt: now,
|
|
206
213
|
createdBy: by,
|
|
@@ -305,6 +312,13 @@ export function normalizeStored(raw, options = {}) {
|
|
|
305
312
|
}
|
|
306
313
|
})(),
|
|
307
314
|
links,
|
|
315
|
+
steps: (() => {
|
|
316
|
+
try {
|
|
317
|
+
return normalizeSteps(raw.steps)
|
|
318
|
+
} catch {
|
|
319
|
+
return []
|
|
320
|
+
}
|
|
321
|
+
})(),
|
|
308
322
|
createdAt,
|
|
309
323
|
updatedAt: Number.isFinite(raw.updatedAt) ? Number(raw.updatedAt) : createdAt,
|
|
310
324
|
createdBy: normalizeActor(raw.createdBy),
|
|
@@ -563,11 +577,23 @@ export function applyPatch(todo, patch, options = {}) {
|
|
|
563
577
|
changed.push('tags')
|
|
564
578
|
}
|
|
565
579
|
}
|
|
580
|
+
if (patch.steps !== undefined) {
|
|
581
|
+
const steps = normalizeSteps(patch.steps)
|
|
582
|
+
const same =
|
|
583
|
+
steps.length === todo.steps.length && steps.every((step, index) => step.text === todo.steps[index].text && step.done === todo.steps[index].done)
|
|
584
|
+
if (!same) {
|
|
585
|
+
todo.steps = steps
|
|
586
|
+
changed.push('steps')
|
|
587
|
+
}
|
|
588
|
+
}
|
|
566
589
|
if (patch.status !== undefined) {
|
|
567
590
|
const status = normalizeEnum(patch.status, STATUSES, 'invalid-status', '状态', todo.status)
|
|
568
591
|
if (status !== todo.status) {
|
|
569
592
|
todo.status = status
|
|
570
593
|
todo.doneAt = status === 'done' ? now : null
|
|
594
|
+
// Reaching done through a patch carries the same review rule as the dedicated
|
|
595
|
+
// status path: an agent's "done" is a claim, a person's is a decision.
|
|
596
|
+
todo.review = status === 'done' && by === 'agent' ? { pending: true, at: now, by } : null
|
|
571
597
|
changed.push('status')
|
|
572
598
|
}
|
|
573
599
|
}
|
|
@@ -626,6 +652,40 @@ export function setStatus(todo, status, options = {}) {
|
|
|
626
652
|
return true
|
|
627
653
|
}
|
|
628
654
|
|
|
655
|
+
/**
|
|
656
|
+
* An optional plan inside a todo.
|
|
657
|
+
*
|
|
658
|
+
* A step is text plus a tick, and the order is the plan. Strings are accepted for the
|
|
659
|
+
* common case (create, or an agent rewriting the plan) so a caller never has to spell out
|
|
660
|
+
* `{ done: false }` for every line.
|
|
661
|
+
*
|
|
662
|
+
* @param {unknown} value
|
|
663
|
+
* @returns {Array<{ text: string, done: boolean }>}
|
|
664
|
+
*/
|
|
665
|
+
export function normalizeSteps(value) {
|
|
666
|
+
if (value === null || value === undefined) return []
|
|
667
|
+
if (!Array.isArray(value)) throw new TodoError('invalid-steps', '步骤必须是数组')
|
|
668
|
+
if (value.length > LIMITS.steps) {
|
|
669
|
+
throw new TodoError('invalid-steps', `步骤最多 ${LIMITS.steps} 条,收到 ${value.length} 条`)
|
|
670
|
+
}
|
|
671
|
+
const out = []
|
|
672
|
+
for (const item of value) {
|
|
673
|
+
if (typeof item === 'string') {
|
|
674
|
+
const text = item.trim().slice(0, LIMITS.step)
|
|
675
|
+
// A blank line in a textarea is a typo, not a step.
|
|
676
|
+
if (text !== '') out.push({ text, done: false })
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
|
|
680
|
+
throw new TodoError('invalid-steps', '步骤必须是字符串或 { text, done }')
|
|
681
|
+
}
|
|
682
|
+
const text = typeof item.text === 'string' ? item.text.trim().slice(0, LIMITS.step) : ''
|
|
683
|
+
if (text === '') continue
|
|
684
|
+
out.push({ text, done: item.done === true })
|
|
685
|
+
}
|
|
686
|
+
return out
|
|
687
|
+
}
|
|
688
|
+
|
|
629
689
|
/**
|
|
630
690
|
* @param {object} todo
|
|
631
691
|
* @param {unknown} link
|
package/lib/service.js
CHANGED
|
@@ -555,6 +555,15 @@ export function createService(options) {
|
|
|
555
555
|
if (todo.description !== '') {
|
|
556
556
|
lines.push('', '描述:', todo.description)
|
|
557
557
|
}
|
|
558
|
+
// The plan, if there is one: a numbered list is what makes a hand-off concrete, and
|
|
559
|
+
// the ticks say which parts are already behind us.
|
|
560
|
+
const steps = Array.isArray(todo.steps) ? todo.steps : []
|
|
561
|
+
if (steps.length > 0) {
|
|
562
|
+
lines.push('', `步骤(共 ${steps.length} 步):`)
|
|
563
|
+
steps.forEach((step, index) => {
|
|
564
|
+
lines.push(`${index + 1}. ${step.done === true ? '[已完成] ' : ''}${step.text}`)
|
|
565
|
+
})
|
|
566
|
+
}
|
|
558
567
|
if (todo.links.length > 0) {
|
|
559
568
|
lines.push('', '相关文档与地址:')
|
|
560
569
|
for (const link of todo.links) {
|
package/lib/tools.js
CHANGED
|
@@ -20,6 +20,34 @@ import { LIMITS, PRIORITIES, PRIORITY_LABELS, STATUSES } from './model.js'
|
|
|
20
20
|
|
|
21
21
|
const text = (value) => [{ type: 'text', text: value }]
|
|
22
22
|
|
|
23
|
+
const STEPS_INPUT = {
|
|
24
|
+
type: 'array',
|
|
25
|
+
description:
|
|
26
|
+
'要做成的几步(可选,最多 ' + LIMITS.steps + ' 条)。每步一句话;已完成的写 { text, done: true }。'
|
|
27
|
+
+ '派发时会把它们作为编号计划交给 Agent。',
|
|
28
|
+
items: {
|
|
29
|
+
oneOf: [
|
|
30
|
+
{ type: 'string' },
|
|
31
|
+
{
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: { text: { type: 'string' }, done: { type: 'boolean' } },
|
|
34
|
+
required: ['text'],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STEPS_SHAPE = {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: { text: { type: 'string' }, done: { type: 'boolean' } },
|
|
46
|
+
required: ['text'],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
const LINKS_INPUT = {
|
|
24
52
|
type: 'array',
|
|
25
53
|
description:
|
|
@@ -92,6 +120,7 @@ const TODO_SHAPE = { type: 'object',
|
|
|
92
120
|
priority: { type: 'string', enum: [...PRIORITIES] },
|
|
93
121
|
dueAt: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
94
122
|
tags: { type: 'array', items: { type: 'string' } },
|
|
123
|
+
steps: STEPS_SHAPE,
|
|
95
124
|
links: {
|
|
96
125
|
type: 'array',
|
|
97
126
|
items: {
|
|
@@ -152,6 +181,7 @@ export function presentTodo(todo, now = Date.now()) {
|
|
|
152
181
|
priority: todo.priority,
|
|
153
182
|
dueAt: todo.dueAt,
|
|
154
183
|
tags: [...todo.tags],
|
|
184
|
+
steps: (todo.steps ?? []).map((step) => ({ text: step.text, done: step.done === true })),
|
|
155
185
|
links: todo.links.map((link) => ({ ...link })),
|
|
156
186
|
createdAt: todo.createdAt,
|
|
157
187
|
createdAtIso: new Date(todo.createdAt).toISOString(),
|
|
@@ -301,6 +331,7 @@ export function createTools(service, options = {}) {
|
|
|
301
331
|
title: { type: 'string', description: `一句话说清要做什么(最多 ${LIMITS.title} 字)` },
|
|
302
332
|
description: { type: 'string', description: '补充说明、验收标准、已知的坑' },
|
|
303
333
|
links: LINKS_INPUT,
|
|
334
|
+
steps: STEPS_INPUT,
|
|
304
335
|
tags: { type: 'array', items: { type: 'string' }, description: `标签,最多 ${LIMITS.tags} 个` },
|
|
305
336
|
priority: { type: 'string', enum: [...PRIORITIES], description: 'P0 紧急 / P1 高 / P2 常规(缺省)/ P3 低' },
|
|
306
337
|
dueAt: { type: 'string', description: 'ISO-8601 截止时间,例如 2026-10-20T10:00 或 2026-10-20' },
|
|
@@ -528,7 +559,7 @@ export function createTools(service, options = {}) {
|
|
|
528
559
|
{
|
|
529
560
|
name: 'global_todo_update',
|
|
530
561
|
description:
|
|
531
|
-
'改一条全局待办。只传要改的字段;`dueAt: ""` 清空截止时间,`tags: []` 清空标签,`links` 是整体替换(要单独加附件请用 global_todo_link),`status` 可以设为 archived(归档,不删除)。',
|
|
562
|
+
'改一条全局待办。只传要改的字段;`dueAt: ""` 清空截止时间,`tags: []` 清空标签,`links` / `steps` 是整体替换(要单独加附件请用 global_todo_link),`status` 可以设为 archived(归档,不删除)。',
|
|
532
563
|
parameters: {
|
|
533
564
|
type: 'object',
|
|
534
565
|
properties: {
|
|
@@ -542,6 +573,7 @@ export function createTools(service, options = {}) {
|
|
|
542
573
|
dueAt: { oneOf: [{ type: 'string' }, { type: 'null' }], description: '空串或 null 表示清空' },
|
|
543
574
|
tags: { type: 'array', items: { type: 'string' } },
|
|
544
575
|
links: LINKS_INPUT,
|
|
576
|
+
steps: STEPS_INPUT,
|
|
545
577
|
},
|
|
546
578
|
required: ['id'],
|
|
547
579
|
additionalProperties: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-global-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"icon": "./docs/icon.svg",
|
|
5
5
|
"description": "Global, cross-session todos for DeepSeek Harness: one list owned by the human and the agent together, with creation time, attached documents and links, and one-click hand-off to an agent session. 全局待办:跨会话一份清单,记录加入时间,可挂文档与地址,可一键交给 Agent 继续干。",
|
|
6
6
|
"type": "module",
|