dsh-global-todo 0.6.6 → 0.6.8
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 +30 -0
- package/README.md +11 -0
- package/README.zh.md +10 -0
- package/docs/screenshots/panel-detail.png +0 -0
- package/docs/screenshots/panel-empty.png +0 -0
- package/docs/screenshots/panel-filtered.png +0 -0
- package/docs/screenshots/panel-inbox.png +0 -0
- package/docs/screenshots/panel-reminders.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 +79 -7
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## [0.6.8] — 2026-10-20
|
|
2
|
+
|
|
3
|
+
Every conditional state now has a baseline image, and the harness refuses to render a blank
|
|
4
|
+
one.
|
|
5
|
+
|
|
6
|
+
- **Four new preview scenarios**: empty list, filter with no match, inbox, and a due reminder.
|
|
7
|
+
`tools/preview.mjs` grew a `SCENARIOS` table the fake fetch reads, so a state that used to be
|
|
8
|
+
reachable only by clicking can be rendered and photographed like any other.
|
|
9
|
+
- **`npm run screenshots` writes eight images** (list, detail, selection, settings + the four
|
|
10
|
+
above). Both READMEs index them in a state table instead of embedding all eight.
|
|
11
|
+
- **`npm run check:render` asserts each state's marker** (`记第一条`, `清除筛选`, the inbox
|
|
12
|
+
explanation, the reminder title), so a regression shows up as a failing check rather than a
|
|
13
|
+
silently blank picture.
|
|
14
|
+
|
|
15
|
+
## [0.6.7] — 2026-10-20
|
|
16
|
+
|
|
17
|
+
Copy safety, and the last orphan of the old footer.
|
|
18
|
+
|
|
19
|
+
- **A test now guards the dictionaries.** `test/locale.test.mjs` reads them as source text
|
|
20
|
+
(a duplicate key is legal JavaScript and invisible at runtime) and fails on: a key defined
|
|
21
|
+
twice, a key present in one language and missing in the other, full-width punctuation inside
|
|
22
|
+
English copy, and a literal `t('…')` key that does not exist. It immediately found a real
|
|
23
|
+
duplicate — `select.clear` — the same class of bug that shipped "more fields" on the overflow
|
|
24
|
+
menu.
|
|
25
|
+
- **The duplicate is fixed properly**: the selection bar clears with its own key (`清空`/`Clear`)
|
|
26
|
+
while the pick toggle and the two ask-bars share `清除`/`取消`.
|
|
27
|
+
- **Keyboard shortcuts get a home.** The hints left the footer in 0.6.0 and only survived in the
|
|
28
|
+
empty state; they now open from `⋯ → Keyboard shortcuts` as a small card with `kbd` chips, and
|
|
29
|
+
Escape closes it before anything else.
|
|
30
|
+
|
|
1
31
|
## [0.6.6] — 2026-10-20
|
|
2
32
|
|
|
3
33
|
Both forms now look like forms.
|
package/README.md
CHANGED
|
@@ -35,6 +35,17 @@ one record:
|
|
|
35
35
|
|
|
36
36
|

|
|
37
37
|
|
|
38
|
+
Every state that only appears under a condition has a fixed baseline image, rendered by the same
|
|
39
|
+
preview and photographed by the same tool:
|
|
40
|
+
|
|
41
|
+
| State | Image |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Empty list | [`panel-empty.png`](docs/screenshots/panel-empty.png) |
|
|
44
|
+
| Filter with no match | [`panel-filtered.png`](docs/screenshots/panel-filtered.png) |
|
|
45
|
+
| Inbox (awaiting confirmation) | [`panel-inbox.png`](docs/screenshots/panel-inbox.png) |
|
|
46
|
+
| Due reminder | [`panel-reminders.png`](docs/screenshots/panel-reminders.png) |
|
|
47
|
+
| Selection mode | [`panel-select.png`](docs/screenshots/panel-select.png) |
|
|
48
|
+
|
|
38
49
|
These are produced by `npm run screenshots` (which chains `npm run preview`): the real client
|
|
39
50
|
half is rendered to static HTML with no Host and no dependencies, then photographed at 1280×720
|
|
40
51
|
into `docs/screenshots/`. Playwright is not a dependency — the tool borrows the copy a DSH
|
package/README.zh.md
CHANGED
|
@@ -32,6 +32,16 @@ DSH 自带的 `todo_write` 属于**某一个 agent 会话**:整表替换、三
|
|
|
32
32
|
|
|
33
33
|

