dsh-global-todo 0.6.4 → 0.6.5

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,15 @@
1
+ ## [0.6.5] — 2026-10-20
2
+
3
+ The three loose ends the spec called out but the earlier passes left behind.
4
+
5
+ - **Five type sizes, for real.** 12.5/11.5/10.5 collapse into 12 and 11, so the panel now uses
6
+ exactly 20 / 14 / 13 / 12 / 11 — one ladder instead of eight near-identical steps.
7
+ - **Group actions move behind one kebab.** `Collect` and `Exempt` stop competing with the group
8
+ title; an exemption keeps a small always-visible mute mark (it is a state, not an action) and
9
+ the menu states both intents in words (`Collect from this conversation`, `Mute this workspace`).
10
+ - **Escape closes the innermost layer.** Settings, then the add form, then the open row — and a
11
+ click outside the settings card dismisses it, matching every other popover. The gear toggles.
12
+
1
13
  ## [0.6.4] — 2026-10-20
2
14
 
3
15
  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 = 'ui41'
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 立刻失效。',
@@ -359,6 +363,10 @@ window.__ModuleLoader__.load({
359
363
  'settings.cancel': 'Cancel',
360
364
  'settings.saved': 'Settings saved',
361
365
  'settings.about': 'About',
366
+ 'group.actions': 'Group actions',
367
+ 'group.capture': 'Collect from this conversation',
368
+ 'group.exemptOn': 'Mute this workspace',
369
+ 'group.exemptOff': 'Unmute this workspace',
362
370
  'settings.remindersHint': 'Flag a deadline at the top of the panel when it comes due.',
363
371
  'settings.ingestHint': 'Let other tools POST todos with a token; they start in the inbox.',
364
372
  'settings.tokenHint': 'Leave blank to keep the current one; rotating invalidates the old token.',
@@ -983,7 +991,7 @@ window.__ModuleLoader__.load({
983
991
  font: inherit; font-size: 13px; padding: 0; min-width: 0; }
984
992
  .gt-search input:focus { outline: none; }
985
993
  .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;
994
+ .gt-kbd { font-size: 11px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px;
987
995
  padding: 0 4px; color: var(--dsw-alias-label-secondary); }
988
996
  .gt-dir { display: inline-flex; align-items: center; gap: 5px; font-size: 12px;
989
997
  color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l1);
@@ -1014,7 +1022,7 @@ window.__ModuleLoader__.load({
1014
1022
  the two toggles. Chips stay for facts about a todo (tags, levels, notes). */
1015
1023
  .gt-tabs { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
1016
1024
  .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;
1025
+ font-size: 12px; border-radius: 6px; padding: 0 10px; min-height: 24px; box-sizing: border-box;
1018
1026
  cursor: pointer; white-space: nowrap; }
1019
1027
  .gt-tab:hover { color: var(--dsw-alias-label-primary); }
1020
1028
  .gt-tab[data-on="1"] { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); font-weight: 600; }
@@ -1047,29 +1055,26 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1047
1055
  display: inline-flex; align-items: center; gap: 6px; font: inherit; padding: 2px 0; }
1048
1056
  .gt-group-head svg { transition: transform .12s ease; transform: rotate(90deg); }
1049
1057
  .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); }
1058
+ .gt-group-flag { margin-left: 2px; display: inline-flex; align-items: center; color: var(--dsw-alias-label-secondary); }
1059
+ /* The group kebab stays quiet until the row is hovered or focused. */
1060
+ .gt-group-head .gt-iconbtn { margin-left: 2px; min-width: 24px; min-height: 24px; opacity: 0;
1061
+ transition: opacity .12s ease; }
1062
+ .gt-group:hover .gt-group-head .gt-iconbtn, .gt-group-head .gt-iconbtn:focus-visible,
1063
+ .gt-group-head .gt-iconbtn[aria-expanded="true"] { opacity: .72; }
1064
+ .gt-group-head .gt-iconbtn:hover { opacity: 1; }
1060
1065
  .gt-group-count { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 400; }
1061
1066
  /* A group's level is redundant while it is expanded — the rows show it. */
1062
1067
  .gt-group:not([data-collapsed="1"]) .gt-group-head .gt-level { display: none; }
1063
1068
  .gt-group-label { color: var(--dsw-alias-label-primary); font-weight: 600; font-size: 12px;
1064
1069
  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;
1070
+ .gt-level { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; padding: 0;
1066
1071
  border: none; color: var(--dsw-alias-label-secondary); }
1067
1072
  .gt-level[data-level="urgent"] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); font-weight: 600; }
