dsh-global-todo 0.6.0 → 0.6.2

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,31 @@
1
+ ## [0.6.2] — 2026-10-20
2
+
3
+ The narrow-width details the spec called for after the two-column work.
4
+
5
+ - **Grouping becomes a dropdown when the column is narrow.** The three segmented buttons collapse
6
+ into one `select` (Conversation / Time / Level) below 960px, so the toolbar stops taking a row
7
+ for grouping at the width where rows are already tight.
8
+ - **The inbox explains itself.** Opening the inbox view now leads with one line: items were written
9
+ by external sources or the agent and only join the list after confirmation.
10
+ - **Toasts are anchored to the panel.** `position: absolute` on the panel root instead of `fixed`
11
+ against the viewport, so a notice appears above the list rather than at the bottom of the window.
12
+
13
+ ## [0.6.1] — 2026-10-20
14
+
15
+ The follow-up pass from the design spec: the breakpoint now follows the panel, not the window,
16
+ and the form stops competing with itself.
17
+
18
+ - **Panel-measured breakpoint.** A `ResizeObserver` on the column sets `data-wide`; the detail card
19
+ becomes a right-hand card only when the column itself is at least 960px. At 1100 the detail now
20
+ expands inline under its row instead of squeezing the list into a 416px strip.
21
+ - **Inline detail stops repeating itself.** The card title is hidden in inline mode (the row title
22
+ is directly above it) and keeps only the close affordance.
23
+ - **Add form.** The header action turns into an icon-only close while the form is open, so there is
24
+ one `+ Add` on screen; the form orders 更多字段 before 添加, matching the spec.
25
+ - **Rows are 72px tall** at minimum, matching the spec's rhythm, and the detail/card radius is 10px.
26
+ - Dark theme verified against the real host: layer-2 rows, overlay inspector and ghost-active
27
+ selected states all resolve correctly.
28
+
1
29
  ## [0.6.0] — 2026-10-20
2
30
 
3
31
  A design pass over the whole panel: one list, fewer competing controls, one type scale.
