dsh-global-todo 0.5.3 → 0.6.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 CHANGED
@@ -1,3 +1,25 @@
1
+ ## [0.6.0] — 2026-10-20
2
+
3
+ A design pass over the whole panel: one list, fewer competing controls, one type scale.
4
+
5
+ - **The layout is a column, not a wrapping row.** `.gt-root`/`.gt-inner` gained `min-width: 0` so the
6
+ column shrinks with its slot instead of overflowing at 1280 and 1100; the header wraps its actions
7
+ and the toolbar is two semantic rows (filter/search above, context above the list). The working
8
+ directory, handoff target and browse controls move behind a single Context toggle, collapsed by
9
+ default.
10
+ - **Five type sizes and one weight scale.** 20/28 panel title, 14/22 row title, 13/20 controls and
11
+ body, 12/18 metadata and section labels, 11.5 mono for paths. Buttons, inputs and tabs share a
12
+ 28px control height; the settings card is a 340px floating inspector (fixed, mutually exclusive
13
+ with the detail card) instead of an in-flow block that pushed the list down.
14
+ - **Rows read top-down.** The title opens the detail (the kebab remains as a redundant door); the
15
+ dispatch button is a quiet action instead of an outlined primary; metadata loses the origin path
16
+ and the stray separator (CSS-generated dots), and tags cap at four. Group-level priority hides
17
+ while the group is expanded, where every row already shows it.
18
+ - **Copy and states.** Full-width colons no longer leak into the English UI, `1 items` is fixed,
19
+ priority chips say Urgent/High/Normal/Low instead of P-codes, the empty state has a primary
20
+ action, the error banner can be dismissed, and the reminder bar keeps its accent on a 2px edge
21
+ (amber text failed contrast). The footer's store path and version move into Settings → About.
22
+
1
23
  ## [0.5.3] — 2026-10-20
2
24
 
3
25
  Settings a person can reach, and reminders that behave like reminders.
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 = 'ui35'
53
+ const UI_REV = 'ui36'
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
 
