dsh-global-todo 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.6.7] — 2026-10-20
2
+
3
+ Copy safety, and the last orphan of the old footer.
4
+
5
+ - **A test now guards the dictionaries.** `test/locale.test.mjs` reads them as source text
6
+ (a duplicate key is legal JavaScript and invisible at runtime) and fails on: a key defined
7
+ twice, a key present in one language and missing in the other, full-width punctuation inside
8
+ English copy, and a literal `t('…')` key that does not exist. It immediately found a real
9
+ duplicate — `select.clear` — the same class of bug that shipped "more fields" on the overflow
10
+ menu.
11
+ - **The duplicate is fixed properly**: the selection bar clears with its own key (`清空`/`Clear`)
12
+ while the pick toggle and the two ask-bars share `清除`/`取消`.
13
+ - **Keyboard shortcuts get a home.** The hints left the footer in 0.6.0 and only survived in the
14
+ empty state; they now open from `⋯ → Keyboard shortcuts` as a small card with `kbd` chips, and
15
+ Escape closes it before anything else.
16
+
1
17
  ## [0.6.6] — 2026-10-20
2
18
 
3
19
  Both forms now look like forms.
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 = 'ui42'
53
+ const UI_REV = 'ui43'
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.clear': '清空',
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; }
@@ -2279,6 +2293,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2279
2293
  // One settings popover: reminders and the external door both need a place a person can
2280
2294
  // reach without curl.
2281
2295
  const [settingsOpen, setSettingsOpen] = useState(initialSettingsOpen)
2296
+ const [keysOpen, setKeysOpen] = useState(false)
2282
2297
  const [settingsBox, setSettingsBox] = useState(null)
2283
2298
  // The detail card's breakpoint follows the panel's own width, not the viewport: the
2284
2299
  // slot is ~265px narrower than the window, so a viewport query opens the two-column
@@ -2637,6 +2652,10 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2637
2652
  (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable === true)
2638
2653
  if (event.key === 'Escape') {
2639
2654
  if (typing && typeof target.blur === 'function') target.blur()
2655
+ if (keysOpen) {
2656
+ setKeysOpen(false)
2657
+ return
2658
+ }
2640
2659
  if (settingsOpen) {
2641
2660
  setSettingsOpen(false)
2642
2661
  return
@@ -2659,7 +2678,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2659
2678
  }
2660
2679
  document.addEventListener('keydown', onKey)
2661
2680
  return () => document.removeEventListener('keydown', onKey)
2662
- }, [settingsOpen, adding])
2681
+ }, [settingsOpen, keysOpen, adding])
2663
2682
 
2664
2683
  // The settings card floats over the list, so a click anywhere else dismisses it
2665
2684
  // the way every other popover on the web does.
@@ -3389,6 +3408,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3389
3408
  { key: 'export', label: t('action.export'), icon: 'doc', onSelect: () => void exportJson() },
3390
3409
  { key: 'import', label: t('action.import'), icon: 'plus', onSelect: () => importRef.current?.click?.() },
3391
3410
  { key: 'settings', label: t('settings.title'), icon: 'gear', onSelect: () => openSettings() },
3411
+ { key: 'keys', label: t('action.keys'), icon: 'kebab', onSelect: () => setKeysOpen(true) },
3392
3412
  ],
3393
3413
  }),
3394
3414
  ),
@@ -3554,6 +3574,57 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3554
3574
  )
3555
3575
  : null,
3556
3576
 
3577
+ // The keyboard hints used to sit in a footer nobody read; they belong where the
3578
+ // other occasional things live, one menu away.
3579
+ keysOpen
3580
+ ? h(
3581
+ 'div',
3582
+ {
3583
+ ref: settingsRef,
3584
+ className: 'gt-settings gt-keys',
3585
+ role: 'dialog',
3586
+ 'aria-label': t('action.keys'),
3587
+ style:
3588
+ settingsBox === null
3589
+ ? undefined
3590
+ : {
3591
+ position: 'fixed',
3592
+ top: `${settingsBox.top}px`,
3593
+ right: `${settingsBox.right}px`,
3594
+ maxHeight: `calc(100dvh - ${settingsBox.top + 20}px)`,
3595
+ zIndex: 40,
3596
+ },
3597
+ },
3598
+ h(
3599
+ 'div',
3600
+ { className: 'gt-settings-head' },
3601
+ h('span', { className: 'gt-settings-title' }, t('action.keys')),
3602
+ h(
3603
+ 'button',
3604
+ { type: 'button', className: 'gt-iconbtn', title: t('action.hide'), 'aria-label': t('action.hide'), onClick: () => setKeysOpen(false) },
3605
+ h(Icon, { name: 'close', size: 13 }),
3606
+ ),
3607
+ ),
3608
+ h(
3609
+ 'div',
3610
+ { className: 'gt-settings-body' },
3611
+ [
3612
+ ['/', t('keys.search')],
3613
+ ['n', t('keys.new')],
3614
+ ['Enter', t('keys.save')],
3615
+ ['Esc', t('keys.close')],
3616
+ ].map(([combo, sentence]) =>
3617
+ h(
3618
+ 'div',
3619
+ { className: 'gt-key-row', key: combo },
3620
+ h('span', { className: 'gt-kbd' }, combo),
3621
+ h('span', { className: 'gt-hint' }, sentence),
3622
+ ),
3623
+ ),
3624
+ ),
3625
+ )
3626
+ : null,
3627
+
3557
3628
  error !== null
3558
3629
  ? h(
3559
3630
  'div',
@@ -3949,7 +4020,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3949
4020
  h(
3950
4021
  'button',
3951
4022
  { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setSelected({}) },
3952
- t('select.clear'),
4023
+ t('select.clearAll'),
3953
4024
  ),
3954
4025
  h('span', { className: 'gt-spacer' }),
3955
4026
  h(
@@ -4112,7 +4183,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
4112
4183
  h(
4113
4184
  'button',
4114
4185
  { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setAssignAsk(null) },
4115
- t('select.clear'),
4186
+ t('action.cancel'),
4116
4187
  ),
4117
4188
  ),
4118
4189
 
@@ -4138,7 +4209,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
4138
4209
  h(
4139
4210
  'button',
4140
4211
  { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setImportAsk(null) },
4141
- t('select.clear'),
4212
+ t('action.cancel'),
4142
4213
  ),
4143
4214
  ),
4144
4215
 
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.6'
39
+ export const VERSION = '0.6.7'
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.6",
3
+ "version": "0.6.7",
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",