dsh-global-todo 0.6.4 → 0.6.6

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,27 @@
1
+ ## [0.6.6] — 2026-10-20
2
+
3
+ Both forms now look like forms.
4
+
5
+ - **Add form** gets real labels and a hint under each field instead of placeholder-only inputs
6
+ (a placeholder vanishes the moment you type, so it can never carry a rule), and the expanded
7
+ area is separated from the title row by a hairline.
8
+ - **Detail editor** gets the same treatment: labelled Title / Priority / Due / Description rows.
9
+ - **The 340px detail card no longer fights its own grid.** Meta rows stack to one column inside
10
+ the card, attachments wrap to their own lines, and the raw path is dropped there because the
11
+ card cannot show it next to two buttons — the title still carries it as a tooltip.
12
+
13
+ ## [0.6.5] — 2026-10-20
14
+
15
+ The three loose ends the spec called out but the earlier passes left behind.
16
+
17
+ - **Five type sizes, for real.** 12.5/11.5/10.5 collapse into 12 and 11, so the panel now uses
18
+ exactly 20 / 14 / 13 / 12 / 11 — one ladder instead of eight near-identical steps.
19
+ - **Group actions move behind one kebab.** `Collect` and `Exempt` stop competing with the group
20
+ title; an exemption keeps a small always-visible mute mark (it is a state, not an action) and
21
+ the menu states both intents in words (`Collect from this conversation`, `Mute this workspace`).
22
+ - **Escape closes the innermost layer.** Settings, then the add form, then the open row — and a
23
+ click outside the settings card dismisses it, matching every other popover. The gear toggles.
24
+
1
25
  ## [0.6.4] — 2026-10-20
2
26
 