Binary file
Binary file
Binary file
Binary file
Binary file
package/lib/client.js CHANGED
@@ -50,7 +50,7 @@ window.__ModuleLoader__.load({
50
50
  * different mechanisms (restart vs. hot reload), so "which one am I looking
51
51
  * at" needs an answer that the bundle itself carries.
52
52
  */
53
- const UI_REV = 'ui36'
53
+ const UI_REV = 'ui38'
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,7 @@ window.__ModuleLoader__.load({
106
106
  'settings.cancel': '取消',
107
107
  'settings.saved': '设置已保存',
108
108
  'settings.about': '关于',
109
+ 'inbox.explain': '以下由外部写入或 Agent 提交,确认后才进入清单。',
109
110
  'notice.exported': '已导出 {n} 条待办',
110
111
  'notice.imported': '已导入 {n} 条(合并,现有共 {total} 条)',
111
112
  'import.badJson': '这个文件不是合法的 JSON',
@@ -347,6 +348,7 @@ window.__ModuleLoader__.load({
347
348
  'settings.cancel': 'Cancel',
348
349
  'settings.saved': 'Settings saved',
349
350
  'settings.about': 'About',
351
+ 'inbox.explain': 'Written by external sources or the agent. Confirm it before it joins the list.',
350
352
  'notice.exported': 'Exported {n} todos',
351
353
  'notice.imported': 'Imported {n} (merged; {total} in the list now)',
352
354
  'import.badJson': 'That file is not valid JSON',
@@ -908,7 +910,7 @@ window.__ModuleLoader__.load({
908
910
  // ------------------------------------------------------------------ 样式
909
911
 
910
912
  const CSS = `
911
- .gt-root { display: flex; flex-direction: column; height: 100%; min-height: 0; min-width: 0;
913
+ .gt-root { position: relative; display: flex; flex-direction: column; height: 100%; min-height: 0; min-width: 0;
912
914
  color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.5; }
913
915
  .gt-inner { display: flex; flex-direction: column; gap: 14px; width: 100%; max-width: 1040px; min-width: 0;
914
916
  box-sizing: border-box; margin: 0 auto; padding: 20px 24px 32px; flex: 1; }
@@ -1051,7 +1053,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1051
1053
  .gt-list { display: flex; flex-direction: column; gap: 8px; }
1052
1054
  .gt-item { position: relative; display: flex; flex-direction: column; gap: 8px;
1053
1055
  background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1);
1054
- border-radius: 12px; padding: 12px 14px; }
1056
+ border-radius: 10px; padding: 12px 14px; min-height: 72px; box-sizing: border-box; }
1055
1057
  .gt-item:hover { border-color: var(--dsw-alias-border-l2); }
1056
1058
  .gt-item[data-status="doing"] { border-left: 2px solid var(--dsw-alias-brand-primary); }
1057
1059
  .gt-item[data-overdue="1"] { border-left: 2px solid var(--dsw-alias-state-error-primary); }
@@ -1091,6 +1093,13 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1091
1093
  .gt-settings { display: flex; flex-direction: column; gap: 12px; padding: 16px; width: 340px;
1092
1094
  box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px;
1093
1095
  background: var(--dsw-alias-bg-overlay); font-size: 13px; line-height: 20px; }
1096
+ .gt-group-select { display: none; min-height: 28px; max-width: 150px; padding: 0 8px; font: inherit;
1097
+ font-size: 12.5px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1);
1098
+ border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; }
1099
+ .gt-root[data-wide="0"] .gt-group-tabs { display: none; }
1100
+ .gt-root[data-wide="0"] .gt-group-select { display: inline-flex; align-items: center; }
1101
+ .gt-inbox-note { padding: 0 2px 2px; color: var(--dsw-alias-label-secondary); font-size: 13px;
1102
+ line-height: 20px; }
1094
1103
  .gt-settings-path { font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1095
1104
  .gt-settings .gt-input { flex: 0 0 auto; width: 100%; min-height: 32px; }
1096
1105
  .gt-settings .gt-check-line .gt-input { width: 64px; }
@@ -1105,6 +1114,9 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1105
1114
  background: var(--dsw-alias-bg-layer-1); }
1106
1115
  .gt-context-details[data-open="1"] { display: flex; }
1107
1116
  .gt-detail-head { display: flex; align-items: flex-start; gap: 8px; justify-content: space-between; }
1117
+ /* Inline detail sits directly under its row: repeating the title would read as a second item. */
1118
+ .gt-root[data-wide="0"][data-wide-detail="1"] .gt-detail-title { display: none; }
1119
+ .gt-root[data-wide="0"][data-wide-detail="1"] .gt-detail-head { justify-content: flex-end; }
1108
1120
  .gt-detail-title { font-size: 14px; line-height: 22px; font-weight: 600; min-width: 0;
1109
1121
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1110
1122
  .gt-truncated { color: var(--dsw-alias-state-warn-primary); font-size: 12px; }
@@ -1139,12 +1151,12 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1139
1151
  /* Wide screens: the open row's detail becomes a card on the right instead of pushing the list
1140
1152
  down. Deliberately CSS-only over unchanged markup: one detail implementation, and it only
1141
1153
  applies where there is room for a second column. */
1142
- @media (min-width: 1224px) {
1154
+ /* Wide layout follows the panel's measured width (data-wide), not the viewport. */
1143
1155
  /* Only the list gives way: the title row and the toolbar keep their full width, so the
1144
1156
  controls never wrap or hide behind the card. */
1145
- .gt-root[data-wide-detail="1"] .gt-groups { padding-right: 360px; }
1146
- .gt-root[data-wide-detail="1"] .gt-inner { position: relative; }
1147
- .gt-root[data-wide-detail="1"] .gt-item[data-open="1"] .gt-detail {
1157
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-groups { padding-right: 360px; }
1158
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-inner { position: relative; }
1159
+ .gt-root[data-wide="1"][data-wide-detail="1"] .gt-item[data-open="1"] .gt-detail {
1148
1160
  /* Anchored to the panel's own column (.gt-inner), not to the viewport: the slot does not
1149
1161
  always fill the window, and a viewport-centred offset put the card over the list. */
1150
1162
  /* The var fallback matters for the static previews (server-rendered, so effects never
@@ -1158,7 +1170,6 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1158
1170
  overflow: auto; padding: 16px; margin: 0; border: 1px solid var(--dsw-alias-border-l2);
1159
1171
  border-radius: 12px; background: var(--dsw-alias-bg-overlay); z-index: 30;
1160
1172
  }
1161
- }
1162
1173
  .gt-section { display: flex; flex-direction: column; gap: 5px; }
1163
1174
  .gt-completion { white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
1164
1175
  .gt-section-title { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; font-weight: 600; }
@@ -1185,7 +1196,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
1185
1196
  .gt-empty-title { font-size: 14px; color: var(--dsw-alias-label-primary); }
1186
1197
  .gt-foot { color: var(--dsw-alias-label-secondary); font-size: 11px; display: flex; gap: 10px; flex-wrap: wrap;
1187
1198
  align-items: center; }
1188
- .gt-toast { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); z-index: 5;
1199
+ .gt-toast { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 50;
1189
1200
  display: flex; align-items: center; gap: 12px; background: var(--dsw-alias-bg-overlay);
1190
1201
  border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; padding: 9px 13px;
1191
1202
  color: var(--dsw-alias-label-primary); }
@@ -2166,6 +2177,10 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2166
2177
  // reach without curl.
2167
2178
  const [settingsOpen, setSettingsOpen] = useState(false)
2168
2179
  const [settingsBox, setSettingsBox] = useState(null)
2180
+ // The detail card's breakpoint follows the panel's own width, not the viewport: the
2181
+ // slot is ~265px narrower than the window, so a viewport query opens the two-column
2182
+ // layout while the panel is still too narrow for it.
2183
+ const [panelWidth, setPanelWidth] = useState(null)
2169
2184
  const [contextOpen, setContextOpen] = useState(false)
2170
2185
  const [tokenDraft, setTokenDraft] = useState('')
2171
2186
  const [ingestDraft, setIngestDraft] = useState(false)
@@ -2406,6 +2421,20 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
2406
2421
  }
2407
2422
  }, [lang])
2408
2423
 
2424
+ useEffect(() => {
2425
+ const element = innerRef.current
2426
+ if (element === null || typeof window === 'undefined') return undefined
2427
+ const measure = () => setPanelWidth(element.getBoundingClientRect().width)
2428
+ measure()
2429
+ window.addEventListener('resize', measure)
2430
+ const observer = typeof window.ResizeObserver === 'function' ? new window.ResizeObserver(measure) : null
2431
+ if (observer !== null) observer.observe(element)
2432
+ return () => {
2433
+ window.removeEventListener('resize', measure)
2434
+ if (observer !== null) observer.disconnect()
2435
+ }
2436
+ }, [])
2437
+
2409
2438
  useEffect(() => {
2410
2439
  if (!settingsOpen || typeof window === 'undefined') {
2411
2440
  setSettingsBox(null)
@@ -3157,6 +3186,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3157
3186
  {
3158
3187
  className: 'gt-root',
3159
3188
  'data-wide-detail': expandedId === '' ? '0' : '1',
3189
+ 'data-wide': panelWidth === null || panelWidth >= 960 ? '1' : '0',
3160
3190
  // The slot does not always fill the window, so the card's viewport offsets are
3161
3191
  // measured from the panel itself instead of assumed from 50vw.
3162
3192
  style:
@@ -3198,15 +3228,16 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3198
3228
  'button',
3199
3229
  {
3200
3230
  type: 'button',
3201
- className: 'gt-btn gt-btn-primary',
3202
- 'aria-expanded': adding,
3203
- onClick: () => {
3204
- setAdding((value) => !value)
3205
- setMore(false)
3231
+ className: adding ? 'gt-btn gt-btn-quiet' : 'gt-btn gt-btn-primary',
3232
+ 'aria-expanded': adding,
3233
+ title: adding ? t('action.less') : t('action.add'),
3234
+ onClick: () => {
3235
+ setAdding((value) => !value)
3236
+ setMore(false)
3237
+ },
3206
3238
  },
3207
- },
3208
- h(Icon, { name: 'plus', size: 13 }),
3209
- t('action.add'),
3239
+ h(Icon, { name: adding ? 'close' : 'plus', size: 13 }),
3240
+ adding ? '' : t('action.add'),
3210
3241
  ),
3211
3242
  // One overflow menu instead of four competing buttons: the primary action is
3212
3243
  // adding a todo; everything else is occasional.
@@ -3351,14 +3382,14 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3351
3382
  }),
3352
3383
  h(
3353
3384
  'button',
3354
- { type: 'button', className: 'gt-btn gt-btn-primary', disabled: busyId === '__new__' || draft.title.trim() === '', onClick: () => void submit() },
3355
- h(Icon, { name: 'plus', size: 13 }),
3356
- t('action.add'),
3385
+ { type: 'button', className: 'gt-btn gt-btn-quiet', 'aria-expanded': more, onClick: () => setMore((value) => !value) },
3386
+ more ? t('action.less') : t('action.more'),
3357
3387
  ),
3358
3388
  h(
3359
3389
  'button',
3360
- { type: 'button', className: 'gt-btn gt-btn-quiet', 'aria-expanded': more, onClick: () => setMore((value) => !value) },
3361
- more ? t('action.less') : t('action.more'),
3390
+ { type: 'button', className: 'gt-btn gt-btn-primary', disabled: busyId === '__new__' || draft.title.trim() === '', onClick: () => void submit() },
3391
+ h(Icon, { name: 'plus', size: 13 }),
3392
+ t('action.add'),
3362
3393
  ),
3363
3394
  ),
3364
3395
  more
@@ -3455,7 +3486,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3455
3486
  h('div', { className: 'gt-spacer' }),
3456
3487
  h(
3457
3488
  'span',
3458
- { className: 'gt-tabs', role: 'group', 'aria-label': t('group.by'), title: t('group.by') },
3489
+ { className: 'gt-tabs gt-group-tabs', role: 'group', 'aria-label': t('group.by'), title: t('group.by') },
3459
3490
  GROUP_MODES.map((mode) =>
3460
3491
  h(
3461
3492
  'button',
@@ -3473,6 +3504,20 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3473
3504
  ),
3474
3505
  ),
3475
3506
  ),
3507
+ h(
3508
+ 'select',
3509
+ {
3510
+ className: 'gt-group-select',
3511
+ value: groupMode,
3512
+ 'aria-label': t('group.by'),
3513
+ title: t('group.by'),
3514
+ onChange: (event) => {
3515
+ setGroupMode(event.target.value)
3516
+ storeStoredGroupMode(event.target.value)
3517
+ },
3518
+ },
3519
+ GROUP_MODES.map((mode) => h('option', { key: mode, value: mode }, t(`group.${mode}`))),
3520
+ ),
3476
3521
  ),
3477
3522
  h('div', { className: 'gt-spacer' }),
3478
3523
  h(
@@ -3888,6 +3933,7 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
3888
3933
  : h(
3889
3934
  'div',
3890
3935
  { className: 'gt-groups', role: 'list' },
3936
+ filter === 'inbox' ? h('div', { className: 'gt-inbox-note', role: 'note' }, t('inbox.explain')) : null,
3891
3937
  groups.map((group) => {
3892
3938
  const isCollapsed = collapsed[group.key] ?? group.collapsed === true
3893
3939
  const exemptGroup = typeof group.dir === 'string' && group.dir !== '' && exemptWorkspaces.includes(group.dir)
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.0'
39
+ export const VERSION = '0.6.2'
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.0",
3
+ "version": "0.6.2",
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",