@@ -64,6 +64,7 @@ window.__ModuleLoader__.load({
64
64
  'action.retry': '重试',
65
65
  'action.add': '添加',
66
66
  'action.more': '更多字段',
67
+ 'action.overflow': '更多操作',
67
68
  'action.less': '收起字段',
68
69
  'action.detail': '详情',
69
70
  'action.hide': '收起',
@@ -91,6 +92,7 @@ window.__ModuleLoader__.load({
91
92
  'action.exportHint': '把整份清单导出成 JSON 文件(就是存储用的那份文档)',
92
93
  'action.importHint': '从 JSON 文件导入:同 id 覆盖、新 id 追加,不删除现有待办',
93
94
  'settings.title': '设置',
95
+ 'settings.context': '上下文',
94
96
  'settings.reminders': '到点提醒',
95
97
  'settings.remindersEnable': '开启到点提醒',
96
98
  'settings.lead': '提前(分钟)',
@@ -103,6 +105,7 @@ window.__ModuleLoader__.load({
103
105
  'settings.save': '保存',
104
106
  'settings.cancel': '取消',
105
107
  'settings.saved': '设置已保存',
108
+ 'settings.about': '关于',
106
109
  'notice.exported': '已导出 {n} 条待办',
107
110
  'notice.imported': '已导入 {n} 条(合并,现有共 {total} 条)',
108
111
  'import.badJson': '这个文件不是合法的 JSON',
@@ -124,7 +127,7 @@ window.__ModuleLoader__.load({
124
127
  'notice.bulk': '已更新 {n} 条',
125
128
  'notice.bulkPartial': '已更新 {n} 条,跳过 {m} 条',
126
129
  'select.clear': '清除',
127
- 'select.done': '完成',
130
+ 'select.done': '退出选择',
128
131
  'select.hint': '先勾几条待办,再点「一键分配」:优先回到写下它的那个对话,其次同工作区的对话,都找不到才让你选。',
129
132
  'assign.working': '正在分配…',
130
133
  'assign.summary': '已分配 {n} 条(原对话 {origin} · 同工作区 {workspace} · 你选的 {chosen})',
@@ -218,10 +221,10 @@ window.__ModuleLoader__.load({
218
221
  'field.linkValue': '文档绝对路径或网址',
219
222
  'field.linkTitle': '给它起个短名字(可空)',
220
223
  'field.search': '搜标题、描述、标签、附件…',
221
- 'priority.low': 'P3 低',
222
- 'priority.normal': 'P2 常规',
223
- 'priority.high': 'P1 高',
224
- 'priority.urgent': 'P0 紧急',
224
+ 'priority.low': '低',
225
+ 'priority.normal': '常规',
226
+ 'priority.high': '高',
227
+ 'priority.urgent': '紧急',
225
228
  'group.by': '分组',
226
229
  'group.conversation': '对话',
227
230
  'group.time': '时间',
@@ -229,6 +232,7 @@ window.__ModuleLoader__.load({
229
232
  'group.manual': '手动添加',
230
233
  'group.other': '其他',
231
234
  'group.items': '{n} 条',
235
+ 'group.item': '1 条',
232
236
  'group.thisConversation': '本对话',
233
237
  'group.overdue': '逾期',
234
238
  'group.today': '今天',
@@ -238,6 +242,7 @@ window.__ModuleLoader__.load({
238
242
  'group.done': '已完成',
239
243
  'group.cancelled': '已取消',
240
244
  'empty.none': '还没有待办。上面写一句,回车就记下了。',
245
+ 'empty.add': '记第一条',
241
246
  'empty.filtered': '这个筛选下没有待办。',
242
247
  'meta.added': '加入于',
243
248
  'meta.due': '截止',
@@ -300,6 +305,7 @@ window.__ModuleLoader__.load({
300
305
  'action.retry': 'Retry',
301
306
  'action.add': 'Add',
302
307
  'action.more': 'More fields',
308
+ 'action.overflow': 'More actions',
303
309
  'action.less': 'Fewer fields',
304
310
  'action.detail': 'Details',
305
311
  'action.hide': 'Hide',
@@ -327,6 +333,7 @@ window.__ModuleLoader__.load({
327
333
  'action.exportHint': 'Download the whole list as JSON (the document the store keeps)',
328
334
  'action.importHint': 'Import a JSON file: same ids are updated, new ids appended, nothing deleted',
329
335
  'settings.title': 'Settings',
336
+ 'settings.context': 'Context',
330
337
  'settings.reminders': 'Due reminders',
331
338
  'settings.remindersEnable': 'Announce deadlines',
332
339
  'settings.lead': 'Lead time (min)',
@@ -339,6 +346,7 @@ window.__ModuleLoader__.load({
339
346
  'settings.save': 'Save',
340
347
  'settings.cancel': 'Cancel',
341
348
  'settings.saved': 'Settings saved',
349
+ 'settings.about': 'About',
342
350
  'notice.exported': 'Exported {n} todos',
343
351
  'notice.imported': 'Imported {n} (merged; {total} in the list now)',
344
352
  'import.badJson': 'That file is not valid JSON',
@@ -456,16 +464,17 @@ window.__ModuleLoader__.load({
456
464
  'field.linkValue': 'Absolute document path or URL',
457
465
  'field.linkTitle': 'Short label (optional)',
458
466
  'field.search': 'Search title, description, tags, attachments…',
459
- 'priority.low': 'P3 Low',
460
- 'priority.normal': 'P2 Normal',
461
- 'priority.high': 'P1 High',
462
- 'priority.urgent': 'P0 Urgent',
467
+ 'priority.low': 'Low',
468
+ 'priority.normal': 'Normal',
469
+ 'priority.high': 'High',
470
+ 'priority.urgent': 'Urgent',
463
471
  'group.by': 'Group',
464
472
  'group.time': 'Time',
465
473
  'group.level': 'Level',
466
474
  'group.manual': 'Added by hand',
467
475
  'group.other': 'Other',
468
476
  'group.items': '{n} items',
477
+ 'group.item': '1 item',
469
478
  'group.thisConversation': 'this conversation',
470
479
  'group.overdue': 'Overdue',
471
480
  'group.today': 'Today',
@@ -475,6 +484,7 @@ window.__ModuleLoader__.load({
475
484
  'group.done': 'Done',
476
485
  'group.cancelled': 'Cancelled',
477
486
  'empty.none': 'No todos yet. Write one above and press Enter.',
487
+ 'empty.add': 'Add your first todo',
478
488
  'empty.filtered': 'Nothing matches this filter.',
479
489
  'meta.added': 'added',
480
490
  'meta.due': 'due',
@@ -871,6 +881,17 @@ window.__ModuleLoader__.load({
871
881
  )
872
882
  }
873
883
  if (props.name === 'clock') return h('svg', common, h('circle', { cx: 8, cy: 8, r: 5.6 }), h('path', { d: 'M8 4.8V8l2.2 1.4' }))
884
+ if (props.name === 'close') return h('svg', common, h('path', { d: 'M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6' }))
885
+ if (props.name === 'gear') {
886
+ return h(
887
+ 'svg',
888
+ common,
889
+ h('path', { d: 'M2.6 4.4h10.8M2.6 8h10.8M2.6 11.6h10.8' }),
890
+ h('circle', { cx: 6, cy: 4.4, r: 1.5, fill: 'currentColor', stroke: 'none' }),
891
+ h('circle', { cx: 10, cy: 8, r: 1.5, fill: 'currentColor', stroke: 'none' }),
892
+ h('circle', { cx: 5.2, cy: 11.6, r: 1.5, fill: 'currentColor', stroke: 'none' }),
893
+ )
894
+ }
874
895
  if (props.name === 'chevron') return h('svg', common, h('path', { d: 'M5.6 3.8L10 8l-4.4 4.2' }))
875
896
  if (props.name === 'flag') return h('svg', common, h('path', { d: 'M4 14V2.6h7l-1.2 2.8L11 8.2H4' }))
876
897
  if (props.name === 'mute') {
@@ -887,25 +908,29 @@ window.__ModuleLoader__.load({
887
908
  // ------------------------------------------------------------------ 样式
888
909
 
889
910
  const CSS = `
890
- .gt-root { display: flex; flex-direction: column; height: 100%; min-height: 0;
891
- color: var(--dsw-alias-label-primary); font-size: 13px; }
892
- .gt-inner { display: flex; flex-direction: column; gap: 12px; width: 100%; max-width: 980px;
893
- margin: 0 auto; padding: 16px 20px 28px; min-height: 0; flex: 1; }
894
- .gt-head { display: flex; align-items: flex-start; gap: 12px; }
911
+ .gt-root { display: flex; flex-direction: column; height: 100%; min-height: 0; min-width: 0;
912
+ color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.5; }
913
+ .gt-inner { display: flex; flex-direction: column; gap: 14px; width: 100%; max-width: 1040px; min-width: 0;
914
+ box-sizing: border-box; margin: 0 auto; padding: 20px 24px 32px; flex: 1; }
915
+ .gt-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
916
+ .gt-head-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; flex: 0 0 auto; }
895
917
  .gt-headline { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
896
- .gt-title { font-size: 17px; font-weight: 650; letter-spacing: .2px; }
897
- .gt-sub { color: var(--dsw-alias-label-secondary); font-size: 12px; }
918
+ .gt-title { font-size: 20px; line-height: 28px; font-weight: 600; letter-spacing: 0; }
919
+ .gt-sub { color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; }
898
920
  .gt-spacer { flex: 1; }
899
921
  .gt-iconbtn { display: inline-flex; align-items: center; justify-content: center; gap: 5px;
900
922
  background: transparent; color: var(--dsw-alias-label-secondary); border: 1px solid transparent;
901
- border-radius: 8px; padding: 5px 7px; font: inherit; cursor: pointer; }
923
+ border-radius: 8px; padding: 0 7px; min-height: 28px; min-width: 28px; box-sizing: border-box;
924
+ font: inherit; cursor: pointer; }
902
925
  .gt-iconbtn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l1);
903
926
  background: var(--dsw-alias-bg-layer-1); }
904
- .gt-btn { display: inline-flex; align-items: center; gap: 5px; background: var(--dsw-alias-bg-layer-1);
927
+ .gt-btn { display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-1);
905
928
  color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px;
906
- padding: 4px 9px; font: inherit; cursor: pointer; white-space: nowrap; }
929
+ padding: 0 11px; min-height: 28px; box-sizing: border-box; font: inherit; font-size: 13px;
930
+ font-weight: 500; line-height: 20px; cursor: pointer; white-space: nowrap; }
907
931
  .gt-btn:hover { border-color: var(--dsw-alias-border-l2); }
908
- .gt-btn:disabled { opacity: .5; cursor: default; }
932
+ .gt-btn:disabled { opacity: 1; background: var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary);
933
+ border-color: transparent; cursor: default; }
909
934
  .gt-btn-primary { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary);
910
935
  color: var(--dsw-alias-bg-base); font-weight: 600; }
911
936
  .gt-btn-primary:hover { border-color: var(--dsw-alias-brand-primary); }
@@ -922,22 +947,22 @@ window.__ModuleLoader__.load({
922
947
  .gt-new { display: flex; flex-direction: column; gap: 10px; }
923
948
  .gt-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
924
949
  .gt-input, .gt-select, .gt-textarea { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary);
925
- border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 6px 9px; font: inherit; }
950
+ border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 7px 10px; font: inherit; }
926
951
  .gt-input:focus, .gt-select:focus, .gt-textarea:focus { outline: none; border-color: var(--dsw-alias-brand-primary); }
927
952
  .gt-input { flex: 1; min-width: 120px; }
928
953
  .gt-input-sm { flex: 0 1 auto; min-width: 96px; }
929
954
  .gt-textarea { width: 100%; min-height: 54px; resize: vertical; font-family: inherit; }
930
955
  .gt-select { padding: 6px 7px; }
931
- .gt-search { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 160px;
956
+ .gt-search { display: flex; align-items: center; gap: 6px; flex: 1 1 220px; min-width: 0;
932
957
  background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px;
933
- padding: 0 9px; color: var(--dsw-alias-label-secondary); }
958
+ min-height: 30px; box-sizing: border-box; padding: 0 9px; color: var(--dsw-alias-label-secondary); }
934
959
  .gt-search input { flex: 1; background: transparent; border: none; color: var(--dsw-alias-label-primary);
935
- font: inherit; padding: 6px 0; min-width: 0; }
960
+ font: inherit; font-size: 13px; padding: 0; min-width: 0; }
936
961
  .gt-search input:focus { outline: none; }
937
962
  .gt-search:focus-within { border-color: var(--dsw-alias-brand-primary); }
938
963
  .gt-kbd { font-size: 10.5px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px;
939
964
  padding: 0 4px; color: var(--dsw-alias-label-secondary); }
940
- .gt-dir { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px;
965
+ .gt-dir { display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
941
966
  color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l1);
942
967
  border-radius: 8px; padding: 3px 6px 3px 8px; max-width: 560px; }
943
968
  /* Only the path may shrink. Everything else in the pill used to shrink with it, so a
@@ -946,20 +971,30 @@ window.__ModuleLoader__.load({
946
971
  .gt-dir > .gt-muted, .gt-dir > .gt-btn, .gt-dir > svg { white-space: nowrap; flex-shrink: 0; }
947
972
  .gt-dir-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--dsw-alias-label-primary);
948
973
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 300px; }
949
- .gt-toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
974
+ .gt-toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
975
+ gap: 8px 10px; }
976
+ .gt-toolbar-main { grid-column: 1 / -1; grid-row: 1; display: flex; align-items: center;
977
+ gap: 8px; min-width: 0; flex-wrap: nowrap; }
978
+ .gt-toolbar-main .gt-search { flex: 1 1 auto; }
979
+ .gt-toolbar > .gt-spacer { display: none; }
980
+ .gt-toolbar > .gt-context-summary { grid-row: 2; grid-column: 1; min-width: 0; }
981
+ .gt-toolbar > .gt-tabs { grid-row: 2; grid-column: 2; justify-self: end; }
982
+ .gt-toolbar > .gt-context-details { grid-column: 1 / -1; grid-row: 3; }
983
+ @media (max-width: 1250px) { .gt-toolbar-main { flex-wrap: wrap; } }
950
984
  .gt-seg { display: inline-flex; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1);
951
985
  border-radius: 9px; padding: 2px; gap: 2px; }
952
986
  .gt-seg button { border: none; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit;
953
987
  border-radius: 7px; padding: 3px 10px; cursor: pointer; }
954
- .gt-seg button[data-on="1"] { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-weight: 600; }
988
+ .gt-seg button[data-on="1"] { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); font-weight: 600; }
955
989
  .gt-chips { display: flex; gap: 6px; flex-wrap: wrap; }
956
990
  /* One segmented style for every control that is "pick one of these": filters, grouping,
957
991
  the two toggles. Chips stay for facts about a todo (tags, levels, notes). */
958
992
  .gt-tabs { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
959
993
  .gt-tab { border: none; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit;
960
- font-size: 12px; border-radius: 6px; padding: 3px 9px; cursor: pointer; white-space: nowrap; }
994
+ font-size: 12.5px; border-radius: 6px; padding: 0 10px; min-height: 24px; box-sizing: border-box;
995
+ cursor: pointer; white-space: nowrap; }
961
996
  .gt-tab:hover { color: var(--dsw-alias-label-primary); }
962
- .gt-tab[data-on="1"] { background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-weight: 600; }
997
+ .gt-tab[data-on="1"] { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); font-weight: 600; }
963
998
  .gt-summary { color: var(--dsw-alias-label-secondary); font-size: 12px; }
964
999
  .gt-browse { border: 1px solid var(--dsw-alias-border-l1); border-radius: 9px; padding: 8px 10px;
965
1000
  display: flex; flex-direction: column; gap: 6px; max-height: 220px; overflow: auto; }
@@ -971,7 +1006,7 @@ window.__ModuleLoader__.load({
971
1006
  .gt-browse-item:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-brand-primary); }
972
1007
  .gt-divider { width: 1px; height: 16px; background: var(--dsw-alias-border-l1); }
973
1008
  .gt-context { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px; }
974
- .gt-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; padding: 2px 8px;
1009
+ .gt-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; line-height: 16px; padding: 2px 8px;
975
1010
  border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); background: transparent;
976
1011
  color: var(--dsw-alias-label-secondary); }
977
1012
  button.gt-chip { cursor: pointer; font-family: inherit; }
@@ -981,27 +1016,31 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
981
1016
  .gt-chip-warn { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
982
1017
  .gt-chip-bad { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); }
983
1018
  .gt-chip-ok { color: var(--dsw-alias-state-success-primary); }
984
- .gt-groups { display: flex; flex-direction: column; gap: 14px; overflow: auto; min-height: 0; flex: 1; }
1019
+ .gt-groups { display: flex; flex-direction: column; gap: 18px; overflow: auto; min-height: 0; flex: 1; }
985
1020
  .gt-group { display: flex; flex-direction: column; gap: 6px; }
986
- .gt-group-head { display: flex; align-items: center; gap: 7px; color: var(--dsw-alias-label-secondary);
987
- font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; padding: 0 2px; }
1021
+ .gt-group-head { display: flex; align-items: center; gap: 8px; color: var(--dsw-alias-label-secondary);
1022
+ font-size: 12px; line-height: 18px; padding: 0 2px; }
988
1023
  .gt-group-head button { border: none; background: transparent; color: inherit; cursor: pointer;
989
1024
  display: inline-flex; align-items: center; gap: 6px; font: inherit; padding: 2px 0; }
990
1025
  .gt-group-head svg { transition: transform .12s ease; transform: rotate(90deg); }
991
1026
  .gt-group[data-collapsed="1"] .gt-group-head svg { transform: rotate(0deg); }
992
1027
  .gt-group-action { margin-left: 4px; border: 1px solid transparent; background: transparent; color: inherit;
993
- border-radius: 999px; padding: 1px 8px; font: inherit; font-size: 11px; letter-spacing: .04em; cursor: pointer;
994
- display: inline-flex; align-items: center; gap: 5px; opacity: .72; transition: opacity .12s ease; }
1028
+ border-radius: 999px; padding: 3px 8px; min-height: 24px; box-sizing: border-box; font: inherit;
1029
+ font-size: 0; letter-spacing: .04em; cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
1030
+ opacity: .72; transition: opacity .12s ease; }
1031
+ .gt-group:hover .gt-group-action, .gt-group-action:focus-visible { font-size: 11px; }
995
1032
  .gt-group:hover .gt-group-action, .gt-group-action:focus-visible { opacity: 1; }
996
1033
  .gt-group-action:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
997
1034
  /* An exemption is a state, not an action: it stays visible and reads as "on" without a hover. */
998
1035
  .gt-group-action[data-on="1"] { opacity: 1; color: var(--dsw-alias-label-primary);
999
1036
  border-color: var(--dsw-alias-border-l2); }
1000
- .gt-group-count { color: var(--dsw-alias-label-secondary); }
1001
- .gt-group-label { color: var(--dsw-alias-label-primary); font-weight: 600; text-transform: none;
1002
- letter-spacing: 0; max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1003
- .gt-level { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; padding: 0 5px;
1004
- border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary); }
1037
+ .gt-group-count { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 400; }
1038
+ /* A group's level is redundant while it is expanded — the rows show it. */
1039
+ .gt-group:not([data-collapsed="1"]) .gt-group-head .gt-level { display: none; }
1040
+ .gt-group-label { color: var(--dsw-alias-label-primary); font-weight: 600; font-size: 12px;
1041
+ line-height: 18px; max-width: 480px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1042
+ .gt-level { display: inline-flex; align-items: center; gap: 3px; font-size: 11.5px; padding: 0;
1043
+ border: none; color: var(--dsw-alias-label-secondary); }
1005
1044
  .gt-level[data-level="urgent"] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); font-weight: 600; }
1006
1045
  .gt-level[data-level="high"] { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
1007
1046
  .gt-level[data-level="low"] { color: var(--dsw-alias-label-secondary); }
@@ -1009,57 +1048,76 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1009
1048
  display: inline-block; }
1010
1049
  .gt-here { font-size: 10.5px; padding: 0 5px; border-radius: 999px; border: 1px solid var(--dsw-alias-brand-primary);
1011
1050
  color: var(--dsw-alias-brand-primary); }
1012
- .gt-list { display: flex; flex-direction: column; gap: 6px; }
1013
- .gt-item { position: relative; display: flex; flex-direction: column; gap: 9px;
1051
+ .gt-list { display: flex; flex-direction: column; gap: 8px; }
1052
+ .gt-item { position: relative; display: flex; flex-direction: column; gap: 8px;
1014
1053
  background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1);
1015
- border-radius: 11px; padding: 11px 13px; }
1054
+ border-radius: 12px; padding: 12px 14px; }
1016
1055
  .gt-item:hover { border-color: var(--dsw-alias-border-l2); }
1017
1056
  .gt-item[data-status="doing"] { border-left: 2px solid var(--dsw-alias-brand-primary); }
1018
1057
  .gt-item[data-overdue="1"] { border-left: 2px solid var(--dsw-alias-state-error-primary); }
1019
1058
  .gt-item[data-status="done"], .gt-item[data-status="cancelled"] { opacity: .72; }
1020
1059
  .gt-item[data-open="1"] { border-color: var(--dsw-alias-brand-primary); }
1021
1060
  .gt-item-head { display: flex; gap: 10px; align-items: flex-start; }
1022
- .gt-check { flex: 0 0 auto; width: 19px; height: 19px; margin-top: 1px; border-radius: 999px; cursor: pointer;
1061
+ .gt-check { flex: 0 0 auto; width: 24px; height: 24px; margin-top: 0; border-radius: 999px; cursor: pointer;
1023
1062
  border: 1.4px solid var(--dsw-alias-border-l2); background: transparent; color: transparent;
1024
1063
  display: inline-flex; align-items: center; justify-content: center; padding: 0; }
1025
1064
  .gt-check:hover { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); }
1026
1065
  .gt-check[data-on="1"] { border-color: var(--dsw-alias-state-success-primary);
1027
1066
  background: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-bg-base); }
1028
- .gt-pick { flex: 0 0 auto; width: 17px; height: 17px; margin-top: 2px; border-radius: 5px; cursor: pointer;
1067
+ .gt-pick { flex: 0 0 auto; width: 22px; height: 22px; margin-top: 1px; border-radius: 6px; cursor: pointer;
1029
1068
  border: 1.4px solid var(--dsw-alias-border-l2); background: transparent; color: transparent;
1030
1069
  display: inline-flex; align-items: center; justify-content: center; padding: 0; }
1031
1070
  .gt-pick[data-on="1"] { border-color: var(--dsw-alias-brand-primary); background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-base); }
1032
1071
  .gt-item[data-selected="1"] { border-color: var(--dsw-alias-brand-primary); }
1033
- .gt-selectbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 7px 10px;
1034
- border: 1px solid var(--dsw-alias-brand-primary); border-radius: 9px; background: var(--dsw-alias-bg-layer-1); font-size: 12.5px; }
1035
- .gt-item-body { flex: 1; display: flex; flex-direction: column; gap: 5px; min-width: 0; }
1036
- .gt-item-title { font-weight: 600; font-size: 13.5px; word-break: break-word; line-height: 1.45; }
1037
- .gt-input-xs { width: 68px; padding: 3px 6px; font-size: 12px; }
1072
+ .gt-selectbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 9px 12px;
1073
+ border: 1px solid var(--dsw-alias-brand-primary); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); font-size: 13px; }
1074
+ .gt-item-body { flex: 1; display: flex; flex-direction: column; gap: 5px; min-width: 0; cursor: pointer; }
1075
+ .gt-item-body:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; border-radius: 6px; }
1076
+ .gt-item-title { font-weight: 600; font-size: 14px; word-break: break-word; line-height: 22px;
1077
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
1078
+ .gt-input-xs { width: 64px; flex: 0 0 auto; min-width: 0; padding: 0 8px; min-height: 28px;
1079
+ box-sizing: border-box; font-size: 12.5px; }
1038
1080
  .gt-seq { font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary); }
1039
- .gt-reminders { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 10px;
1040
- border: 1px solid var(--dsw-alias-state-warn-primary); border-radius: 10px;
1041
- background: var(--dsw-alias-bg-layer-1); font-size: 12.5px; }
1042
- .gt-reminders-title { color: var(--dsw-alias-state-warn-primary); font-weight: 600; }
1043
- .gt-reminder-item { border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2);
1044
- color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; padding: 3px 8px;
1045
- border-radius: 999px; cursor: pointer; max-width: 260px; overflow: hidden; text-overflow: ellipsis;
1081
+ .gt-reminders { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px;
1082
+ min-height: 36px; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1);
1083
+ border-radius: 10px; background: var(--dsw-alias-bg-layer-1); font-size: 13px; }
1084
+ .gt-reminders { border-left: 2px solid var(--dsw-alias-state-warn-primary); }
1085
+ .gt-reminders-title { color: var(--dsw-alias-label-primary); font-weight: 600; }
1086
+ .gt-reminder-item { border: 1px solid transparent; background: transparent;
1087
+ color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; padding: 2px 6px;
1088
+ border-radius: 6px; cursor: pointer; max-width: 300px; overflow: hidden; text-overflow: ellipsis;
1046
1089
  white-space: nowrap; }
1047
- .gt-settings { align-self: flex-end; width: 330px; display: flex; flex-direction: column; gap: 8px;
1048
- padding: 12px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px;
1049
- background: var(--dsw-alias-bg-layer-1); font-size: 12.5px; }
1050
- .gt-settings-head { font-weight: 650; }
1051
- .gt-check-line { display: flex; align-items: center; gap: 8px; justify-content: space-between;
1090
+ .gt-reminder-item:hover { background: var(--dsw-alias-button-ghost-active-fill); }
1091
+ .gt-settings { display: flex; flex-direction: column; gap: 12px; padding: 16px; width: 340px;
1092
+ box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px;
1093
+ background: var(--dsw-alias-bg-overlay); font-size: 13px; line-height: 20px; }
1094
+ .gt-settings-path { font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1095
+ .gt-settings .gt-input { flex: 0 0 auto; width: 100%; min-height: 32px; }
1096
+ .gt-settings .gt-check-line .gt-input { width: 64px; }
1097
+ .gt-settings-head { font-weight: 650; font-size: 14px; }
1098
+ .gt-check-line { display: flex; align-items: center; gap: 8px; justify-content: flex-start;
1052
1099
  color: var(--dsw-alias-label-primary); }
1053
- .gt-settings-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
1100
+ .gt-settings-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
1101
+ .gt-context-summary { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
1102
+ .gt-context-summary .gt-chip { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1103
+ .gt-context-details { display: none; flex-direction: column; gap: 8px; padding: 10px 12px;
1104
+ border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px;
1105
+ background: var(--dsw-alias-bg-layer-1); }
1106
+ .gt-context-details[data-open="1"] { display: flex; }
1107
+ .gt-detail-head { display: flex; align-items: flex-start; gap: 8px; justify-content: space-between; }
1108
+ .gt-detail-title { font-size: 14px; line-height: 22px; font-weight: 600; min-width: 0;
1109
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1110
+ .gt-truncated { color: var(--dsw-alias-state-warn-primary); font-size: 12px; }
1111
+ .gt-bulk-tag { flex: 0 1 120px; min-width: 90px; }
1054
1112
  .gt-inbox { margin-left: 6px; padding: 1px 6px; border-radius: 999px; font-size: 10.5px; font-weight: 600;
1055
1113
  color: var(--dsw-alias-state-warn-primary); border: 1px solid var(--dsw-alias-state-warn-primary);
1056
1114
  vertical-align: 1px; }
1057
1115
  .gt-item[data-status="done"] .gt-item-title, .gt-item[data-status="cancelled"] .gt-item-title {
1058
1116
  text-decoration: line-through; color: var(--dsw-alias-label-secondary); }
1059
- .gt-meta { display: flex; gap: 5px 8px; flex-wrap: wrap; color: var(--dsw-alias-label-secondary); font-size: 11.5px;
1060
- align-items: center; }
1117
+ .gt-meta { display: flex; gap: 2px 0; flex-wrap: wrap; color: var(--dsw-alias-label-secondary); font-size: 12px;
1118
+ line-height: 18px; align-items: center; }
1061
1119
  .gt-meta > span { display: inline-flex; align-items: center; }
1062
- .gt-meta-sep { opacity: .45; }
1120
+ .gt-meta > span + span::before { content: '·'; margin: 0 6px; opacity: .45; }
1063
1121
  .gt-tag { color: var(--dsw-alias-brand-primary); }
1064
1122
  /* The dispatch action stays put: hiding the only way to hand work to an agent
1065
1123
  behind a hover makes a lone todo look like it can do nothing. */
@@ -1068,23 +1126,23 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1068
1126
  .gt-attach .gt-chip { max-width: 280px; color: var(--dsw-alias-label-secondary); }
1069
1127
  .gt-attach a.gt-chip:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
1070
1128
  .gt-attach-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1071
- .gt-menu { position: absolute; top: 40px; right: 10px; z-index: 3; min-width: 168px; display: flex;
1072
- flex-direction: column; padding: 4px; background: var(--dsw-alias-bg-overlay);
1129
+ .gt-menu { position: absolute; top: 40px; right: 10px; z-index: 3; min-width: 184px; display: flex;
1130
+ flex-direction: column; padding: 5px; background: var(--dsw-alias-bg-overlay);
1073
1131
  border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; }
1074
1132
  .gt-menu button { display: flex; align-items: center; gap: 8px; text-align: left; border: none;
1075
- background: transparent; color: var(--dsw-alias-label-primary); font: inherit; padding: 6px 8px;
1133
+ background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; padding: 8px 10px;
1076
1134
  border-radius: 7px; cursor: pointer; }
1077
- .gt-menu button:hover { background: var(--dsw-alias-bg-layer-2); }
1135
+ .gt-menu button:hover { background: var(--dsw-alias-button-ghost-active-fill); }
1078
1136
  .gt-menu button[data-danger="1"] { color: var(--dsw-alias-state-error-primary); }
1079
- .gt-detail { display: flex; flex-direction: column; gap: 10px; border-top: 1px solid var(--dsw-alias-border-l1);
1080
- padding-top: 10px; margin-top: 2px; }
1137
+ .gt-detail { display: flex; flex-direction: column; gap: 12px; border-top: 1px solid var(--dsw-alias-border-l1);
1138
+ padding-top: 12px; margin-top: 2px; }
1081
1139
  /* Wide screens: the open row's detail becomes a card on the right instead of pushing the list
1082
1140
  down. Deliberately CSS-only over unchanged markup: one detail implementation, and it only
1083
1141
  applies where there is room for a second column. */
1084
- @media (min-width: 1100px) {
1142
+ @media (min-width: 1224px) {
1085
1143
  /* Only the list gives way: the title row and the toolbar keep their full width, so the
1086
1144
  controls never wrap or hide behind the card. */
1087
- .gt-root[data-wide-detail="1"] .gt-groups { padding-right: 380px; }
1145
+ .gt-root[data-wide-detail="1"] .gt-groups { padding-right: 360px; }
1088
1146
  .gt-root[data-wide-detail="1"] .gt-inner { position: relative; }
1089
1147
  .gt-root[data-wide-detail="1"] .gt-item[data-open="1"] .gt-detail {
1090
1148
  /* Anchored to the panel's own column (.gt-inner), not to the viewport: the slot does not
@@ -1095,21 +1153,25 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1095
1153
  a gap instead of overlapping by the padding width. */
1096
1154
  box-sizing: border-box;
1097
1155
  position: fixed; top: var(--gt-detail-top, 208px);
1098
- right: var(--gt-detail-right, max(24px, calc((100vw - 980px) / 2 + 24px)));
1099
- bottom: var(--gt-detail-bottom, 64px); width: 360px;
1156
+ right: var(--gt-detail-right, max(24px, calc((100vw - 1040px) / 2 + 24px)));
1157
+ bottom: var(--gt-detail-bottom, 64px); width: 340px;
1100
1158
  overflow: auto; padding: 16px; margin: 0; border: 1px solid var(--dsw-alias-border-l2);
1101
1159
  border-radius: 12px; background: var(--dsw-alias-bg-overlay); z-index: 30;
1102
1160
  }
1103
1161
  }
1104
1162
  .gt-section { display: flex; flex-direction: column; gap: 5px; }
1105
1163
  .gt-completion { white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
1106
- .gt-section-title { color: var(--dsw-alias-label-secondary); font-size: 11.5px; text-transform: uppercase;
1107
- letter-spacing: .06em; }
1164
+ .gt-section-title { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; font-weight: 600; }
1108
1165
  .gt-pre { white-space: pre-wrap; word-break: break-word; margin: 0; line-height: 1.6; }
1109
1166
  .gt-muted { color: var(--dsw-alias-label-secondary); }
1110
1167
  .gt-links { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
1111
1168
  .gt-link { display: flex; gap: 8px; align-items: center; padding: 4px 0; min-width: 0; }
1112
1169
  .gt-link-main { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
1170
+ .gt-note-item { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
1171
+ .gt-note-item + .gt-note-item { border-top: 1px solid var(--dsw-alias-border-l1); }
1172
+ .gt-note-meta { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--dsw-alias-label-secondary);
1173
+ white-space: nowrap; }
1174
+ .gt-note-text { font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-width: 68ch; }
1113
1175
  .gt-link a { color: var(--dsw-alias-brand-primary); text-decoration: none; }
1114
1176
  .gt-link a:hover { text-decoration: underline; }
1115
1177
  .gt-link-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 320px; }
@@ -1471,9 +1533,19 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1471
1533
  if (Number.isInteger(todo.seq) && todo.seq > 0) {
1472
1534
  meta.push(h('span', { key: 'seq', className: 'gt-seq', title: t('meta.seqHint') }, `#${todo.seq}`))
1473
1535
  }
1474
- meta.push(
1475
- h('span', { key: 'added', title: new Date(todo.createdAt).toLocaleString() }, `${t('meta.added')} ${absoluteTime(todo.createdAt)}(${ageText(lang, todo.createdAt, now)})`),
1476
- )
1536
+ if (todo.dueAt !== null) {
1537
+ meta.push(
1538
+ h(
1539
+ 'span',
1540
+ { key: 'due', className: overdue ? 'gt-chip-bad' : '', title: new Date(todo.dueAt).toLocaleString() },
1541
+ overdue
1542
+ ? t('meta.overdue', { n: daysLate(todo.dueAt, now) })
1543
+ : sameLocalDay(Date.parse(todo.dueAt), now)
1544
+ ? t('meta.today')
1545
+ : `${t('meta.due')} ${absoluteTime(Date.parse(todo.dueAt))}`,
1546
+ ),
1547
+ )
1548
+ }
1477
1549
  if (todo.priority !== 'normal') {
1478
1550
  meta.push(
1479
1551
  h(
@@ -1484,23 +1556,17 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1484
1556
  ),
1485
1557
  )
1486
1558
  }
1487
- if (todo.dueAt !== null) {
1559
+ meta.push(h('span', { key: 'added', title: new Date(todo.createdAt).toLocaleString() }, ageText(lang, todo.createdAt, now)))
1560
+ if (todo.tags.length > 0) {
1561
+ const shown = todo.tags.slice(0, 4)
1488
1562
  meta.push(
1489
1563
  h(
1490
1564
  'span',
1491
- { key: 'due', className: overdue ? 'gt-chip-bad' : '', title: new Date(todo.dueAt).toLocaleString() },
1492
- overdue
1493
- ? t('meta.overdue', { n: daysLate(todo.dueAt, now) })
1494
- : sameLocalDay(Date.parse(todo.dueAt), now)
1495
- ? t('meta.today')
1496
- : `${t('meta.due')} ${absoluteTime(Date.parse(todo.dueAt))}`,
1565
+ { key: 'tags', className: 'gt-tag', title: todo.tags.map((tag) => `#${tag}`).join(' ') },
1566
+ shown.map((tag) => `#${tag}`).join(' ') + (todo.tags.length > shown.length ? ` +${todo.tags.length - shown.length}` : ''),
1497
1567
  ),
1498
1568
  )
1499
1569
  }
1500
- if (todo.tags.length > 0) {
1501
- meta.push(h('span', { key: 'tags', className: 'gt-tag' }, todo.tags.map((tag) => `#${tag}`).join(' ')))
1502
- }
1503
- meta.push(h('span', { key: 'by' }, t(`meta.by.${todo.createdBy === 'agent' ? 'agent' : 'human'}`)))
1504
1570
  if (Array.isArray(todo.notes) && todo.notes.length > 0) {
1505
1571
  const latest = todo.notes[todo.notes.length - 1]
1506
1572
  meta.push(h('span', { key: 'notes', title: latest.text }, t('note.count', { n: todo.notes.length })))
@@ -1508,12 +1574,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1508
1574
  if (todo.completion !== null && todo.completion !== undefined) {
1509
1575
  meta.push(h('span', { key: 'completed', title: todo.completion.summary }, t('meta.completed', { when: absoluteTime(todo.completion.at) })))
1510
1576
  }
1511
- if (typeof todo.originDir === 'string' && todo.originDir !== '') {
1512
- meta.push(h('span', { key: 'origin', title: todo.originDir }, t('meta.origin', { dir: shortPath(todo.originDir) })))
1513
- }
1577
+ meta.push(h('span', { key: 'by' }, t(`meta.by.${todo.createdBy === 'agent' ? 'agent' : 'human'}`)))
1514
1578
  if (todo.sessionId !== '') {
1515
- // A live conversation with no working directory cannot do the work. Say
1516
- // so on the row instead of letting the next hand-off land there again.
1517
1579
  const stranded = todo.sessionAlive === true && (todo.sessionDir ?? '') === ''
1518
1580
  meta.push(
1519
1581
  h(
@@ -1521,9 +1583,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1521
1583
  {
1522
1584
  key: 'session',
1523
1585
  className: stranded ? 'gt-chip-warn' : todo.sessionAlive === true ? 'gt-chip-ok' : '',
1524
- title: stranded
1525
- ? t('meta.session.noDirHint')
1526
- : (todo.sessionDir ?? ''),
1586
+ title: stranded ? t('meta.session.noDirHint') : (todo.sessionDir ?? ''),
1527
1587
  },
1528
1588
  stranded ? t('meta.session.noDir') : todo.sessionAlive === true ? t('meta.session.live') : t('meta.session.bound'),
1529
1589
  ),
@@ -1588,7 +1648,21 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1588
1648
  ),
1589
1649
  h(
1590
1650
  'div',
1591
- { className: 'gt-item-body' },
1651
+ {
1652
+ className: 'gt-item-body',
1653
+ role: 'button',
1654
+ tabIndex: 0,
1655
+ 'aria-expanded': open,
1656
+ onClick: (event) => {
1657
+ if (event.target?.closest?.('a,button,input,textarea,select') !== null) return
1658
+ onToggleOpen(todo.id)
1659
+ },
1660
+ onKeyDown: (event) => {
1661
+ if (event.key !== 'Enter' && event.key !== ' ') return
1662
+ event.preventDefault()
1663
+ onToggleOpen(todo.id)
1664
+ },
1665
+ },
1592
1666
  h(
1593
1667
  'div',
1594
1668
  { className: 'gt-item-title' },
@@ -1600,9 +1674,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1600
1674
  h(
1601
1675
  'div',
1602
1676
  { className: 'gt-meta' },
1603
- meta.flatMap((node, index) =>
1604
- index === 0 ? [node] : [h('span', { key: `sep${index}`, className: 'gt-meta-sep' }, '·'), node],
1605
- ),
1677
+ meta,
1606
1678
  ),
1607
1679
  // What is attached is half the point of the list, so the first few are
1608
1680
  // shown as chips instead of being reported as a number.
@@ -1701,7 +1773,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1701
1773
  'button',
1702
1774
  {
1703
1775
  type: 'button',
1704
- className: 'gt-btn gt-btn-brand',
1776
+ className: 'gt-btn gt-btn-quiet',
1705
1777
  disabled: busy,
1706
1778
  title: dispatchLabel,
1707
1779
  onClick: () => onAction('dispatch', todo),
@@ -1752,6 +1824,16 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1752
1824
  ? h(
1753
1825
  'div',
1754
1826
  { className: 'gt-detail' },
1827
+ h(
1828
+ 'div',
1829
+ { className: 'gt-detail-head' },
1830
+ h('span', { className: 'gt-detail-title' }, `${Number.isInteger(todo.seq) && todo.seq > 0 ? `#${todo.seq} ` : ''}${todo.title}`),
1831
+ h(
1832
+ 'button',
1833
+ { type: 'button', className: 'gt-iconbtn', title: t('action.hide'), 'aria-label': t('action.hide'), onClick: () => onToggleOpen(todo.id) },
1834
+ h(Icon, { name: 'close', size: 13 }),
1835
+ ),
1836
+ ),
1755
1837
  h(
1756
1838
  'div',
1757
1839
  { className: 'gt-section' },
@@ -1836,14 +1918,15 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1836
1918
  (todo.notes ?? []).map((entry, index) =>
1837
1919
  h(
1838
1920
  'li',
1839
- { className: 'gt-link', key: `note:${index}:${entry.at}` },
1921
+ { className: 'gt-note-item', key: `note:${index}:${entry.at}` },
1840
1922
  h(
1841
- 'span',
1842
- { className: 'gt-link-main' },
1843
- h(Icon, { name: 'doc', size: 13 }),
1844
- h('span', { className: 'gt-muted' }, `${absoluteTime(entry.at)} · ${entry.by === 'agent' ? t('note.by.agent') : t('note.by.human')}`),
1845
- h('span', { className: 'gt-completion' }, entry.text),
1923
+ 'div',
1924
+ { className: 'gt-note-meta' },
1925
+ h(Icon, { name: 'doc', size: 12 }),
1926
+ h('span', null, absoluteTime(entry.at)),
1927
+ h('span', null, entry.by === 'agent' ? t('note.by.agent') : t('note.by.human')),
1846
1928
  ),
1929
+ h('div', { className: 'gt-note-text' }, entry.text),
1847
1930
  ),
1848
1931
  ),
1849
1932
  ),
@@ -2082,6 +2165,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2082
2165
  // One settings popover: reminders and the external door both need a place a person can
2083
2166
  // reach without curl.
2084
2167
  const [settingsOpen, setSettingsOpen] = useState(false)
2168
+ const [settingsBox, setSettingsBox] = useState(null)
2169
+ const [contextOpen, setContextOpen] = useState(false)
2085
2170
  const [tokenDraft, setTokenDraft] = useState('')
2086
2171
  const [ingestDraft, setIngestDraft] = useState(false)
2087
2172
  const [belowDraft, setBelowDraft] = useState('0.8')
@@ -2321,6 +2406,26 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2321
2406
  }
2322
2407
  }, [lang])
2323
2408
 
2409
+ useEffect(() => {
2410
+ if (!settingsOpen || typeof window === 'undefined') {
2411
+ setSettingsBox(null)
2412
+ return undefined
2413
+ }
2414
+ const measure = () => {
2415
+ const element = innerRef.current
2416
+ if (element === null) return
2417
+ const rect = element.getBoundingClientRect()
2418
+ setSettingsBox({
2419
+ top: Math.round(rect.top + 210),
2420
+ right: Math.round(window.innerWidth - rect.right + 20),
2421
+ bottom: Math.round(window.innerHeight - rect.bottom + 20),
2422
+ })
2423
+ }
2424
+ measure()
2425
+ window.addEventListener('resize', measure)
2426
+ return () => window.removeEventListener('resize', measure)
2427
+ }, [settingsOpen, lang])
2428
+
2324
2429
  useEffect(() => {
2325
2430
  if (expandedId === '' || typeof window === 'undefined') {
2326
2431
  setDetailAnchor(null)
@@ -2916,6 +3021,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2916
3021
 
2917
3022
  /** Fill the popover from the host's state, so saving cannot send stale values. */
2918
3023
  const openSettings = () => {
3024
+ // One inspector at a time: the settings card and the detail card occupy the same side.
3025
+ setExpandedId('')
2919
3026
  const current = state.health?.settings ?? {}
2920
3027
  setIngestDraft(current.ingest?.enabled === true)
2921
3028
  setBelowDraft(String(current.ingest?.confirmBelow ?? 0.8))
@@ -3071,26 +3178,27 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3071
3178
  'div',
3072
3179
  { className: 'gt-headline' },
3073
3180
  h('div', { className: 'gt-title' }, t('panel.title')),
3074
- h(
3075
- 'div',
3076
- { className: 'gt-sub' },
3077
- t('panel.subtitle'),
3078
- h('span', { className: 'gt-summary' }, ' · '),
3079
- h('span', { className: 'gt-summary' }, t('summary.active', { n: counts.open + counts.doing })),
3080
- counts.overdue > 0
3081
- ? h('span', { className: 'gt-summary' }, ` · ${t('summary.overdue', { n: counts.overdue })}`)
3082
- : null,
3083
- counts.done > 0
3084
- ? h('span', { className: 'gt-summary' }, ` · ${t('summary.done', { n: counts.done })}`)
3085
- : null,
3086
- ),
3181
+ h('div', { className: 'gt-sub' }, t('panel.subtitle')),
3087
3182
  ),
3088
- h('div', { className: 'gt-spacer' }),
3089
3183
  h(
3090
- 'button',
3091
- {
3092
- type: 'button',
3093
- className: 'gt-btn gt-btn-primary',
3184
+ 'div',
3185
+ { className: 'gt-head-actions' },
3186
+ h(
3187
+ 'button',
3188
+ {
3189
+ type: 'button',
3190
+ className: 'gt-iconbtn',
3191
+ title: t('settings.title'),
3192
+ 'aria-label': t('settings.title'),
3193
+ onClick: openSettings,
3194
+ },
3195
+ h(Icon, { name: 'gear' }),
3196
+ ),
3197
+ h(
3198
+ 'button',
3199
+ {
3200
+ type: 'button',
3201
+ className: 'gt-btn gt-btn-primary',
3094
3202
  'aria-expanded': adding,
3095
3203
  onClick: () => {
3096
3204
  setAdding((value) => !value)
@@ -3100,42 +3208,17 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3100
3208
  h(Icon, { name: 'plus', size: 13 }),
3101
3209
  t('action.add'),
3102
3210
  ),
3103
- h(
3104
- 'button',
3105
- { type: 'button', className: 'gt-iconbtn', onClick: () => void reload(), disabled: loading, title: t('action.refresh'), 'aria-label': t('action.refresh') },
3106
- h(Icon, { name: 'refresh' }),
3107
- ),
3108
- // Import/export of the same document the store keeps: the small door
3109
- // out of a plugin whose whole point is that the list is a file.
3110
- h(
3111
- 'button',
3112
- {
3113
- type: 'button',
3114
- className: 'gt-btn gt-btn-quiet',
3115
- title: t('action.exportHint'),
3116
- onClick: () => void exportJson(),
3117
- },
3118
- t('action.export'),
3119
- ),
3120
- h(
3121
- 'button',
3122
- {
3123
- type: 'button',
3124
- className: 'gt-btn gt-btn-quiet',
3125
- title: t('action.importHint'),
3126
- onClick: () => importRef.current?.click?.(),
3127
- },
3128
- t('action.import'),
3129
- ),
3130
- h(
3131
- 'button',
3132
- {
3133
- type: 'button',
3134
- className: 'gt-btn gt-btn-quiet',
3135
- 'aria-expanded': settingsOpen,
3136
- onClick: () => (settingsOpen ? setSettingsOpen(false) : openSettings()),
3137
- },
3138
- t('settings.title'),
3211
+ // One overflow menu instead of four competing buttons: the primary action is
3212
+ // adding a todo; everything else is occasional.
3213
+ h(Menu, {
3214
+ label: t('action.overflow'),
3215
+ items: [
3216
+ { key: 'refresh', label: t('action.refresh'), icon: 'refresh', onSelect: () => void reload() },
3217
+ { key: 'export', label: t('action.export'), icon: 'doc', onSelect: () => void exportJson() },
3218
+ { key: 'import', label: t('action.import'), icon: 'plus', onSelect: () => importRef.current?.click?.() },
3219
+ { key: 'settings', label: t('settings.title'), icon: 'gear', onSelect: () => openSettings() },
3220
+ ],
3221
+ }),
3139
3222
  ),
3140
3223
  h('input', {
3141
3224
  ref: importRef,
@@ -3149,7 +3232,23 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3149
3232
  settingsOpen
3150
3233
  ? h(
3151
3234
  'div',
3152
- { className: 'gt-settings', role: 'dialog', 'aria-label': t('settings.title') },
3235
+ {
3236
+ className: 'gt-settings',
3237
+ role: 'dialog',
3238
+ 'aria-label': t('settings.title'),
3239
+ style:
3240
+ settingsBox === null
3241
+ ? undefined
3242
+ : {
3243
+ position: 'fixed',
3244
+ top: `${settingsBox.top}px`,
3245
+ right: `${settingsBox.right}px`,
3246
+ bottom: `${settingsBox.bottom}px`,
3247
+ maxHeight: 'none',
3248
+ overflow: 'auto',
3249
+ zIndex: 40,
3250
+ },
3251
+ },
3153
3252
  h('div', { className: 'gt-settings-head' }, t('settings.title')),
3154
3253
  h('div', { className: 'gt-section-title' }, t('settings.reminders')),
3155
3254
  h(
@@ -3191,8 +3290,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3191
3290
  'div',
3192
3291
  { className: 'gt-settings-row' },
3193
3292
  h('button', { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: generateToken }, t('settings.generate')),
3194
- h('button', { type: 'button', className: 'gt-btn gt-btn-brand', onClick: () => void saveSettings() }, t('settings.save')),
3195
3293
  h('button', { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setSettingsOpen(false) }, t('settings.cancel')),
3294
+ h('button', { type: 'button', className: 'gt-btn gt-btn-primary', onClick: () => void saveSettings() }, t('settings.save')),
3196
3295
  ),
3197
3296
  h(
3198
3297
  'label',
@@ -3208,6 +3307,13 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3208
3307
  onChange: (event) => setBelowDraft(event.target.value),
3209
3308
  }),
3210
3309
  ),
3310
+ h('div', { className: 'gt-section-title' }, t('settings.about')),
3311
+ h(
3312
+ 'div',
3313
+ { className: 'gt-muted gt-mono gt-settings-path', title: state.health?.store?.file ?? '' },
3314
+ state.health?.store?.file ?? '',
3315
+ ),
3316
+ h('div', { className: 'gt-muted' }, `v${state.health?.version ?? ''} · ${UI_REV}`),
3211
3317
  )
3212
3318
  : null,
3213
3319
 
@@ -3215,8 +3321,13 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3215
3321
  ? h(
3216
3322
  'div',
3217
3323
  { className: 'gt-banner', role: 'alert' },
3218
- `${t('error.prefix')}: ${error.message} (${error.reason ?? 'internal'})`,
3324
+ h('span', { className: 'gt-banner-text' }, `${t('error.prefix')}: ${error.message}`),
3219
3325
  h('button', { type: 'button', className: 'gt-btn', onClick: () => void reload() }, t('action.retry')),
3326
+ h(
3327
+ 'button',
3328
+ { type: 'button', className: 'gt-iconbtn', title: t('action.hide'), 'aria-label': t('action.hide'), onClick: () => setError(null) },
3329
+ h(Icon, { name: 'close', size: 13 }),
3330
+ ),
3220
3331
  )
3221
3332
  : null,
3222
3333
 
@@ -3306,6 +3417,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3306
3417
  h(
3307
3418
  'div',
3308
3419
  { className: 'gt-toolbar' },
3420
+ h('div', { className: 'gt-toolbar-main' },
3309
3421
  h(
3310
3422
  'span',
3311
3423
  { className: 'gt-tabs', role: 'group', 'aria-label': t('filter.label') },
@@ -3361,12 +3473,37 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3361
3473
  ),
3362
3474
  ),
3363
3475
  ),
3476
+ ),
3364
3477
  h('div', { className: 'gt-spacer' }),
3478
+ h(
3479
+ 'div',
3480
+ { className: 'gt-context-summary' },
3481
+ h(
3482
+ 'button',
3483
+ { type: 'button', className: 'gt-chip', title: effectiveDir === '' ? t('dir.none') : effectiveDir, onClick: () => setContextOpen((value) => !value) },
3484
+ h(Icon, { name: 'doc', size: 12 }),
3485
+ effectiveDir === '' ? t('dir.none') : effectiveDir,
3486
+ ),
3487
+ h(
3488
+ 'button',
3489
+ { type: 'button', className: 'gt-chip', title: t('target.pick'), onClick: () => setContextOpen((value) => !value) },
3490
+ h(Icon, { name: 'play', size: 11 }),
3491
+ `${t('target.label')}: ${conversations.find((entry) => entry.id === targetValue)?.title ?? t('target.auto')}`,
3492
+ ),
3493
+ h(
3494
+ 'button',
3495
+ { type: 'button', className: 'gt-btn gt-btn-quiet', 'aria-expanded': contextOpen, onClick: () => setContextOpen((value) => !value) },
3496
+ t('settings.context'),
3497
+ ),
3498
+ ),
3499
+ h(
3500
+ 'div',
3501
+ { className: 'gt-context-details', 'data-open': contextOpen ? '1' : '0' },
3365
3502
  h(
3366
3503
  'span',
3367
3504
  { className: 'gt-dir', title: effectiveDir === '' ? t('dir.none') : effectiveDir },
3368
3505
  h(Icon, { name: 'doc', size: 12 }),
3369
- h('span', { className: 'gt-muted' }, `${t('field.dir')}:`),
3506
+ h('span', { className: 'gt-muted' }, `${t('field.dir')}: `),
3370
3507
  h('span', { className: 'gt-dir-path' }, effectiveDir === '' ? t('dir.none') : effectiveDir),
3371
3508
  h('span', { className: 'gt-muted' }, pickedDir === '' ? t('dir.fromSession') : t('dir.picked')),
3372
3509
  h('button', { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => void chooseDir() }, t('action.pickDir')),
@@ -3471,7 +3608,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3471
3608
  ),
3472
3609
  ),
3473
3610
  ),
3474
- h('span', { className: 'gt-muted' }, `${t('target.label')}:`),
3611
+ h('span', { className: 'gt-muted' }, `${t('target.label')}: `),
3475
3612
  h(
3476
3613
  'select',
3477
3614
  {
@@ -3501,6 +3638,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3501
3638
  { value: 'auto', label: t('target.auto') },
3502
3639
  ].map((option) => h('option', { key: option.value, value: option.value }, option.label)),
3503
3640
  ),
3641
+ ),
3504
3642
  h('span', { className: 'gt-spacer' }),
3505
3643
  h('span', { className: 'gt-tabs', role: 'group', 'aria-label': t('autoCollect.label') }, togglesNodes),
3506
3644
  ),
@@ -3731,6 +3869,20 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3731
3869
  'div',
3732
3870
  { className: 'gt-empty' },
3733
3871
  h('div', { className: 'gt-empty-title' }, (state.counts?.total ?? 0) === 0 && query.trim() === '' ? t('empty.none') : t('empty.filtered')),
3872
+ (state.counts?.total ?? 0) === 0 && query.trim() === ''
3873
+ ? h(
3874
+ 'button',
3875
+ {
3876
+ type: 'button',
3877
+ className: 'gt-btn gt-btn-primary',
3878
+ onClick: () => {
3879
+ setAdding(true)
3880
+ setMore(false)
3881
+ },
3882
+ },
3883
+ t('empty.add'),
3884
+ )
3885
+ : null,
3734
3886
  h('div', { className: 'gt-sub' }, t('hint.keys')),
3735
3887
  )
3736
3888
  : h(
@@ -3755,7 +3907,11 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3755
3907
  },
3756
3908
  h(Icon, { name: 'chevron', size: 12 }),
3757
3909
  h('span', { className: 'gt-group-label' }, group.label),
3758
- h('span', { className: 'gt-group-count' }, t('group.items', { n: group.todos.length })),
3910
+ h(
3911
+ 'span',
3912
+ { className: 'gt-group-count' },
3913
+ group.todos.length === 1 ? t('group.item') : t('group.items', { n: group.todos.length }),
3914
+ ),
3759
3915
  group.level === 'normal'
3760
3916
  ? null
3761
3917
  : h(
@@ -3821,7 +3977,10 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3821
3977
  open: expandedId === todo.id,
3822
3978
  dispatchLabel,
3823
3979
  onAction,
3824
- onToggleOpen: (id) => setExpandedId((current) => (current === id ? '' : id)),
3980
+ onToggleOpen: (id) => {
3981
+ setSettingsOpen(false)
3982
+ setExpandedId((current) => (current === id ? '' : id))
3983
+ },
3825
3984
  }),
3826
3985
  ),
3827
3986
  ),
@@ -3837,15 +3996,6 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3837
3996
  )
3838
3997
  : null,
3839
3998
 
3840
- h(
3841
- 'div',
3842
- { className: 'gt-foot' },
3843
- state.health !== null ? h('span', null, t('store.at', { file: state.health.store?.file ?? '' })) : null,
3844
- state.health !== null
3845
- ? h('span', { className: 'gt-mono' }, `v${state.health.version ?? ''}·${UI_REV}`)
3846
- : null,
3847
- h('span', null, t('hint.keys')),
3848
- ),
3849
3999
  ),
3850
4000
  notice !== null && notice !== undefined
3851
4001
  ? h(
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.5.3'
39
+ export const VERSION = '0.6.0'
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.5.3",
3
+ "version": "0.6.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",