dsh-global-todo 0.5.2 → 0.5.3
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 +15 -0
- package/docs/screenshots/panel-detail.png +0 -0
- package/docs/screenshots/panel-select.png +0 -0
- package/docs/screenshots/panel.png +0 -0
- package/lib/client.js +205 -1
- package/lib/index.js +11 -2
- package/lib/reminders.js +3 -1
- package/lib/routes.js +2 -0
- package/lib/settings.js +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [0.5.3] — 2026-10-20
|
|
2
|
+
|
|
3
|
+
Settings a person can reach, and reminders that behave like reminders.
|
|
4
|
+
|
|
5
|
+
- **A settings popover.** Reminders and the external ingest door were both API-only; the header
|
|
6
|
+
now opens a small panel for the reminder switch, the lead time, the ingest switch, the token
|
|
7
|
+
(generate a fresh one in place; leave blank to keep the current) and the inbox confidence
|
|
8
|
+
threshold. The token still never comes back from the host.
|
|
9
|
+
- **Reminders gained a lead time and a switch.** remindersEnabled and reminderLeadMinutes live in
|
|
10
|
+
settings; the host sweep reads them each tick, announces at dueAt minus lead, and clears the due
|
|
11
|
+
list when reminders are off.
|
|
12
|
+
- **Browser notifications are opt-in.** When the panel is open and a new deadline becomes due, it
|
|
13
|
+
fires a system notification if permission was granted; otherwise the due bar offers to ask for
|
|
14
|
+
permission. The notification is a bonus — the bar is the source of truth.
|
|
15
|
+
|
|
1
16
|
# Changelog
|
|
2
17
|
|
|
3
18
|
All notable changes to this project are documented in this 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 = '
|
|
53
|
+
const UI_REV = 'ui35'
|
|
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
|
|
|
@@ -90,6 +90,19 @@ window.__ModuleLoader__.load({
|
|
|
90
90
|
'action.import': '导入',
|
|
91
91
|
'action.exportHint': '把整份清单导出成 JSON 文件(就是存储用的那份文档)',
|
|
92
92
|
'action.importHint': '从 JSON 文件导入:同 id 覆盖、新 id 追加,不删除现有待办',
|
|
93
|
+
'settings.title': '设置',
|
|
94
|
+
'settings.reminders': '到点提醒',
|
|
95
|
+
'settings.remindersEnable': '开启到点提醒',
|
|
96
|
+
'settings.lead': '提前(分钟)',
|
|
97
|
+
'settings.ingest': '外部接入(ingest)',
|
|
98
|
+
'settings.ingestEnable': '启用外部写入',
|
|
99
|
+
'settings.token': '访问 token',
|
|
100
|
+
'settings.tokenSet': '已设置,留空保持不变',
|
|
101
|
+
'settings.generate': '生成新 token',
|
|
102
|
+
'settings.confirmBelow': '低于此置信度进收件箱',
|
|
103
|
+
'settings.save': '保存',
|
|
104
|
+
'settings.cancel': '取消',
|
|
105
|
+
'settings.saved': '设置已保存',
|
|
93
106
|
'notice.exported': '已导出 {n} 条待办',
|
|
94
107
|
'notice.imported': '已导入 {n} 条(合并,现有共 {total} 条)',
|
|
95
108
|
'import.badJson': '这个文件不是合法的 JSON',
|
|
@@ -138,6 +151,8 @@ window.__ModuleLoader__.load({
|
|
|
138
151
|
'notice.externalChange': '清单在别处被改过,已载入最新内容',
|
|
139
152
|
'reminder.title': '到点提醒({n})',
|
|
140
153
|
'reminder.dismiss': '知道了',
|
|
154
|
+
'reminder.enableNotifications': '开启系统通知',
|
|
155
|
+
'reminder.notifyTitle': '待办到点',
|
|
141
156
|
'import.preview': '将导入 {total} 条:新增 {create}、更新 {update}',
|
|
142
157
|
'import.confirm': '导入',
|
|
143
158
|
'meta.seqHint': '给人看的编号',
|
|
@@ -311,6 +326,19 @@ window.__ModuleLoader__.load({
|
|
|
311
326
|
'action.import': 'Import',
|
|
312
327
|
'action.exportHint': 'Download the whole list as JSON (the document the store keeps)',
|
|
313
328
|
'action.importHint': 'Import a JSON file: same ids are updated, new ids appended, nothing deleted',
|
|
329
|
+
'settings.title': 'Settings',
|
|
330
|
+
'settings.reminders': 'Due reminders',
|
|
331
|
+
'settings.remindersEnable': 'Announce deadlines',
|
|
332
|
+
'settings.lead': 'Lead time (min)',
|
|
333
|
+
'settings.ingest': 'External ingest',
|
|
334
|
+
'settings.ingestEnable': 'Accept external writes',
|
|
335
|
+
'settings.token': 'Access token',
|
|
336
|
+
'settings.tokenSet': 'Set — leave blank to keep',
|
|
337
|
+
'settings.generate': 'New token',
|
|
338
|
+
'settings.confirmBelow': 'Below this confidence → inbox',
|
|
339
|
+
'settings.save': 'Save',
|
|
340
|
+
'settings.cancel': 'Cancel',
|
|
341
|
+
'settings.saved': 'Settings saved',
|
|
314
342
|
'notice.exported': 'Exported {n} todos',
|
|
315
343
|
'notice.imported': 'Imported {n} (merged; {total} in the list now)',
|
|
316
344
|
'import.badJson': 'That file is not valid JSON',
|
|
@@ -359,6 +387,8 @@ window.__ModuleLoader__.load({
|
|
|
359
387
|
'notice.externalChange': 'The list changed somewhere else; the latest content is loaded',
|
|
360
388
|
'reminder.title': 'Due now ({n})',
|
|
361
389
|
'reminder.dismiss': 'Dismiss',
|
|
390
|
+
'reminder.enableNotifications': 'Enable notifications',
|
|
391
|
+
'reminder.notifyTitle': 'Todo due',
|
|
362
392
|
'import.preview': 'Will import {total}: {create} new, {update} updated',
|
|
363
393
|
'import.confirm': 'Import',
|
|
364
394
|
'meta.seqHint': 'Human-facing number',
|
|
@@ -1014,6 +1044,13 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
1014
1044
|
color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; padding: 3px 8px;
|
|
1015
1045
|
border-radius: 999px; cursor: pointer; max-width: 260px; overflow: hidden; text-overflow: ellipsis;
|
|
1016
1046
|
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;
|
|
1052
|
+
color: var(--dsw-alias-label-primary); }
|
|
1053
|
+
.gt-settings-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
1017
1054
|
.gt-inbox { margin-left: 6px; padding: 1px 6px; border-radius: 999px; font-size: 10.5px; font-weight: 600;
|
|
1018
1055
|
color: var(--dsw-alias-state-warn-primary); border: 1px solid var(--dsw-alias-state-warn-primary);
|
|
1019
1056
|
vertical-align: 1px; }
|
|
@@ -2042,6 +2079,20 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2042
2079
|
// A picked file is previewed first: a merge is not undoable, and "did I pick the right
|
|
2043
2080
|
// export" is exactly the kind of question a dry run answers before anything changes.
|
|
2044
2081
|
const [importAsk, setImportAsk] = useState(null)
|
|
2082
|
+
// One settings popover: reminders and the external door both need a place a person can
|
|
2083
|
+
// reach without curl.
|
|
2084
|
+
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
2085
|
+
const [tokenDraft, setTokenDraft] = useState('')
|
|
2086
|
+
const [ingestDraft, setIngestDraft] = useState(false)
|
|
2087
|
+
const [belowDraft, setBelowDraft] = useState('0.8')
|
|
2088
|
+
const [remindersDraft, setRemindersDraft] = useState(true)
|
|
2089
|
+
const [leadDraft, setLeadDraft] = useState('0')
|
|
2090
|
+
const [notificationPerm, setNotificationPerm] = useState(
|
|
2091
|
+
typeof window !== 'undefined' && typeof window.Notification === 'function'
|
|
2092
|
+
? window.Notification.permission
|
|
2093
|
+
: 'unsupported',
|
|
2094
|
+
)
|
|
2095
|
+
const notifiedRef = useRef(new Set())
|
|
2045
2096
|
const [groupMode, setGroupMode] = useState(() => readStoredGroupMode())
|
|
2046
2097
|
// A notice may carry an action: after a hand-off the useful next step is
|
|
2047
2098
|
// to go and look at the conversation the work is actually in.
|
|
@@ -2863,6 +2914,47 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2863
2914
|
return undefined
|
|
2864
2915
|
}
|
|
2865
2916
|
|
|
2917
|
+
/** Fill the popover from the host's state, so saving cannot send stale values. */
|
|
2918
|
+
const openSettings = () => {
|
|
2919
|
+
const current = state.health?.settings ?? {}
|
|
2920
|
+
setIngestDraft(current.ingest?.enabled === true)
|
|
2921
|
+
setBelowDraft(String(current.ingest?.confirmBelow ?? 0.8))
|
|
2922
|
+
setRemindersDraft(current.remindersEnabled !== false)
|
|
2923
|
+
setLeadDraft(String(current.reminderLeadMinutes ?? 0))
|
|
2924
|
+
setTokenDraft('')
|
|
2925
|
+
setSettingsOpen(true)
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
const generateToken = () => {
|
|
2929
|
+
const bytes = new window.Uint8Array(24)
|
|
2930
|
+
window.crypto.getRandomValues(bytes)
|
|
2931
|
+
setTokenDraft(Array.from(bytes).map((byte) => byte.toString(16).padStart(2, '0')).join(''))
|
|
2932
|
+
setIngestDraft(true)
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
const saveSettings = async () => {
|
|
2936
|
+
try {
|
|
2937
|
+
await call('settings', {
|
|
2938
|
+
method: 'POST',
|
|
2939
|
+
body: {
|
|
2940
|
+
remindersEnabled: remindersDraft,
|
|
2941
|
+
reminderLeadMinutes: Number(leadDraft),
|
|
2942
|
+
ingestEnabled: ingestDraft,
|
|
2943
|
+
ingestConfirmBelow: Number(belowDraft),
|
|
2944
|
+
...(tokenDraft.trim() === '' ? {} : { ingestToken: tokenDraft.trim() }),
|
|
2945
|
+
},
|
|
2946
|
+
})
|
|
2947
|
+
setSettingsOpen(false)
|
|
2948
|
+
setTokenDraft('')
|
|
2949
|
+
await refreshHealth()
|
|
2950
|
+
setNotice({ text: t('settings.saved'), sessionId: '' })
|
|
2951
|
+
setTimeout(() => setNotice(null), 5000)
|
|
2952
|
+
} catch (failure) {
|
|
2953
|
+
reportFailure('settings', { message: failure?.message })
|
|
2954
|
+
setError(failure)
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2866
2958
|
/** Acknowledge reminders so the host stops listing them. */
|
|
2867
2959
|
const ackReminders = async (ids) => {
|
|
2868
2960
|
try {
|
|
@@ -2901,6 +2993,30 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
2901
2993
|
|
|
2902
2994
|
const counts = state.counts ?? { total: 0, open: 0, doing: 0, done: 0, cancelled: 0, archived: 0, overdue: 0, inbox: 0 }
|
|
2903
2995
|
const remindersDue = Array.isArray(state.health?.reminders?.due) ? state.health.reminders.due : []
|
|
2996
|
+
const dueKey = remindersDue.map((item) => `${item.id}:${item.dueAt}`).join('|')
|
|
2997
|
+
useEffect(() => {
|
|
2998
|
+
if (typeof window === 'undefined' || typeof window.Notification !== 'function') return
|
|
2999
|
+
for (const item of remindersDue) {
|
|
3000
|
+
const key = `${item.id}:${item.dueAt}`
|
|
3001
|
+
if (notifiedRef.current.has(key)) continue
|
|
3002
|
+
notifiedRef.current.add(key)
|
|
3003
|
+
if (window.Notification.permission !== 'granted') continue
|
|
3004
|
+
try {
|
|
3005
|
+
const note = new window.Notification(t('reminder.notifyTitle'), { body: item.title, tag: item.id })
|
|
3006
|
+
note.onclick = () => {
|
|
3007
|
+
try {
|
|
3008
|
+
window.focus()
|
|
3009
|
+
} catch {
|
|
3010
|
+
/* focus is a nicety */
|
|
3011
|
+
}
|
|
3012
|
+
setExpandedId(item.id)
|
|
3013
|
+
}
|
|
3014
|
+
} catch {
|
|
3015
|
+
/* a notification is a bonus, never a failure */
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3019
|
+
}, [dueKey])
|
|
2904
3020
|
const groups = useMemo(
|
|
2905
3021
|
() => groupTodos(state.todos, Date.now(), groupMode, sessionTitles, t),
|
|
2906
3022
|
[state.todos, groupMode, sessionTitles, lang],
|
|
@@ -3011,6 +3127,16 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3011
3127
|
},
|
|
3012
3128
|
t('action.import'),
|
|
3013
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'),
|
|
3139
|
+
),
|
|
3014
3140
|
h('input', {
|
|
3015
3141
|
ref: importRef,
|
|
3016
3142
|
type: 'file',
|
|
@@ -3020,6 +3146,71 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3020
3146
|
onChange: (event) => void importJson(event),
|
|
3021
3147
|
}),
|
|
3022
3148
|
),
|
|
3149
|
+
settingsOpen
|
|
3150
|
+
? h(
|
|
3151
|
+
'div',
|
|
3152
|
+
{ className: 'gt-settings', role: 'dialog', 'aria-label': t('settings.title') },
|
|
3153
|
+
h('div', { className: 'gt-settings-head' }, t('settings.title')),
|
|
3154
|
+
h('div', { className: 'gt-section-title' }, t('settings.reminders')),
|
|
3155
|
+
h(
|
|
3156
|
+
'label',
|
|
3157
|
+
{ className: 'gt-check-line' },
|
|
3158
|
+
h('input', { type: 'checkbox', checked: remindersDraft, onChange: (event) => setRemindersDraft(event.target.checked) }),
|
|
3159
|
+
t('settings.remindersEnable'),
|
|
3160
|
+
),
|
|
3161
|
+
h(
|
|
3162
|
+
'label',
|
|
3163
|
+
{ className: 'gt-check-line' },
|
|
3164
|
+
t('settings.lead'),
|
|
3165
|
+
h('input', {
|
|
3166
|
+
type: 'number',
|
|
3167
|
+
min: 0,
|
|
3168
|
+
max: 1440,
|
|
3169
|
+
className: 'gt-input gt-input-xs',
|
|
3170
|
+
value: leadDraft,
|
|
3171
|
+
onChange: (event) => setLeadDraft(event.target.value),
|
|
3172
|
+
}),
|
|
3173
|
+
),
|
|
3174
|
+
h('div', { className: 'gt-section-title' }, t('settings.ingest')),
|
|
3175
|
+
h(
|
|
3176
|
+
'label',
|
|
3177
|
+
{ className: 'gt-check-line' },
|
|
3178
|
+
h('input', { type: 'checkbox', checked: ingestDraft, onChange: (event) => setIngestDraft(event.target.checked) }),
|
|
3179
|
+
t('settings.ingestEnable'),
|
|
3180
|
+
),
|
|
3181
|
+
h('input', {
|
|
3182
|
+
className: 'gt-input',
|
|
3183
|
+
type: 'password',
|
|
3184
|
+
value: tokenDraft,
|
|
3185
|
+
placeholder:
|
|
3186
|
+
state.health?.settings?.ingest?.tokenSet === true ? t('settings.tokenSet') : t('settings.token'),
|
|
3187
|
+
autoComplete: 'new-password',
|
|
3188
|
+
onChange: (event) => setTokenDraft(event.target.value),
|
|
3189
|
+
}),
|
|
3190
|
+
h(
|
|
3191
|
+
'div',
|
|
3192
|
+
{ className: 'gt-settings-row' },
|
|
3193
|
+
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
|
+
h('button', { type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => setSettingsOpen(false) }, t('settings.cancel')),
|
|
3196
|
+
),
|
|
3197
|
+
h(
|
|
3198
|
+
'label',
|
|
3199
|
+
{ className: 'gt-check-line' },
|
|
3200
|
+
t('settings.confirmBelow'),
|
|
3201
|
+
h('input', {
|
|
3202
|
+
type: 'number',
|
|
3203
|
+
min: 0,
|
|
3204
|
+
max: 1,
|
|
3205
|
+
step: 0.05,
|
|
3206
|
+
className: 'gt-input gt-input-xs',
|
|
3207
|
+
value: belowDraft,
|
|
3208
|
+
onChange: (event) => setBelowDraft(event.target.value),
|
|
3209
|
+
}),
|
|
3210
|
+
),
|
|
3211
|
+
)
|
|
3212
|
+
: null,
|
|
3213
|
+
|
|
3023
3214
|
error !== null
|
|
3024
3215
|
? h(
|
|
3025
3216
|
'div',
|
|
@@ -3462,6 +3653,19 @@ button.gt-chip[data-on="1"] { color: var(--dsw-alias-label-primary); border-colo
|
|
|
3462
3653
|
`${item.seq > 0 ? `#${item.seq} ` : ''}${item.title}`,
|
|
3463
3654
|
),
|
|
3464
3655
|
),
|
|
3656
|
+
notificationPerm === 'default'
|
|
3657
|
+
? h(
|
|
3658
|
+
'button',
|
|
3659
|
+
{
|
|
3660
|
+
type: 'button',
|
|
3661
|
+
className: 'gt-btn gt-btn-quiet',
|
|
3662
|
+
onClick: () => {
|
|
3663
|
+
void window.Notification.requestPermission().then((permission) => setNotificationPerm(permission))
|
|
3664
|
+
},
|
|
3665
|
+
},
|
|
3666
|
+
t('reminder.enableNotifications'),
|
|
3667
|
+
)
|
|
3668
|
+
: null,
|
|
3465
3669
|
h(
|
|
3466
3670
|
'button',
|
|
3467
3671
|
{ type: 'button', className: 'gt-btn gt-btn-quiet', onClick: () => void ackReminders(remindersDue.map((item) => item.id)) },
|
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.
|
|
39
|
+
export const VERSION = '0.5.3'
|
|
40
40
|
|
|
41
41
|
/** Plugin id. Also the Loader row id in `cordis.patch.yml`. */
|
|
42
42
|
export const name = 'dsh-global-todo'
|
|
@@ -418,8 +418,17 @@ export function apply(ctx, config) {
|
|
|
418
418
|
ctx.effect(() => {
|
|
419
419
|
const tick = async () => {
|
|
420
420
|
try {
|
|
421
|
+
const current = settings.get()
|
|
422
|
+
if (current.remindersEnabled === false) {
|
|
423
|
+
reminderState.due = []
|
|
424
|
+
return
|
|
425
|
+
}
|
|
421
426
|
const { todos } = await service.list({ status: 'active' })
|
|
422
|
-
const due = dueReminders(todos, {
|
|
427
|
+
const due = dueReminders(todos, {
|
|
428
|
+
now: Date.now(),
|
|
429
|
+
reminded: reminderState.reminded,
|
|
430
|
+
leadMs: (current.reminderLeadMinutes ?? 0) * 60_000,
|
|
431
|
+
})
|
|
423
432
|
if (due.length === 0) return
|
|
424
433
|
for (const item of due) reminderState.reminded[item.id] = item.dueAt
|
|
425
434
|
const known = new Set(reminderState.due.map((item) => item.id))
|
package/lib/reminders.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export function dueReminders(todos, options = {}) {
|
|
19
19
|
const now = Number.isFinite(options.now) ? options.now : Date.now()
|
|
20
|
+
// A lead time turns "at the deadline" into "a little before it": the same rule, shifted.
|
|
21
|
+
const lead = Number.isFinite(options.leadMs) && options.leadMs > 0 ? options.leadMs : 0
|
|
20
22
|
const reminded = options.reminded !== null && typeof options.reminded === 'object' ? options.reminded : {}
|
|
21
23
|
const due = []
|
|
22
24
|
for (const todo of Array.isArray(todos) ? todos : []) {
|
|
@@ -24,7 +26,7 @@ export function dueReminders(todos, options = {}) {
|
|
|
24
26
|
if (todo.status !== 'open' && todo.status !== 'doing') continue
|
|
25
27
|
if (todo.dueAt === null || todo.dueAt === undefined || typeof todo.dueAt !== 'string') continue
|
|
26
28
|
const at = Date.parse(todo.dueAt)
|
|
27
|
-
if (!Number.isFinite(at) || at > now) continue
|
|
29
|
+
if (!Number.isFinite(at) || at - lead > now) continue
|
|
28
30
|
if (reminded[todo.id] === todo.dueAt) continue
|
|
29
31
|
due.push({
|
|
30
32
|
id: todo.id,
|
package/lib/routes.js
CHANGED
|
@@ -417,6 +417,8 @@ export function createRoutes(deps) {
|
|
|
417
417
|
loadError: settingsInfo.error,
|
|
418
418
|
// The token itself never leaves the settings file; the panel only needs to know
|
|
419
419
|
// whether one is set before it can usefully show the ingest section.
|
|
420
|
+
remindersEnabled: deps.settings?.get?.().remindersEnabled !== false,
|
|
421
|
+
reminderLeadMinutes: deps.settings?.get?.().reminderLeadMinutes ?? 0,
|
|
420
422
|
ingest: {
|
|
421
423
|
enabled: deps.settings?.get?.().ingestEnabled === true,
|
|
422
424
|
sources: deps.settings?.get?.().ingestSources ?? [],
|
package/lib/settings.js
CHANGED
|
@@ -66,6 +66,10 @@ export const SETTINGS_DEFAULTS = Object.freeze({
|
|
|
66
66
|
ingestSources: [...INGEST_SOURCES],
|
|
67
67
|
/** Items below this confidence wait in the inbox instead of joining the list. */
|
|
68
68
|
ingestConfirmBelow: 0.8,
|
|
69
|
+
/** Announce deadlines in the panel. On by default; a deadline nobody hears about is just data. */
|
|
70
|
+
remindersEnabled: true,
|
|
71
|
+
/** Announce this many minutes before the deadline (0 = at the deadline). */
|
|
72
|
+
reminderLeadMinutes: 0,
|
|
69
73
|
})
|
|
70
74
|
|
|
71
75
|
/**
|
|
@@ -105,6 +109,7 @@ export function normalizeSettings(raw) {
|
|
|
105
109
|
sources.push(kind)
|
|
106
110
|
}
|
|
107
111
|
const below = Number(source.ingestConfirmBelow)
|
|
112
|
+
const lead = Number(source.reminderLeadMinutes)
|
|
108
113
|
return {
|
|
109
114
|
autoCollect: source.autoCollect === true,
|
|
110
115
|
cooldownMinutes: Number.isInteger(minutes) ? Math.min(Math.max(minutes, 5), 24 * 60) : SETTINGS_DEFAULTS.cooldownMinutes,
|
|
@@ -117,6 +122,8 @@ export function normalizeSettings(raw) {
|
|
|
117
122
|
ingestToken: typeof source.ingestToken === 'string' ? source.ingestToken.trim().slice(0, 512) : '',
|
|
118
123
|
ingestSources: sources.length === 0 ? [...INGEST_SOURCES] : sources,
|
|
119
124
|
ingestConfirmBelow: Number.isFinite(below) ? Math.min(Math.max(below, 0), 1) : SETTINGS_DEFAULTS.ingestConfirmBelow,
|
|
125
|
+
remindersEnabled: source.remindersEnabled !== false,
|
|
126
|
+
reminderLeadMinutes: Number.isFinite(lead) ? Math.min(Math.max(lead, 0), 24 * 60) : SETTINGS_DEFAULTS.reminderLeadMinutes,
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-global-todo",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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",
|