1068
1073
  .gt-level[data-level="high"] { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
1069
1074
  .gt-level[data-level="low"] { color: var(--dsw-alias-label-secondary); }
1070
1075
  .gt-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--dsw-alias-state-success-primary);
1071
1076
  display: inline-block; }
1072
- .gt-here { font-size: 10.5px; padding: 0 5px; border-radius: 999px; border: 1px solid var(--dsw-alias-brand-primary);
1077
+ .gt-here { font-size: 11px; padding: 0 5px; border-radius: 999px; border: 1px solid var(--dsw-alias-brand-primary);
1073
1078
  color: var(--dsw-alias-brand-primary); }
1074
1079
  .gt-list { display: flex; flex-direction: column; gap: 8px; }
1075
1080
  .gt-item { position: relative; display: flex; flex-direction: column; gap: 8px;
@@ -1102,7 +1107,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1102
1107
  .gt-item-title { font-weight: 600; font-size: 14px; word-break: break-word; line-height: 22px;
1103
1108
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
1104
1109
  .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; }
1110
+ box-sizing: border-box; font-size: 12px; }
1106
1111
  .gt-seq { font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary); }
1107
1112
  .gt-reminders { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px;
1108
1113
  min-height: 36px; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1);
@@ -1144,13 +1149,13 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1144
1149
  .gt-settings-foot { display: flex; flex: 0 0 auto; justify-content: flex-end; gap: 8px;
1145
1150
  padding: 12px 16px; border-top: 1px solid var(--dsw-alias-border-l1); }
1146
1151
  .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);
1152
+ font-size: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1);
1148
1153
  border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; }
1149
1154
  .gt-root[data-wide="0"] .gt-group-tabs { display: none; }
1150
1155
  .gt-root[data-wide="0"] .gt-group-select { display: inline-flex; align-items: center; }
1151
1156
  .gt-inbox-note { padding: 0 2px 2px; color: var(--dsw-alias-label-secondary); font-size: 13px;
1152
1157
  line-height: 20px; }