3
27
  The settings panel, rebuilt as an inspector instead of a stack of labelled rows.
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 = 'ui40'
53
+ const UI_REV = 'ui42'
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,10 @@ window.__ModuleLoader__.load({
106
106
  'settings.cancel': '取消',
107
107
  'settings.saved': '设置已保存',
108
108
  'settings.about': '关于',
109
+ 'group.actions': '分组操作',
110
+ 'group.capture': '从本对话抓取',
111
+ 'group.exemptOn': '本工作区免打扰',
112
+ 'group.exemptOff': '取消本工作区免打扰',
109
113
  'settings.remindersHint': '到期时在面板顶部提示,并按设置提前。',
110
114
  'settings.ingestHint': '允许其他工具带 token 写入,先进收件箱。',
111
115
  'settings.tokenHint': '留空表示不改动;轮换后旧 token 立刻失效。',
@@ -225,6 +229,10 @@ window.__ModuleLoader__.load({
225
229
  'field.title': '要做什么?回车即存',
226
230
  'field.description': '描述:验收标准、已知的坑…',
227
231
  'field.links': '相关文档与地址,一行一个(/绝对路径 或 https://…)',
232
+ 'field.tagsLabel': '标签',
233
+ 'field.titleLabel': '标题',
234
+ 'field.descriptionHint': '验收标准、已知的坑都可以写在这里。',
235
+ 'field.linksHint': '一行一个:绝对路径或 https:// 链接。',
228
236
  'field.tags': '标签,逗号分隔',
229
237
  'field.priority': '优先级',
230
238
  'field.due': '截止时间',
@@ -359,6 +367,10 @@ window.__ModuleLoader__.load({
359
367
  'settings.cancel': 'Cancel',
360
368
  'settings.saved': 'Settings saved',
361
369
  'settings.about': 'About',
370
+ 'group.actions': 'Group actions',
371
+ 'group.capture': 'Collect from this conversation',
372
+ 'group.exemptOn': 'Mute this workspace',
373
+ 'group.exemptOff': 'Unmute this workspace',
362
374
  'settings.remindersHint': 'Flag a deadline at the top of the panel when it comes due.',
363
375
  'settings.ingestHint': 'Let other tools POST todos with a token; they start in the inbox.',
364
376
  'settings.tokenHint': 'Leave blank to keep the current one; rotating invalidates the old token.',
@@ -479,6 +491,10 @@ window.__ModuleLoader__.load({
479
491
  'field.title': 'What needs doing? Enter to save',
480
492
  'field.description': 'Description: acceptance, known traps…',
481
493
  'field.links': 'Documents and links, one per line (/abs/path or https://…)',
494
+ 'field.tagsLabel': 'Tags',
495
+ 'field.titleLabel': 'Title',
496
+ 'field.descriptionHint': 'Acceptance criteria and known traps belong here.',
497
+ 'field.linksHint': 'One per line: an absolute path or an https:// link.',
482
498
  'field.tags': 'Tags, comma separated',
483
499
  'field.priority': 'Priority',
484
500
  'field.due': 'Due',
@@ -968,6 +984,19 @@ window.__ModuleLoader__.load({
968
984
  .gt-card { background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1);
969
985
  border-radius: 12px; padding: 12px 14px; }
970
986
  .gt-new { display: flex; flex-direction: column; gap: 10px; }
987
+ .gt-new-fields { display: flex; flex-direction: column; gap: 12px; padding-top: 4px;
988
+ border-top: 1px solid var(--dsw-alias-border-l1); }
989
+ .gt-new-fields .gt-textarea { min-height: 64px; }
990
+ .gt-new-fields .gt-field-row { display: grid; grid-template-columns: minmax(0, 1fr) 190px; gap: 12px; }
991
+ .gt-edit-meta { display: grid; grid-template-columns: minmax(0, 1fr) 190px; gap: 12px; align-items: start; }
992
+ /* The 340px card cannot hold the two-column meta row or a path beside two buttons:
993
+ stack them there, and let attachments wrap onto their own lines. */
994
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-detail .gt-edit-meta,
995
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-detail .gt-new-fields .gt-field-row {
996
+ grid-template-columns: minmax(0, 1fr); }
997
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-detail .gt-link { flex-wrap: wrap; row-gap: 4px; }
998
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-detail .gt-link .gt-mono { display: none; }
999
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-detail .gt-link-main { flex: 1 1 100%; }
971
1000
  .gt-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
972
1001
  .gt-input, .gt-select, .gt-textarea { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary);
973
1002
  border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 7px 10px; font: inherit; }
@@ -983,7 +1012,7 @@ window.__ModuleLoader__.load({
983
1012
  font: inherit; font-size: 13px; padding: 0; min-width: 0; }
984
1013
  .gt-search input:focus { outline: none; }
985
1014
  .gt-search:focus-within { border-color: var(--dsw-alias-brand-primary); }
986
- .gt-kbd { font-size: 10.5px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px;
1015
+ .gt-kbd { font-size: 11px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px;
987
1016
  padding: 0 4px; color: var(--dsw-alias-label-secondary); }
988
1017
  .gt-dir { display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
989
1018
  color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l1);
@@ -1014,7 +1043,7 @@ window.__ModuleLoader__.load({
1014
1043
  the two toggles. Chips stay for facts about a todo (tags, levels, notes). */
1015
1044
  .gt-tabs { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
1016
1045
  .gt-tab { border: none; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit;
1017
- font-size: 12.5px; border-radius: 6px; padding: 0 10px; min-height: 24px; box-sizing: border-box;
1046
+ font-size: 12px; border-radius: 6px; padding: 0 10px; min-height: 24px; box-sizing: border-box;
1018
1047
  cursor: pointer; white-space: nowrap; }
1019
1048
  .gt-tab:hover { color: var(--dsw-alias-label-primary); }
1020
1049
  .gt-tab[data-on="1"] { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); font-weight: 600; }
@@ -1047,29 +1076,26 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1047
1076
  display: inline-flex; align-items: center; gap: 6px; font: inherit; padding: 2px 0; }
1048
1077
  .gt-group-head svg { transition: transform .12s ease; transform: rotate(90deg); }
1049
1078
  .gt-group[data-collapsed="1"] .gt-group-head svg { transform: rotate(0deg); }
1050
- .gt-group-action { margin-left: 4px; border: 1px solid transparent; background: transparent; color: inherit;
1051
- border-radius: 999px; padding: 3px 8px; min-height: 24px; box-sizing: border-box; font: inherit;
1052
- font-size: 0; letter-spacing: .04em; cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
1053
- opacity: .72; transition: opacity .12s ease; }
1054
- .gt-group:hover .gt-group-action, .gt-group-action:focus-visible { font-size: 11px; }
1055
- .gt-group:hover .gt-group-action, .gt-group-action:focus-visible { opacity: 1; }
1056
- .gt-group-action:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
1057
- /* An exemption is a state, not an action: it stays visible and reads as "on" without a hover. */
1058
- .gt-group-action[data-on="1"] { opacity: 1; color: var(--dsw-alias-label-primary);
1059
- border-color: var(--dsw-alias-border-l2); }
1079
+ .gt-group-flag { margin-left: 2px; display: inline-flex; align-items: center; color: var(--dsw-alias-label-secondary); }
1080
+ /* The group kebab stays quiet until the row is hovered or focused. */
1081
+ .gt-group-head .gt-iconbtn { margin-left: 2px; min-width: 24px; min-height: 24px; opacity: 0;
1082
+ transition: opacity .12s ease; }
1083
+ .gt-group:hover .gt-group-head .gt-iconbtn, .gt-group-head .gt-iconbtn:focus-visible,
1084
+ .gt-group-head .gt-iconbtn[aria-expanded="true"] { opacity: .72; }
1085
+ .gt-group-head .gt-iconbtn:hover { opacity: 1; }
1060
1086
  .gt-group-count { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 400; }
1061
1087
  /* A group's level is redundant while it is expanded — the rows show it. */
1062
1088
  .gt-group:not([data-collapsed="1"]) .gt-group-head .gt-level { display: none; }
1063
1089
  .gt-group-label { color: var(--dsw-alias-label-primary); font-weight: 600; font-size: 12px;
1064
1090
  line-height: 18px; max-width: 480px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1065
- .gt-level { display: inline-flex; align-items: center; gap: 3px; font-size: 11.5px; padding: 0;
1091
+ .gt-level { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; padding: 0;
1066
1092
  border: none; color: var(--dsw-alias-label-secondary); }
1067
1093
  .gt-level[data-level="urgent"] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); font-weight: 600; }
1068
1094
  .gt-level[data-level="high"] { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
1069
1095
  .gt-level[data-level="low"] { color: var(--dsw-alias-label-secondary); }
1070
1096
  .gt-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--dsw-alias-state-success-primary);
1071
1097
  display: inline-block; }
1072
- .gt-here { font-size: 10.5px; padding: 0 5px; border-radius: 999px; border: 1px solid var(--dsw-alias-brand-primary);
1098
+ .gt-here { font-size: 11px; padding: 0 5px; border-radius: 999px; border: 1px solid var(--dsw-alias-brand-primary);
1073
1099
  color: var(--dsw-alias-brand-primary); }
1074
1100
  .gt-list { display: flex; flex-direction: column; gap: 8px; }
1075
1101
  .gt-item { position: relative; display: flex; flex-direction: column; gap: 8px;
@@ -1102,7 +1128,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1102
1128
  .gt-item-title { font-weight: 600; font-size: 14px; word-break: break-word; line-height: 22px;
1103
1129
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
1104
1130
  .gt-input-xs { width: 64px; flex: 0 0 auto; min-width: 0; padding: 0 8px; min-height: 28px;
1105
- box-sizing: border-box; font-size: 12.5px; }
1131
+ box-sizing: border-box; font-size: 12px; }
1106
1132
  .gt-seq { font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary); }
1107
1133
  .gt-reminders { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px;
1108
1134
  min-height: 36px; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1);
@@ -1144,13 +1170,13 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1144
1170
  .gt-settings-foot { display: flex; flex: 0 0 auto; justify-content: flex-end; gap: 8px;
1145
1171
  padding: 12px 16px; border-top: 1px solid var(--dsw-alias-border-l1); }
1146
1172
  .gt-group-select { display: none; min-height: 28px; max-width: 150px; padding: 0 8px; font: inherit;
1147
- font-size: 12.5px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1);
1173
+ font-size: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1);
1148
1174
  border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; }
1149
1175
  .gt-root[data-wide="0"] .gt-group-tabs { display: none; }
1150
1176
  .gt-root[data-wide="0"] .gt-group-select { display: inline-flex; align-items: center; }
1151
1177
  .gt-inbox-note { padding: 0 2px 2px; color: var(--dsw-alias-label-secondary); font-size: 13px;
1152
1178
  line-height: 20px; }
1153
- .gt-settings-path { font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1179
+ .gt-settings-path { font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1154
1180
  .gt-settings .gt-input { flex: 0 0 auto; width: 100%; min-height: 32px; }
1155
1181
  .gt-settings .gt-input-num { width: 76px; }
1156
1182
  .gt-check-line { display: flex; align-items: center; gap: 8px; justify-content: flex-start;
@@ -1170,7 +1196,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1170
1196
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1171
1197
  .gt-truncated { color: var(--dsw-alias-state-warn-primary); font-size: 12px; }
1172
1198
  .gt-bulk-tag { flex: 0 1 120px; min-width: 90px; }
1173
- .gt-inbox { margin-left: 6px; padding: 1px 6px; border-radius: 999px; font-size: 10.5px; font-weight: 600;
1199
+ .gt-inbox { margin-left: 6px; padding: 1px 6px; border-radius: 999px; font-size: 11px; font-weight: 600;
1174
1200
  color: var(--dsw-alias-state-warn-primary); border: 1px solid var(--dsw-alias-state-warn-primary);
1175
1201
  vertical-align: 1px; }
1176
1202
  .gt-item[data-status="done"] .gt-item-title, .gt-item[data-status="cancelled"] .gt-item-title {
@@ -1229,17 +1255,17 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1229
1255
  .gt-link-main { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
1230
1256
  .gt-note-item { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
1231
1257
  .gt-note-item + .gt-note-item { border-top: 1px solid var(--dsw-alias-border-l1); }
1232
- .gt-note-meta { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--dsw-alias-label-secondary);
1258
+ .gt-note-meta { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dsw-alias-label-secondary);
1233
1259
  white-space: nowrap; }
1234
1260
  .gt-note-text { font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-width: 68ch; }
1235
1261
  .gt-link a { color: var(--dsw-alias-brand-primary); text-decoration: none; }
1236
1262
  .gt-link a:hover { text-decoration: underline; }
1237
1263
  .gt-link-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 320px; }
1238
- .gt-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px;
1264
+ .gt-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
1239
1265
  color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1240
1266
  .gt-banner { border-radius: 9px; padding: 8px 11px; border: 1px solid var(--dsw-alias-state-error-primary);
1241
1267
  color: var(--dsw-alias-state-error-primary); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
1242
- .gt-activity { display: flex; flex-direction: column; gap: 3px; color: var(--dsw-alias-label-secondary); font-size: 11.5px; }
1268
+ .gt-activity { display: flex; flex-direction: column; gap: 3px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
1243
1269
  .gt-empty { display: flex; flex-direction: column; gap: 6px; align-items: center; justify-content: center;
1244
1270
  color: var(--dsw-alias-label-secondary); padding: 42px 16px; text-align: center; flex: 1; }
1245
1271
  .gt-empty-title { font-size: 14px; color: var(--dsw-alias-label-primary); }
@@ -1900,34 +1926,59 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1900
1926
  h('div', { className: 'gt-section-title' }, t('detail.edit')),
1901
1927
  h(
1902
1928
  'div',
1903
- { className: 'gt-row' },
1904
- h('input', { className: 'gt-input', value: draft.title, 'aria-label': t('field.title'), onChange: (event) => setDraft({ ...draft, title: event.target.value }) }),
1905
- h(Segmented, {
1906
- label: t('field.priority'),
1907
- value: draft.priority,
1908
- onChange: (priority) => setDraft({ ...draft, priority }),
1909
- options: [
1910
- { value: 'low', label: t('priority.low') },
1911
- { value: 'normal', label: t('priority.normal') },
1912
- { value: 'high', label: t('priority.high') },
1913
- { value: 'urgent', label: t('priority.urgent') },
1914
- ],
1915
- }),
1929
+ { className: 'gt-field' },
1930
+ h('label', { className: 'gt-field-label', htmlFor: 'gt-edit-title' }, t('field.titleLabel')),
1916
1931
  h('input', {
1917
- className: 'gt-input gt-input-sm',
1918
- type: 'datetime-local',
1919
- value: draft.due,
1920
- 'aria-label': t('field.due'),
1921
- onChange: (event) => setDraft({ ...draft, due: event.target.value }),
1932
+ id: 'gt-edit-title',
1933
+ className: 'gt-input',
1934
+ value: draft.title,
1935
+ onChange: (event) => setDraft({ ...draft, title: event.target.value }),
1936
+ }),
1937
+ ),
1938
+ h(
1939
+ 'div',
1940
+ { className: 'gt-field-row gt-edit-meta' },
1941
+ h(
1942
+ 'div',
1943
+ { className: 'gt-field' },
1944
+ h('span', { className: 'gt-field-label' }, t('field.priority')),
1945
+ h(Segmented, {
1946
+ label: t('field.priority'),
1947
+ value: draft.priority,
1948
+ onChange: (priority) => setDraft({ ...draft, priority }),
1949
+ options: [
1950
+ { value: 'urgent', label: t('priority.urgent') },
1951
+ { value: 'high', label: t('priority.high') },
1952
+ { value: 'normal', label: t('priority.normal') },
1953
+ { value: 'low', label: t('priority.low') },
1954
+ ],
1955
+ }),
1956
+ ),
1957
+ h(
1958
+ 'div',
1959
+ { className: 'gt-field' },
1960
+ h('label', { className: 'gt-field-label', htmlFor: 'gt-edit-due' }, t('field.due')),
1961
+ h('input', {
1962
+ id: 'gt-edit-due',
1963
+ className: 'gt-input',
1964
+ type: 'datetime-local',
1965
+ value: draft.due,
1966
+ onChange: (event) => setDraft({ ...draft, due: event.target.value }),
1967
+ }),
1968
+ ),
1969
+ ),
1970
+ h(
1971
+ 'div',
1972
+ { className: 'gt-field' },
1973
+ h('label', { className: 'gt-field-label', htmlFor: 'gt-edit-desc' }, t('detail.description')),
1974
+ h('textarea', {
1975
+ id: 'gt-edit-desc',
1976
+ className: 'gt-textarea',
1977
+ value: draft.description,
1978
+ placeholder: t('field.description'),
1979
+ onChange: (event) => setDraft({ ...draft, description: event.target.value }),
1922
1980
  }),
1923
1981
  ),
1924
- h('textarea', {
1925
- className: 'gt-textarea',
1926
- value: draft.description,
1927
- placeholder: t('field.description'),
1928
- 'aria-label': t('detail.description'),
1929
- onChange: (event) => setDraft({ ...draft, description: event.target.value }),
1930
- }),
1931
1982
  dirty
1932
1983
  ? h(
1933
1984
  'div',
@@ -2411,6 +2462,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2411
2462
  // The wide-screen detail card floats over the panel, so its offsets are measured from
2412
2463
  // the panel's own column rather than derived from the viewport width.
2413
2464
  const innerRef = useRef(null)
2465
+ const settingsRef = useRef(null)
2466
+ const settingsTriggerRef = useRef(null)
2414
2467
  const [detailAnchor, setDetailAnchor] = useState(null)
2415
2468
  const titleRef = useRef(null)
2416
2469
  const searchRef = useRef(null)
@@ -2572,8 +2625,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2572
2625
  return () => clearInterval(timer)
2573
2626
  }, [reload])
2574
2627
 
2575
- // Keyboard: `/` search, `n` new, Escape collapse — but never while the
2576
- // user is typing in a field.
2628
+ // Keyboard: `/` search, `n` new, Escape closes the innermost layer — but never
2629
+ // while the user is typing in a field, where the first Escape only drops focus.
2577
2630
  useEffect(() => {
2578
2631
  if (typeof document === 'undefined') return undefined
2579
2632
  const onKey = (event) => {
@@ -2583,8 +2636,16 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2583
2636
  target !== undefined &&
2584
2637
  (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable === true)
2585
2638
  if (event.key === 'Escape') {
2586
- setExpandedId('')
2587
2639
  if (typing && typeof target.blur === 'function') target.blur()
2640
+ if (settingsOpen) {
2641
+ setSettingsOpen(false)
2642
+ return
2643
+ }
2644
+ if (adding) {
2645
+ setAdding(false)
2646
+ return
2647
+ }
2648
+ setExpandedId('')
2588
2649
  return
2589
2650
  }
2590
2651
  if (typing || event.metaKey || event.ctrlKey || event.altKey) return
@@ -2598,7 +2659,23 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2598
2659
  }
2599
2660
  document.addEventListener('keydown', onKey)
2600
2661
  return () => document.removeEventListener('keydown', onKey)
2601
- }, [])
2662
+ }, [settingsOpen, adding])
2663
+
2664
+ // The settings card floats over the list, so a click anywhere else dismisses it
2665
+ // the way every other popover on the web does.
2666
+ useEffect(() => {
2667
+ if (!settingsOpen || typeof document === 'undefined') return undefined
2668
+ const onDown = (event) => {
2669
+ const card = settingsRef.current
2670
+ const gear = settingsTriggerRef.current
2671
+ if (card === null) return
2672
+ if (card.contains(event.target) === true) return
2673
+ if (gear !== null && gear.contains(event.target) === true) return
2674
+ setSettingsOpen(false)
2675
+ }
2676
+ document.addEventListener('mousedown', onDown)
2677
+ return () => document.removeEventListener('mousedown', onDown)
2678
+ }, [settingsOpen])
2602
2679
 
2603
2680
  const run = async (id, operation) => {
2604
2681
  setBusyId(id)
@@ -3273,15 +3350,20 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3273
3350
  'div',
3274
3351
  { className: 'gt-head-actions' },
3275
3352
  h(
3276
- 'button',
3277
- {
3278
- type: 'button',
3279
- className: 'gt-iconbtn',
3280
- title: t('settings.title'),
3281
- 'aria-label': t('settings.title'),
3282
- onClick: openSettings,
3283
- },
3284
- h(Icon, { name: 'gear' }),
3353
+ 'span',
3354
+ { ref: settingsTriggerRef, style: { display: 'inline-flex' } },
3355
+ h(
3356
+ 'button',
3357
+ {
3358
+ type: 'button',
3359
+ className: 'gt-iconbtn',
3360
+ title: t('settings.title'),
3361
+ 'aria-label': t('settings.title'),
3362
+ 'aria-expanded': settingsOpen,
3363
+ onClick: () => (settingsOpen ? setSettingsOpen(false) : openSettings()),
3364
+ },
3365
+ h(Icon, { name: 'gear' }),
3366
+ ),
3285
3367
  ),
3286
3368
  h(
3287
3369
  'button',
@@ -3323,6 +3405,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3323
3405
  ? h(
3324
3406
  'div',
3325
3407
  {
3408
+ ref: settingsRef,
3326
3409
  className: 'gt-settings',
3327
3410
  role: 'dialog',
3328
3411
  'aria-label': t('settings.title'),
@@ -3518,31 +3601,65 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3518
3601
  more
3519
3602
  ? h(
3520
3603
  'div',
3521
- { className: 'gt-new' },
3522
- h('textarea', {
3523
- className: 'gt-textarea',
3524
- placeholder: t('field.description'),
3525
- value: draft.description,
3526
- 'aria-label': t('detail.description'),
3527
- onChange: (event) => setDraft({ ...draft, description: event.target.value }),
3528
- }),
3529
- h('textarea', {
3530
- className: 'gt-textarea',
3531
- placeholder: t('field.links'),
3532
- value: draft.links,
3533
- 'aria-label': t('detail.links'),
3534
- onChange: (event) => setDraft({ ...draft, links: event.target.value }),
3535
- }),
3604
+ { className: 'gt-new-fields' },
3536
3605
  h(
3537
3606
  'div',
3538
- { className: 'gt-row' },
3539
- h('input', {
3540
- className: 'gt-input gt-input-sm',
3541
- placeholder: t('field.tags'),
3542
- value: draft.tags,
3543
- 'aria-label': t('field.tags'),
3544
- onChange: (event) => setDraft({ ...draft, tags: event.target.value }),
3607
+ { className: 'gt-field' },
3608
+ h('label', { className: 'gt-field-label', htmlFor: 'gt-new-desc' }, t('detail.description')),
3609
+ h('textarea', {
3610
+ id: 'gt-new-desc',
3611
+ className: 'gt-textarea',
3612
+ placeholder: t('field.description'),
3613
+ value: draft.description,
3614
+ onChange: (event) => setDraft({ ...draft, description: event.target.value }),
3545
3615
  }),
3616
+ h('span', { className: 'gt-hint' }, t('field.descriptionHint')),
3617
+ ),
3618
+ h(
3619
+ 'div',
3620
+ { className: 'gt-field' },
3621
+ h('label', { className: 'gt-field-label', htmlFor: 'gt-new-links' }, t('detail.links')),
3622
+ h('textarea', {
3623
+ id: 'gt-new-links',
3624
+ className: 'gt-textarea',
3625
+ placeholder: t('field.links'),
3626
+ value: draft.links,
3627
+ onChange: (event) => setDraft({ ...draft, links: event.target.value }),
3628
+ }),
3629
+ h('span', { className: 'gt-hint' }, t('field.linksHint')),
3630
+ ),
3631
+ h(
3632
+ 'div',
3633
+ { className: 'gt-field-row' },
3634
+ h(
3635
+ 'div',
3636
+ { className: 'gt-field' },
3637
+ h('label', { className: 'gt-field-label', htmlFor: 'gt-new-tags' }, t('field.tagsLabel')),
3638
+ h('input', {
3639
+ id: 'gt-new-tags',
3640
+ className: 'gt-input gt-input-sm',
3641
+ placeholder: t('field.tags'),
3642
+ value: draft.tags,
3643
+ onChange: (event) => setDraft({ ...draft, tags: event.target.value }),
3644
+ }),
3645
+ ),
3646
+ h(
3647
+ 'div',
3648
+ { className: 'gt-field' },
3649
+ h('label', { className: 'gt-field-label', htmlFor: 'gt-new-due' }, t('field.due')),
3650
+ h('input', {
3651
+ id: 'gt-new-due',
3652
+ className: 'gt-input gt-input-sm',
3653
+ type: 'datetime-local',
3654
+ value: draft.due,
3655
+ onChange: (event) => setDraft({ ...draft, due: event.target.value }),
3656
+ }),
3657
+ ),
3658
+ ),
3659
+ h(
3660
+ 'div',
3661
+ { className: 'gt-field' },
3662
+ h('span', { className: 'gt-field-label' }, t('field.priority')),
3546
3663
  h(Segmented, {
3547
3664
  label: t('field.priority'),
3548
3665
  value: draft.priority,
@@ -3554,14 +3671,6 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3554
3671
  { value: 'low', label: t('priority.low') },
3555
3672
  ],
3556
3673
  }),
3557
- h('input', {
3558
- className: 'gt-input gt-input-sm',
3559
- type: 'datetime-local',
3560
- value: draft.due,
3561
- title: t('field.due'),
3562
- 'aria-label': t('field.due'),
3563
- onChange: (event) => setDraft({ ...draft, due: event.target.value }),
3564
- }),
3565
3674
  ),
3566
3675
  )
3567
3676
  : null,
@@ -4108,39 +4217,59 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
4108
4217
  ? h('span', { className: 'gt-dot', title: t('meta.session.live'), 'aria-label': t('meta.session.live') })
4109
4218
  : null,
4110
4219
  ),
4111
- group.sessionId === undefined
4112
- ? null
4113
- : h(
4114
- 'button',
4220
+ // An exemption is a state, so it keeps a small always-visible mark; the
4221
+ // actions themselves live behind one kebab instead of two text buttons
4222
+ // competing with the group title.
4223
+ exemptGroup
4224
+ ? h(
4225
+ 'span',
4115
4226
  {
4116
- type: 'button',
4117
- className: 'gt-group-action',
4118
- title: t('action.captureGroupHint'),
4119
- onClick: (event) => {
4120
- event.stopPropagation()
4121
- void requestCapture(group.sessionId, group.label)
4122
- },
4123
- },
4124
- h(Icon, { name: 'doc', size: 11 }),
4125
- t('action.captureShort'),
4126
- ),
4127
- group.dir === undefined || group.dir === ''
4128
- ? null
4129
- : h(
4130
- 'button',
4131
- {
4132
- type: 'button',
4133
- className: 'gt-group-action',
4134
- 'data-on': exemptGroup ? '1' : '0',
4135
- title: exemptGroup ? t('action.exemptHintOn') : t('action.exemptHint'),
4136
- onClick: (event) => {
4137
- event.stopPropagation()
4138
- void toggleExemptWorkspace(group.dir, !exemptGroup)
4139
- },
4227
+ className: 'gt-group-flag',
4228
+ title: t('action.exemptHintOn'),
4140
4229
  },
4141
4230
  h(Icon, { name: 'mute', size: 11 }),
4142
- exemptGroup ? t('action.exemptOn') : t('action.exempt'),
4143
- ),
4231
+ )
4232
+ : null,
4233
+ [
4234
+ group.sessionId === undefined
4235
+ ? null
4236
+ : {
4237
+ key: 'capture',
4238
+ label: t('action.captureShort'),
4239
+ icon: 'doc',
4240
+ onSelect: () => void requestCapture(group.sessionId, group.label),
4241
+ },
4242
+ group.dir === undefined || group.dir === ''
4243
+ ? null
4244
+ : {
4245
+ key: 'exempt',
4246
+ label: exemptGroup ? t('action.exemptOn') : t('action.exempt'),
4247
+ icon: 'mute',
4248
+ onSelect: () => void toggleExemptWorkspace(group.dir, !exemptGroup),
4249
+ },
4250
+ ].filter((item) => item !== null).length === 0
4251
+ ? null
4252
+ : h(Menu, {
4253
+ label: t('group.actions'),
4254
+ items: [
4255
+ group.sessionId === undefined
4256
+ ? null
4257
+ : {
4258
+ key: 'capture',
4259
+ label: t('group.capture'),
4260
+ icon: 'doc',
4261
+ onSelect: () => void requestCapture(group.sessionId, group.label),
4262
+ },
4263
+ group.dir === undefined || group.dir === ''
4264
+ ? null
4265
+ : {
4266
+ key: 'exempt',
4267
+ label: exemptGroup ? t('group.exemptOff') : t('group.exemptOn'),
4268
+ icon: 'mute',
4269
+ onSelect: () => void toggleExemptWorkspace(group.dir, !exemptGroup),
4270
+ },
4271
+ ].filter((item) => item !== null && item !== undefined),
4272
+ }),
4144
4273
  ),
4145
4274
  isCollapsed
4146
4275
  ? null
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.4'
39
+ export const VERSION = '0.6.6'
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.4",
3
+ "version": "0.6.6",
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",