|
|
34
34
|
|
|
35
|
+
每个「只在特定情况下出现」的面板状态都有一张固定基线图,由同一套预览渲染、同一支工具拍摄,改样式时可以直接对比:
|
|
36
|
+
|
|
37
|
+
| 状态 | 图片 |
|
|
38
|
+
|---|---|
|
|
39
|
+
| 清单为空 | [`panel-empty.png`](docs/screenshots/panel-empty.png) |
|
|
40
|
+
| 筛选无结果 | [`panel-filtered.png`](docs/screenshots/panel-filtered.png) |
|
|
41
|
+
| 收件箱(待确认) | [`panel-inbox.png`](docs/screenshots/panel-inbox.png) |
|
|
42
|
+
| 到点提醒 | [`panel-reminders.png`](docs/screenshots/panel-reminders.png) |
|
|
43
|
+
| 批量选择 | [`panel-select.png`](docs/screenshots/panel-select.png) |
|
|
44
|
+
|
|
35
45
|
图都由 `npm run screenshots`(内部串起 `npm run preview`)生成:先把真实的客户端半区渲染成静态
|
|
36
46
|
HTML(不依赖宿主、零依赖),再以 1280×720 拍进 `docs/screenshots/`。Playwright 不是依赖——工具
|
|
37
47
|
借的是 DSH profile 里已经装好的那份,或 `DSH_PLAYWRIGHT` 指向的那份。
|
|
Binary file
|
|
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 = 'ui44'
|
|
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
|
|
|
@@ -106,6 +106,11 @@ window.__ModuleLoader__.load({
|
|
|
106
106
|
'settings.cancel': '取消',
|
|
107
107
|
'settings.saved': '设置已保存',
|
|
108
108
|
'settings.about': '关于',
|
|
109
|
+
'action.keys': '快捷键',
|
|
110
|
+
'keys.search': '聚焦搜索',
|
|
111
|
+
'keys.new': '开始记一条',
|
|
112
|
+
'keys.save': '保存这条(在标题里)',
|
|
113
|
+
'keys.close': '收起最内层浮层',
|
|
109
114
|
'group.actions': '分组操作',
|
|
110
115
|
'group.capture': '从本对话抓取',
|
|
111
116
|
'group.exemptOn': '本工作区免打扰',
|
|
@@ -139,7 +144,7 @@ window.__ModuleLoader__.load({
|
|
|
139
144
|
'notice.bulkPartial': '已更新 {n} 条,跳过 {m} 条',
|
|
140
145
|
'select.clear': '清除',
|
|
141
146
|
'select.done': '退出选择',
|
|
142
|
-
'select.
|
|
147
|
+
'select.clearAll': '清空',
|
|
143
148
|
'select.hint': '先勾几条待办,再点「一键分配」:优先回到写下它的那个对话,其次同工作区的对话,都找不到才让你选。',
|
|
144
149
|
'assign.working': '正在分配…',
|
|
145
150
|
'assign.summary': '已分配 {n} 条(原对话 {origin} · 同工作区 {workspace} · 你选的 {chosen})',
|
|
@@ -367,6 +372,11 @@ window.__ModuleLoader__.load({
|
|
|
367
372
|
'settings.cancel': 'Cancel',
|
|
368
373
|
'settings.saved': 'Settings saved',
|
|
369
374
|
'settings.about': 'About',
|
|
375
|
+
'action.keys': 'Keyboard shortcuts',
|
|
376
|
+
'keys.search': 'Focus the search box',
|
|
377
|
+
'keys.new': 'Start a new todo',
|
|
378
|
+
'keys.save': 'Save it (from the title field)',
|
|
379
|
+
'keys.close': 'Close the innermost layer',
|
|
370
380
|
'group.actions': 'Group actions',
|
|
371
381
|
'group.capture': 'Collect from this conversation',
|
|
372
382
|
'group.exemptOn': 'Mute this workspace',
|
|
@@ -400,6 +410,7 @@ window.__ModuleLoader__.load({
|
|
|
400
410
|
'notice.bulkPartial': 'Updated {n}, skipped {m}',
|
|
401
411
|
'select.clear': 'Clear',
|
|
402
412
|
'select.done': 'Exit selection',
|
|
413
|
+
'select.clearAll': 'Clear',
|
|
403
414
|
'select.hint': 'Pick a few todos, then Assign: back to the conversation that wrote each one, else a conversation in the same workspace, else you choose.',
|
|
404
415
|
'assign.working': 'Assigning…',
|
|
405
416
|
'assign.summary': 'Assigned {n} ({origin} to origin · {workspace} same workspace · {chosen} chosen)',
|
|
@@ -1169,6 +1180,9 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1169
1180
|
.gt-about-row { display: grid; grid-template-columns: 88px minmax(0, 1fr); align-items: baseline; gap: 8px; }
|
|
1170
1181
|
.gt-settings-foot { display: flex; flex: 0 0 auto; justify-content: flex-end; gap: 8px;
|
|
1171
1182
|
padding: 12px 16px; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
1183
|
+
.gt-keys { width: 320px; }
|
|
1184
|
+
.gt-key-row { display: flex; align-items: center; gap: 10px; }
|
|
1185
|
+
.gt-key-row .gt-kbd { min-width: 44px; text-align: center; }
|
|
1172
1186
|
.gt-group-select { display: none; min-height: 28px; max-width: 150px; padding: 0 8px; font: inherit;
|
|
1173
1187
|
font-size: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1);
|
|
1174
1188
|
border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; }
|
|
@@ -2239,7 +2253,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2239
2253
|
const [state, setState] = useState({ todos: [], counts: null, health: null, matched: 0, truncated: false })
|
|
2240
2254
|
const [loading, setLoading] = useState(true)
|
|
2241
2255
|
const [error, setError] = useState(null)
|
|
2242
|
-
|
|
2256
|
+
// The static preview asks for a starting view it cannot click its way into.
|
|
2257
|
+
const [filter, setFilter] = useState(typeof props.initialFilter === 'string' ? props.initialFilter : 'active')
|
|
2243
2258
|
const [query, setQuery] = useState('')
|
|
2244
2259
|
// Multi-select for batch assignment. A separate mode, because the round control on
|
|
2245
2260
|
// each row already means "done".
|
|
@@ -2279,6 +2294,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2279
2294
|
// One settings popover: reminders and the external door both need a place a person can
|
|
2280
2295
|
// reach without curl.
|
|
2281
2296
|
const [settingsOpen, setSettingsOpen] = useState(initialSettingsOpen)
|
|
2297
|
+
const [keysOpen, setKeysOpen] = useState(false)
|
|
2282
2298
|
const [settingsBox, setSettingsBox] = useState(null)
|
|
2283
2299
|
// The detail card's breakpoint follows the panel's own width, not the viewport: the
|
|
2284
2300
|
// slot is ~265px narrower than the window, so a viewport query opens the two-column
|
|
@@ -2637,6 +2653,10 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2637
2653
|
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable === true)
|
|
2638
2654
|
if (event.key === 'Escape') {
|
|
2639
2655
|
if (typing && typeof target.blur === 'function') target.blur()
|
|
2656
|
+
if (keysOpen) {
|
|
2657
|
+
setKeysOpen(false)
|
|
2658
|
+
return
|
|
2659
|
+
}
|
|
2640
2660
|
if (settingsOpen) {
|
|
2641
2661
|
setSettingsOpen(false)
|
|
2642
2662
|
return
|
|
@@ -2659,7 +2679,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2659
2679
|
}
|
|
2660
2680
|
document.addEventListener('keydown', onKey)
|
|
2661
2681
|
return () => document.removeEventListener('keydown', onKey)
|
|
2662
|
-
}, [settingsOpen, adding])
|
|
2682
|
+
}, [settingsOpen, keysOpen, adding])
|
|
2663
2683
|
|
|
2664
2684
|
// The settings card floats over the list, so a click anywhere else dismisses it
|
|
2665
2685
|
// the way every other popover on the web does.
|
|
@@ -3389,6 +3409,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3389
3409
|
{ key: 'export', label: t('action.export'), icon: 'doc', onSelect: () => void exportJson() },
|
|
3390
3410
|
{ key: 'import', label: t('action.import'), icon: 'plus', onSelect: () => importRef.current?.click?.() },
|
|
3391
3411
|
{ key: 'settings', label: t('settings.title'), icon: 'gear', onSelect: () => openSettings() },
|
|
3412
|
+
{ key: 'keys', label: t('action.keys'), icon: 'kebab', onSelect: () => setKeysOpen(true) },
|
|
3392
3413
|
],
|
|
3393
3414
|
}),
|
|
3394
3415
|
),
|
|
@@ -3554,6 +3575,57 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3554
3575
|
)
|
|
3555
3576
|
: null,
|
|
3556
3577
|
|
|
3578
|
+
// The keyboard hints used to sit in a footer nobody read; they belong where the
|
|
3579
|
+
// other occasional things live, one menu away.
|
|
3580
|
+
keysOpen
|
|
3581
|
+
? h(
|
|
3582
|
+
'div',
|
|
3583
|
+
{
|
|
3584
|
+
ref: settingsRef,
|
|
3585
|
+
className: 'gt-settings gt-keys',
|
|
3586
|
+
role: 'dialog',
|
|
3587
|
+
'aria-label': t('action.keys'),
|
|
3588
|
+
style:
|
|
3589
|
+
settingsBox === null
|
|
3590
|
+
? undefined
|
|
3591
|
+
: {
|
|
3592
|
+
position: 'fixed',
|
|
3593
|
+
top: `${settingsBox.top}px`,
|
|
3594
|
+
right: `${settingsBox.right}px`,
|
|
3595
|
+
maxHeight: `calc(100dvh - ${settingsBox.top + 20}px)`,
|
|
3596
|
+
zIndex: 40,
|
|
3597
|
+
},
|
|
3598
|
+
},
|
|
3599
|
+
h(
|
|
3600
|
+
'div',
|
|
3601
|
+
{ className: 'gt-settings-head' },
|
|
3602
|
+
h('span', { className: 'gt-settings-title' }, t('action.keys')),
|
|
3603
|
+
h(
|
|
3604
|
+
'button',
|
|
3605
|
+
{ type: 'button', className: 'gt-iconbtn', title: t('action.hide'), 'aria-label': t('action.hide'), onClick: () => setKeysOpen(false) },
|
|
3606
|
+
h(Icon, { name: 'close', size: 13 }),
|
|
3607
|
+
),
|
|
3608
|
+
),
|
|
3609
|
+
h(
|
|
3610
|
+
'div',
|
|
3611
|
+
{ className: 'gt-settings-body' },
|
|
3612
|
+
[
|
|
3613
|
+
['/', t('keys.search')],
|
|
3614
|
+
['n', t('keys.new')],
|
|
3615
|
+
['Enter', t('keys.save')],
|
|
3616
|
+
['Esc', t('keys.close')],
|
|
3617
|
+
].map(([combo, sentence]) =>
|
|
3618
|
+
h(
|
|
3619
|
+
'div',
|
|
3620
|
+
{ className: 'gt-key-row', key: combo },
|
|
3621
|
+
h('span', { className: 'gt-kbd' }, combo),
|
|
3622
|
+
h('span', { className: 'gt-hint' }, sentence),
|
|
3623
|
+
),
|
|
3624
|
+
),
|
|
3625
|
+
),
|
|
3626
|
+
)
|
|
3627
|
+
: null,
|
|
3628
|
+
|
|
3557
3629
|
error !== null
|
|
3558
3630
|
? h(
|
|
3559
3631
|
'div',
|
|
@@ -3949,7 +4021,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3949
4021
|
h(
|
|
3950
4022
|
'button',
|
|
3951
4023
|
{ type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setSelected({}) },
|
|
3952
|
-
t('select.
|
|
4024
|
+
t('select.clearAll'),
|
|
3953
4025
|
),
|
|
3954
4026
|
h('span', { className: 'gt-spacer' }),
|
|
3955
4027
|
h(
|
|
@@ -4112,7 +4184,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
4112
4184
|
h(
|
|
4113
4185
|
'button',
|
|
4114
4186
|
{ type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setAssignAsk(null) },
|
|
4115
|
-
t('
|
|
4187
|
+
t('action.cancel'),
|
|
4116
4188
|
),
|
|
4117
4189
|
),
|
|
4118
4190
|
|
|
@@ -4138,7 +4210,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
4138
4210
|
h(
|
|
4139
4211
|
'button',
|
|
4140
4212
|
{ type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setImportAsk(null) },
|
|
4141
|
-
t('
|
|
4213
|
+
t('action.cancel'),
|
|
4142
4214
|
),
|
|
4143
4215
|
),
|
|
4144
4216
|
|
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.6.
|
|
39
|
+
export const VERSION = '0.6.8'
|
|
40
40
|
|
|
41
41
|
/** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
|
|
42
42
|
export const name = 'dsh-global-todo'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-global-todo",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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",
|