1153
- .gt-settings-path { font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1158
+ .gt-settings-path { font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1154
1159
  .gt-settings .gt-input { flex: 0 0 auto; width: 100%; min-height: 32px; }
1155
1160
  .gt-settings .gt-input-num { width: 76px; }
1156
1161
  .gt-check-line { display: flex; align-items: center; gap: 8px; justify-content: flex-start;
@@ -1170,7 +1175,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1170
1175
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1171
1176
  .gt-truncated { color: var(--dsw-alias-state-warn-primary); font-size: 12px; }
1172
1177
  .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;
1178
+ .gt-inbox { margin-left: 6px; padding: 1px 6px; border-radius: 999px; font-size: 11px; font-weight: 600;
1174
1179
  color: var(--dsw-alias-state-warn-primary); border: 1px solid var(--dsw-alias-state-warn-primary);
1175
1180
  vertical-align: 1px; }
1176
1181
  .gt-item[data-status="done"] .gt-item-title, .gt-item[data-status="cancelled"] .gt-item-title {
@@ -1229,17 +1234,17 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1229
1234
  .gt-link-main { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
1230
1235
  .gt-note-item { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
1231
1236
  .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);
1237
+ .gt-note-meta { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dsw-alias-label-secondary);
1233
1238
  white-space: nowrap; }
1234
1239
  .gt-note-text { font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-width: 68ch; }
1235
1240
  .gt-link a { color: var(--dsw-alias-brand-primary); text-decoration: none; }
1236
1241
  .gt-link a:hover { text-decoration: underline; }
1237
1242
  .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;
1243
+ .gt-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
1239
1244
  color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1240
1245
  .gt-banner { border-radius: 9px; padding: 8px 11px; border: 1px solid var(--dsw-alias-state-error-primary);
1241
1246
  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; }
1247
+ .gt-activity { display: flex; flex-direction: column; gap: 3px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
1243
1248
  .gt-empty { display: flex; flex-direction: column; gap: 6px; align-items: center; justify-content: center;
1244
1249
  color: var(--dsw-alias-label-secondary); padding: 42px 16px; text-align: center; flex: 1; }
1245
1250
  .gt-empty-title { font-size: 14px; color: var(--dsw-alias-label-primary); }
@@ -2411,6 +2416,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2411
2416
  // The wide-screen detail card floats over the panel, so its offsets are measured from
2412
2417
  // the panel's own column rather than derived from the viewport width.
2413
2418
  const innerRef = useRef(null)
2419
+ const settingsRef = useRef(null)
2420
+ const settingsTriggerRef = useRef(null)
2414
2421
  const [detailAnchor, setDetailAnchor] = useState(null)
2415
2422
  const titleRef = useRef(null)
2416
2423
  const searchRef = useRef(null)
@@ -2572,8 +2579,8 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2572
2579
  return () => clearInterval(timer)
2573
2580
  }, [reload])
2574
2581
 
2575
- // Keyboard: `/` search, `n` new, Escape collapse — but never while the
2576
- // user is typing in a field.
2582
+ // Keyboard: `/` search, `n` new, Escape closes the innermost layer — but never
2583
+ // while the user is typing in a field, where the first Escape only drops focus.
2577
2584
  useEffect(() => {
2578
2585
  if (typeof document === 'undefined') return undefined
2579
2586
  const onKey = (event) => {
@@ -2583,8 +2590,16 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2583
2590
  target !== undefined &&
2584
2591
  (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable === true)
2585
2592
  if (event.key === 'Escape') {
2586
- setExpandedId('')
2587
2593
  if (typing && typeof target.blur === 'function') target.blur()
2594
+ if (settingsOpen) {
2595
+ setSettingsOpen(false)
2596
+ return
2597
+ }
2598
+ if (adding) {
2599
+ setAdding(false)
2600
+ return
2601
+ }
2602
+ setExpandedId('')
2588
2603
  return
2589
2604
  }
2590
2605
  if (typing || event.metaKey || event.ctrlKey || event.altKey) return
@@ -2598,7 +2613,23 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2598
2613
  }
2599
2614
  document.addEventListener('keydown', onKey)
2600
2615
  return () => document.removeEventListener('keydown', onKey)
2601
- }, [])
2616
+ }, [settingsOpen, adding])
2617
+
2618
+ // The settings card floats over the list, so a click anywhere else dismisses it
2619
+ // the way every other popover on the web does.
2620
+ useEffect(() => {
2621
+ if (!settingsOpen || typeof document === 'undefined') return undefined
2622
+ const onDown = (event) => {
2623
+ const card = settingsRef.current
2624
+ const gear = settingsTriggerRef.current
2625
+ if (card === null) return
2626
+ if (card.contains(event.target) === true) return
2627
+ if (gear !== null && gear.contains(event.target) === true) return
2628
+ setSettingsOpen(false)
2629
+ }
2630
+ document.addEventListener('mousedown', onDown)
2631
+ return () => document.removeEventListener('mousedown', onDown)
2632
+ }, [settingsOpen])
2602
2633
 
2603
2634
  const run = async (id, operation) => {
2604
2635
  setBusyId(id)
@@ -3273,15 +3304,20 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3273
3304
  'div',
3274
3305
  { className: 'gt-head-actions' },
3275
3306
  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' }),
3307
+ 'span',
3308
+ { ref: settingsTriggerRef, style: { display: 'inline-flex' } },
3309
+ h(
3310
+ 'button',
3311
+ {
3312
+ type: 'button',
3313
+ className: 'gt-iconbtn',
3314
+ title: t('settings.title'),
3315
+ 'aria-label': t('settings.title'),
3316
+ 'aria-expanded': settingsOpen,
3317
+ onClick: () => (settingsOpen ? setSettingsOpen(false) : openSettings()),
3318
+ },
3319
+ h(Icon, { name: 'gear' }),
3320
+ ),
3285
3321
  ),
3286
3322
  h(
3287
3323
  'button',
@@ -3323,6 +3359,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3323
3359
  ? h(
3324
3360
  'div',
3325
3361
  {
3362
+ ref: settingsRef,
3326
3363
  className: 'gt-settings',
3327
3364
  role: 'dialog',
3328
3365
  'aria-label': t('settings.title'),
@@ -4108,39 +4145,59 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
4108
4145
  ? h('span', { className: 'gt-dot', title: t('meta.session.live'), 'aria-label': t('meta.session.live') })
4109
4146
  : null,
4110
4147
  ),
4111
- group.sessionId === undefined
4112
- ? null
4113
- : h(
4114
- 'button',
4148
+ // An exemption is a state, so it keeps a small always-visible mark; the
4149
+ // actions themselves live behind one kebab instead of two text buttons
4150
+ // competing with the group title.
4151
+ exemptGroup
4152
+ ? h(
4153
+ 'span',
4115
4154
  {
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
- },
4155
+ className: 'gt-group-flag',
4156
+ title: t('action.exemptHintOn'),
4140
4157
  },
4141
4158
  h(Icon, { name: 'mute', size: 11 }),
4142
- exemptGroup ? t('action.exemptOn') : t('action.exempt'),
4143
- ),
4159
+ )
4160
+ : null,
4161
+ [
4162
+ group.sessionId === undefined
4163
+ ? null
4164
+ : {
4165
+ key: 'capture',
4166
+ label: t('action.captureShort'),
4167
+ icon: 'doc',
4168
+ onSelect: () => void requestCapture(group.sessionId, group.label),
4169
+ },
4170
+ group.dir === undefined || group.dir === ''
4171
+ ? null
4172
+ : {
4173
+ key: 'exempt',
4174
+ label: exemptGroup ? t('action.exemptOn') : t('action.exempt'),
4175
+ icon: 'mute',
4176
+ onSelect: () => void toggleExemptWorkspace(group.dir, !exemptGroup),
4177
+ },
4178
+ ].filter((item) => item !== null).length === 0
4179
+ ? null
4180
+ : h(Menu, {
4181
+ label: t('group.actions'),
4182
+ items: [
4183
+ group.sessionId === undefined
4184
+ ? null
4185
+ : {
4186
+ key: 'capture',
4187
+ label: t('group.capture'),
4188
+ icon: 'doc',
4189
+ onSelect: () => void requestCapture(group.sessionId, group.label),
4190
+ },
4191
+ group.dir === undefined || group.dir === ''
4192
+ ? null
4193
+ : {
4194
+ key: 'exempt',
4195
+ label: exemptGroup ? t('group.exemptOff') : t('group.exemptOn'),
4196
+ icon: 'mute',
4197
+ onSelect: () => void toggleExemptWorkspace(group.dir, !exemptGroup),
4198
+ },
4199
+ ].filter((item) => item !== null && item !== undefined),
4200
+ }),
4144
4201
  ),
4145
4202
  isCollapsed
4146
4203
  ? 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.5'
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.5",
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",