@goodandready/dsh-task-tracker 0.0.0-stage → 0.2.13

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/lib/client.js ADDED
@@ -0,0 +1,1901 @@
1
+ window.__ModuleLoader__.load({
2
+ id: '@goodandready/dsh-task-tracker',
3
+ factory: (require) => {
4
+ var module = { exports: {} }
5
+ const React = require('react')
6
+ const h = React.createElement
7
+
8
+ let ReactDOM = null
9
+ try {
10
+ ReactDOM = require('react-dom/client')
11
+ } catch (_) {
12
+ try {
13
+ ReactDOM = require('react-dom')
14
+ } catch (_) {
15
+ ReactDOM = null
16
+ }
17
+ }
18
+
19
+ const NS = 'dsh-task-tracker'
20
+ // Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
21
+ const PKG = '@goodandready/dsh-task-tracker'
22
+ const ROW_ID = 'dsh-task-tracker'
23
+ const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
24
+ const PANEL_ID = 'dsh-task-tracker'
25
+ const ACTIVATE_EVENT = 'dsh:active-panel'
26
+ const DATA_CHANGE_EVENT = 'dsh-task-tracker:data-changed'
27
+
28
+ function notifyDataChanged() {
29
+ if (typeof document !== 'undefined') {
30
+ try {
31
+ document.dispatchEvent(new CustomEvent(DATA_CHANGE_EVENT))
32
+ } catch (_) { /* bestEffort */ }
33
+ }
34
+ }
35
+ const COLUMN_SELECTOR = '[data-pane=conversation], [class*=centerCol]'
36
+
37
+ let ChevronIcon = null
38
+ try {
39
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
40
+ ChevronIcon = primitives && primitives.IconChevronDownOutline14
41
+ } catch (_) {
42
+ ChevronIcon = null
43
+ }
44
+
45
+ function FallbackChevron(props) {
46
+ return h(
47
+ 'svg',
48
+ {
49
+ width: 14,
50
+ height: 14,
51
+ viewBox: '0 0 14 14',
52
+ fill: 'none',
53
+ stroke: 'currentColor',
54
+ strokeWidth: 1.5,
55
+ className: props.className,
56
+ style: props.style,
57
+ },
58
+ h('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' }),
59
+ )
60
+ }
61
+ const Chevron = ChevronIcon || FallbackChevron
62
+
63
+ const ICON_TASK = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>'
64
+ const BACK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3.5 5.5 8l4.5 4.5"/></svg>'
65
+
66
+ const POPULAR_EMOJIS = [
67
+ '📁', '📥', '💼', '🚀', '💡', '📌', '🎯', '⚡', '🔥', '📦',
68
+ '🏷️', '📝', '⭐', '🛠️', '💬', '📊', '🎨', '❤️', '✅', '🔍',
69
+ ]
70
+
71
+ const CSS =
72
+ '.tt-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
73
+ '.tt-head{appearance:none;width:100%;font:inherit;text-align:left;display:flex;align-items:center;padding:16px 20px;cursor:pointer;background:transparent;border:0;color:inherit;outline:none}' +
74
+ '.tt-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
75
+ '.tt-sub{font-size:12px;color:var(--dsw-alias-label-secondary);margin-left:12px;flex:1}' +
76
+ '.tt-chev{width:14px;height:14px;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}' +
77
+ '.tt-chev-open{transform:rotate(180deg)}' +
78
+ '.tt-body{padding:0 20px 20px;border-top:1px solid var(--dsw-alias-border-l1)}' +
79
+ '.tt-field{display:flex;flex-direction:column;gap:6px;margin-top:14px}' +
80
+ '.tt-input{height:36px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:0 12px;color:var(--dsw-alias-label-primary);font-size:13px;outline:none}' +
81
+ '.tt-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:16px;align-items:center}' +
82
+ '.tt-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:500;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-bg-base)}' +
83
+ '.tt-chip-btn{appearance:none;background:0 0;border:0;padding:4px;cursor:pointer;color:var(--dsw-alias-label-secondary);display:flex;align-items:center;position:relative;border-radius:6px;line-height:1}' +
84
+ '.tt-chip-badge{position:absolute;top:-2px;right:-4px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-bg-base);font-size:9px;font-weight:700;padding:0 4px;border-radius:10px;line-height:13px;min-width:13px;text-align:center}' +
85
+ '[data-dsh-task-tracker-view]{display:none!important;position:absolute;inset:0;z-index:60;background:var(--dsw-alias-bg-base)}' +
86
+ 'html[data-dsh-task-tracker-open] [data-dsh-task-tracker-view]{display:flex!important}' +
87
+ 'html[data-dsh-task-tracker-open] [data-pane=conversation]>:not([data-dsh-task-tracker-view]){display:none!important}' +
88
+ 'html[data-dsh-task-tracker-open] [class*=centerCol]>:not([data-dsh-task-tracker-view]){display:none!important}' +
89
+ '.tt-screen-container{display:flex;flex-direction:column;width:100%;height:100%;min-height:0;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);position:relative;overflow:hidden}' +
90
+ '.tt-screen-head{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);flex:none}' +
91
+ '.tt-screen-title{font-size:16px;font-weight:600;display:flex;align-items:center;gap:8px}' +
92
+ '.tt-back-btn{appearance:none;background:transparent;border:0;color:var(--dsw-alias-label-secondary);cursor:pointer;display:inline-flex;align-items:center;gap:4px;font-size:13px;padding:4px 8px;border-radius:6px}' +
93
+ '.tt-search-input{height:30px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:0 10px;color:var(--dsw-alias-label-primary);font-size:12px;width:130px;outline:none}' +
94
+ '.tt-add-main-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:4px 12px;font-size:13px;font-weight:500;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-bg-base);height:30px;display:inline-flex;align-items:center;gap:6px}.tt-quick-add-row{display:flex;align-items:center;gap:8px;padding:4px 0 8px;flex:none}.tt-quick-add-input{flex:1;height:34px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:0 12px;color:var(--dsw-alias-label-primary);font-size:13px;outline:none}.tt-quick-add-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:0 14px;font-size:13px;font-weight:500;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-bg-base);height:34px;display:inline-flex;align-items:center;gap:6px;flex:none}.tt-empty-box{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;gap:12px;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center}.tt-updater-block{margin-top:12px;padding:12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px}.tt-updater-info{font-size:12px;display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary)}.tt-badge-ok{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:color-mix(in srgb, var(--dsw-alias-button-primary-fill) 15%, transparent);color:var(--dsw-alias-button-primary-fill)}.tt-badge-warn{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 20%, transparent);color:var(--dsw-alias-state-warn-primary)}.tt-badge-err{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 20%, transparent);color:var(--dsw-alias-state-error-primary)}' +
95
+ '.tt-close-btn{appearance:none;background:0 0;border:0;color:var(--dsw-alias-label-secondary);font-size:16px;cursor:pointer;padding:4px 8px;border-radius:6px;line-height:1}' +
96
+ '.tt-filter-group{display:flex;align-items:center;gap:4px;background:var(--dsw-alias-bg-layer-2);padding:2px 4px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2)}' +
97
+ '.tt-filter-btn{background:transparent;border:0;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;padding:3px 8px;border-radius:6px;cursor:pointer;transition:all .15s}' +
98
+ '.tt-filter-btn-active{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}' +
99
+ '.tt-tasks-main{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:12px 16px;display:flex;flex-direction:column;gap:6px}' +
100
+ '.tt-task-row{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;transition:border-color .15s, background .15s;user-select:none}' +
101
+ '.tt-task-row:hover{background:color-mix(in srgb, var(--dsw-alias-label-primary) 3%, var(--dsw-alias-bg-layer-3));border-color:color-mix(in srgb, var(--dsw-alias-border-l2) 80%, var(--dsw-alias-label-primary))}' +
102
+ '.tt-task-check{appearance:none;width:16px;height:16px;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;cursor:pointer;background:transparent;display:flex;align-items:center;justify-content:center;flex:none;transition:all .15s}' +
103
+ '.tt-task-check-checked{background:var(--dsw-alias-button-primary-fill);border-color:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-bg-base)}' +
104
+ '.tt-task-title{flex:1;min-width:0;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
105
+ '.tt-task-title-done{text-decoration:line-through;color:var(--dsw-alias-label-tertiary)}' +
106
+ '.tt-task-priority-badge{font-size:10px;font-weight:600;padding:2px 6px;border-radius:6px;flex:none;text-transform:uppercase}' +
107
+ '.tt-priority-0{display:none}' +
108
+ '.tt-priority-1{background:color-mix(in srgb, var(--dsw-alias-label-secondary) 15%, transparent);color:var(--dsw-alias-label-secondary)}' +
109
+ '.tt-priority-2{background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 20%, transparent);color:var(--dsw-alias-state-warn-primary)}' +
110
+ '.tt-priority-3{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 20%, transparent);color:var(--dsw-alias-state-error-primary)}' +
111
+ '.tt-task-action-btn{appearance:none;background:0 0;border:0;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-tertiary);padding:4px;border-radius:4px;display:flex;align-items:center;justify-content:center;flex:none}' +
112
+ '.tt-bottom-shelf{flex:none;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:8px 16px;display:flex;flex-direction:column;gap:6px}' +
113
+ '.tt-shelf-top{display:flex;align-items:center;justify-content:space-between;padding:0 2px}' +
114
+ '.tt-folders-grid{display:flex;flex-wrap:wrap;gap:8px;max-height:124px;overflow-y:auto;align-items:flex-start}' +
115
+ '.tt-folder-tile{width:140px;min-height:52px;max-height:56px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);padding:6px 8px;display:flex;align-items:center;gap:8px;cursor:pointer;position:relative;box-sizing:border-box;user-select:none;transition:border-color .15s, background .15s}' +
116
+ '.tt-folder-tile:hover{background:color-mix(in srgb, var(--dsw-alias-label-primary) 4%, var(--dsw-alias-bg-layer-3))}' +
117
+ '.tt-folder-tile-active{border-color:var(--dsw-alias-button-primary-fill);background:color-mix(in srgb, var(--dsw-alias-button-primary-fill) 12%, var(--dsw-alias-bg-layer-3))}' +
118
+ '.tt-folder-tile-dragover{border-color:var(--dsw-alias-button-primary-fill);box-shadow:0 0 0 2px var(--dsw-alias-button-primary-fill);background:color-mix(in srgb, var(--dsw-alias-button-primary-fill) 20%, var(--dsw-alias-bg-layer-3))}' +
119
+ '.tt-folder-icon-btn{appearance:none;background:0 0;border:0;padding:2px;font-size:18px;cursor:pointer;line-height:1;border-radius:4px;display:flex;align-items:center;justify-content:center;flex:none}' +
120
+ '.tt-folder-title-box{flex:1;min-width:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word;font-size:12px;line-height:1.25;font-weight:500;color:var(--dsw-alias-label-primary)}' +
121
+ '.tt-folder-badge{font-size:10px;font-weight:600;padding:1px 5px;border-radius:10px;background:color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);color:var(--dsw-alias-label-secondary);flex:none}' +
122
+ '.tt-add-folder-btn{height:52px;border-radius:8px;border:1px dashed var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);padding:0 12px;font-size:12px;font-weight:500;display:flex;align-items:center;gap:6px;cursor:pointer}' +
123
+ '.tt-icon-picker{position:absolute;bottom:100%;left:0;margin-bottom:6px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px;display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;z-index:80;box-shadow:0 4px 16px color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent)}' +
124
+ '.tt-emoji-item{appearance:none;background:0 0;border:0;padding:4px;font-size:16px;cursor:pointer;border-radius:4px;line-height:1}' +
125
+ '.tt-modal-backdrop{position:fixed;inset:0;background:color-mix(in srgb, var(--dsw-alias-bg-base) 65%, transparent);display:flex;align-items:center;justify-content:center;z-index:100;backdrop-filter:blur(3px)}' +
126
+ '.tt-modal-box{width:560px;max-width:92vw;max-height:88vh;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;display:flex;flex-direction:column;box-shadow:0 12px 36px color-mix(in srgb, var(--dsw-alias-bg-base) 70%, transparent);overflow:hidden}' +
127
+ '.tt-modal-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}' +
128
+ '.tt-modal-body{padding:18px;overflow-y:auto;display:flex;flex-direction:column;gap:14px;flex:1}' +
129
+ '.tt-modal-foot{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}' +
130
+ '.tt-modal-input{height:36px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:0 12px;color:var(--dsw-alias-label-primary);font-size:13px;outline:none}' +
131
+ '.tt-modal-textarea{border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:8px 12px;color:var(--dsw-alias-label-primary);font-size:13px;outline:none;resize:vertical;min-height:90px;font-family:inherit}' +
132
+ '.tt-meta-row{display:flex;align-items:center;gap:16px;font-size:12px;color:var(--dsw-alias-label-secondary)}' +
133
+ '.tt-meta-item{display:flex;align-items:center;gap:6px}' +
134
+ '.tt-meta-badge{padding:2px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-weight:500;color:var(--dsw-alias-label-primary)}' +
135
+ '.tt-start-chat-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:500;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-bg-base);display:inline-flex;align-items:center;gap:6px}' +
136
+ '.tt-btn-secondary{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 14px;font-size:13px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}' +
137
+ '.tt-btn-danger{appearance:none;font:inherit;cursor:pointer;border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent);border-radius:8px;padding:6px 14px;font-size:13px;font-weight:500;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent);color:var(--dsw-alias-state-error-primary)}' +
138
+ '.tt-error-banner{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent);color:var(--dsw-alias-state-error-primary);padding:8px 12px;border-radius:6px;font-size:12px}'
139
+
140
+ function createToggle() {
141
+ let open = false
142
+ const listeners = new Set()
143
+ return {
144
+ isOpen: () => open,
145
+ set: (value) => {
146
+ open = Boolean(value)
147
+ listeners.forEach((fn) => {
148
+ try { fn(open) } catch (_) { /* bestEffort */ }
149
+ })
150
+ },
151
+ toggle: () => {
152
+ open = !open
153
+ listeners.forEach((fn) => {
154
+ try { fn(open) } catch (_) { /* bestEffort */ }
155
+ })
156
+ },
157
+ subscribe: (fn) => {
158
+ listeners.add(fn)
159
+ return () => listeners.delete(fn)
160
+ },
161
+ }
162
+ }
163
+
164
+ async function apiRequest(path, options = {}) {
165
+ const res = await fetch('/dsh-task-tracker/api' + path, {
166
+ headers: { 'Content-Type': 'application/json' },
167
+ ...options,
168
+ })
169
+ if (!res.ok) {
170
+ throw new Error('API error: ' + res.status)
171
+ }
172
+ return res.json()
173
+ }
174
+
175
+ function formatDateTime(iso) {
176
+ if (!iso) return ''
177
+ try {
178
+ const d = new Date(iso)
179
+ return d.toLocaleString('ru-RU', {
180
+ year: 'numeric',
181
+ month: 'short',
182
+ day: 'numeric',
183
+ hour: '2-digit',
184
+ minute: '2-digit',
185
+ })
186
+ } catch (_) {
187
+ return String(iso)
188
+ }
189
+ }
190
+
191
+ function startChatWithAgent(ctx, toggle, task) {
192
+ try {
193
+ const nodes = document.querySelectorAll('button[class*="newSession"]')
194
+ let target = null
195
+ for (const n of nodes) {
196
+ if (String(n.className).indexOf('newSessionLabel') === -1) {
197
+ target = n
198
+ break
199
+ }
200
+ }
201
+ if (target && typeof target.click === 'function') {
202
+ target.click()
203
+ } else if (ctx && ctx.get && typeof ctx.get === 'function') {
204
+ const sessions = ctx.get('sessions')
205
+ if (sessions && typeof sessions.create === 'function') {
206
+ sessions.create()
207
+ }
208
+ }
209
+ } catch (err) {
210
+ console.warn('[dsh-task-tracker] failed to trigger new session button:', err)
211
+ }
212
+
213
+ if (toggle && typeof toggle.set === 'function') {
214
+ toggle.set(false)
215
+ }
216
+
217
+ const pMap = { 0: 'Normal', 1: 'Low', 2: 'Medium', 3: 'High' }
218
+ const priorityLabel = pMap[task.priority] || 'Normal'
219
+ let prompt = 'Task: ' + (task.title || '')
220
+ if (task.folder) prompt += '\nFolder: ' + task.folder
221
+ prompt += '\nPriority: ' + priorityLabel
222
+ if (task.description && task.description.trim()) {
223
+ prompt += '\n\nDescription:\n' + task.description.trim()
224
+ }
225
+
226
+ setTimeout(() => {
227
+ try {
228
+ const sel =
229
+ '.conversation-input textarea, .cb-composer textarea, .dsh-composer textarea, [data-pane="conversation"] textarea, textarea'
230
+ const ta = document.querySelector(sel)
231
+ if (ta) {
232
+ ta.value = prompt
233
+ ta.dispatchEvent(new Event('input', { bubbles: true }))
234
+ ta.dispatchEvent(new Event('change', { bubbles: true }))
235
+ ta.focus()
236
+ }
237
+ } catch (e) {
238
+ console.warn('[dsh-task-tracker] failed to fill composer:', e)
239
+ }
240
+ }, 300)
241
+ }
242
+
243
+ function TaskDetailModal(props) {
244
+ const ctx = props.ctx
245
+ const task = props.task
246
+ const folders = props.folders || []
247
+ const onClose = props.onClose
248
+ const onSave = props.onSave
249
+ const onDelete = props.onDelete
250
+ const onStartChat = props.onStartChat
251
+
252
+ const t = (() => {
253
+ try {
254
+ return ctx?.locale?.bind ? ctx.locale.bind(NS) : (k) => k
255
+ } catch (_) {
256
+ return (k) => k
257
+ }
258
+ })()
259
+
260
+ const [title, setTitle] = React.useState(task.title || '')
261
+ const [description, setDescription] = React.useState(task.description || '')
262
+ const [folder, setFolder] = React.useState(task.folder || 'Inbox')
263
+ const [priority, setPriority] = React.useState(task.priority || 0)
264
+ const [completed, setCompleted] = React.useState(Boolean(task.completed || task.status === 'completed'))
265
+ const [saving, setSaving] = React.useState(false)
266
+
267
+ const handleSave = async () => {
268
+ if (!title.trim()) return
269
+ setSaving(true)
270
+ try {
271
+ await onSave({
272
+ id: task.id,
273
+ title: title.trim(),
274
+ description: description.trim(),
275
+ folder,
276
+ priority: Number(priority),
277
+ completed,
278
+ })
279
+ onClose()
280
+ } catch (err) {
281
+ console.error(err)
282
+ } finally {
283
+ setSaving(false)
284
+ }
285
+ }
286
+
287
+ return h(
288
+ 'div',
289
+ { className: 'tt-modal-backdrop', onClick: onClose },
290
+ h(
291
+ 'div',
292
+ { className: 'tt-modal-box', onClick: (e) => e.stopPropagation() },
293
+ h(
294
+ 'div',
295
+ { className: 'tt-modal-head' },
296
+ h('span', { style: { fontWeight: 600, fontSize: 15 } }, t('tasks.modal.title', 'Task')),
297
+ h('button', {
298
+ type: 'button',
299
+ className: 'tt-close-btn',
300
+ onClick: onClose,
301
+ }, '✕'),
302
+ ),
303
+ h(
304
+ 'div',
305
+ { className: 'tt-modal-body' },
306
+ h(
307
+ 'div',
308
+ { className: 'tt-field', style: { marginTop: 0 } },
309
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.form.title', 'Title')),
310
+ h('input', {
311
+ type: 'text',
312
+ className: 'tt-modal-input',
313
+ value: title,
314
+ placeholder: t('tasks.form.titlePlaceholder', 'Task title'),
315
+ onChange: (e) => setTitle(e.target.value),
316
+ }),
317
+ ),
318
+ h(
319
+ 'div',
320
+ { className: 'tt-field', style: { marginTop: 0 } },
321
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.form.desc', 'Description')),
322
+ h('textarea', {
323
+ className: 'tt-modal-textarea',
324
+ value: description,
325
+ placeholder: t('tasks.form.descPlaceholder', 'Detailed task description…'),
326
+ onChange: (e) => setDescription(e.target.value),
327
+ }),
328
+ ),
329
+ h(
330
+ 'div',
331
+ { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
332
+ h(
333
+ 'div',
334
+ { className: 'tt-field', style: { marginTop: 0 } },
335
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.form.folder', 'Folder')),
336
+ h(
337
+ 'select',
338
+ {
339
+ className: 'tt-modal-input',
340
+ value: folder,
341
+ onChange: (e) => setFolder(e.target.value),
342
+ },
343
+ folders.map((f) =>
344
+ h('option', { key: f.name, value: f.name }, (f.icon || '📁') + ' ' + f.name),
345
+ ),
346
+ ),
347
+ ),
348
+ h(
349
+ 'div',
350
+ { className: 'tt-field', style: { marginTop: 0 } },
351
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.form.priority', 'Priority')),
352
+ h(
353
+ 'select',
354
+ {
355
+ className: 'tt-modal-input',
356
+ value: priority,
357
+ onChange: (e) => setPriority(Number(e.target.value)),
358
+ },
359
+ h('option', { value: 0 }, t('tasks.priority.normal', 'Normal')),
360
+ h('option', { value: 1 }, t('tasks.priority.low', 'Low')),
361
+ h('option', { value: 2 }, t('tasks.priority.medium', 'Medium')),
362
+ h('option', { value: 3 }, t('tasks.priority.high', 'High')),
363
+ ),
364
+ ),
365
+ ),
366
+ h(
367
+ 'div',
368
+ { className: 'tt-meta-row' },
369
+ h(
370
+ 'label',
371
+ { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' } },
372
+ h('input', {
373
+ type: 'checkbox',
374
+ checked: completed,
375
+ onChange: (e) => setCompleted(e.target.checked),
376
+ }),
377
+ h('span', null, t('tasks.status.completed', 'Completed')),
378
+ ),
379
+ h('div', { className: 'tt-meta-item' },
380
+ h('span', null, t('tasks.meta.author', 'Author:')),
381
+ h('span', { className: 'tt-meta-badge' }, task.author || 'User'),
382
+ ),
383
+ task.createdAt
384
+ ? h('div', { className: 'tt-meta-item' },
385
+ h('span', null, t('tasks.meta.created', 'Created:')),
386
+ h('span', { className: 'tt-meta-badge' }, formatDateTime(task.createdAt)),
387
+ )
388
+ : null,
389
+ ),
390
+ ),
391
+ h(
392
+ 'div',
393
+ { className: 'tt-modal-foot' },
394
+ h('div', { style: { display: 'flex', gap: 8 } },
395
+ h('button', {
396
+ type: 'button',
397
+ className: 'tt-btn-danger',
398
+ onClick: () => {
399
+ if (onDelete) onDelete(task.id)
400
+ onClose()
401
+ },
402
+ }, t('tasks.delete', 'Delete')),
403
+ h('button', {
404
+ type: 'button',
405
+ className: 'tt-start-chat-btn',
406
+ onClick: () => {
407
+ if (onStartChat) onStartChat({ ...task, title, description, folder, priority })
408
+ },
409
+ }, '💬 ' + t('tasks.chatWithAgent', 'Chat with agent')),
410
+ ),
411
+ h('div', { style: { display: 'flex', gap: 8 } },
412
+ h('button', {
413
+ type: 'button',
414
+ className: 'tt-btn-secondary',
415
+ onClick: onClose,
416
+ }, t('tasks.close', 'Close')),
417
+ h('button', {
418
+ type: 'button',
419
+ className: 'tt-save',
420
+ disabled: saving || !title.trim(),
421
+ onClick: handleSave,
422
+ }, saving ? t('saving', 'Saving…') : t('save', 'Save')),
423
+ ),
424
+ ),
425
+ ),
426
+ )
427
+ }
428
+
429
+ function TaskCreateModal(props) {
430
+ const ctx = props.ctx
431
+ const defaultFolder = props.defaultFolder || 'Inbox'
432
+ const folders = props.folders || []
433
+ const onClose = props.onClose
434
+ const onCreate = props.onCreate
435
+ const onStartChat = props.onStartChat
436
+
437
+ const t = (() => {
438
+ try {
439
+ return ctx?.locale?.bind ? ctx.locale.bind(NS) : (k, fb) => fb || k
440
+ } catch (_) {
441
+ return (k, fb) => fb || k
442
+ }
443
+ })()
444
+
445
+ const [title, setTitle] = React.useState('')
446
+ const [description, setDescription] = React.useState('')
447
+ const [folder, setFolder] = React.useState(defaultFolder)
448
+ const [priority, setPriority] = React.useState(0)
449
+
450
+ const handleCreateOnly = async () => {
451
+ if (!title.trim()) return
452
+ await onCreate({
453
+ title: title.trim(),
454
+ description: description.trim(),
455
+ folder,
456
+ priority: Number(priority),
457
+ })
458
+ onClose()
459
+ }
460
+
461
+ const handleCreateAndChat = async () => {
462
+ if (!title.trim()) return
463
+ const created = await onCreate({
464
+ title: title.trim(),
465
+ description: description.trim(),
466
+ folder,
467
+ priority: Number(priority),
468
+ })
469
+ onClose()
470
+ if (onStartChat) {
471
+ onStartChat(created || { title, description, folder, priority })
472
+ }
473
+ }
474
+
475
+ return h(
476
+ 'div',
477
+ { className: 'tt-modal-backdrop', onClick: onClose },
478
+ h(
479
+ 'div',
480
+ { className: 'tt-modal-box', onClick: (e) => e.stopPropagation() },
481
+ h(
482
+ 'div',
483
+ { className: 'tt-modal-head' },
484
+ h('span', { style: { fontWeight: 600, fontSize: 15 } }, t('tasks.newTask', 'New Task')),
485
+ h('button', {
486
+ type: 'button',
487
+ className: 'tt-close-btn',
488
+ onClick: onClose,
489
+ }, '\u2715'),
490
+ ),
491
+ h(
492
+ 'div',
493
+ { className: 'tt-modal-body' },
494
+ h(
495
+ 'div',
496
+ { className: 'tt-field', style: { marginTop: 0 } },
497
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.titleLabel', 'Title *')),
498
+ h('input', {
499
+ type: 'text',
500
+ className: 'tt-modal-input',
501
+ value: title,
502
+ placeholder: t('tasks.newPlaceholder', 'What needs to be done?'),
503
+ autoFocus: true,
504
+ onChange: (e) => setTitle(e.target.value),
505
+ onKeyDown: (e) => {
506
+ if (e.key === 'Enter' && !e.shiftKey) {
507
+ e.preventDefault()
508
+ handleCreateOnly()
509
+ }
510
+ },
511
+ }),
512
+ ),
513
+ h(
514
+ 'div',
515
+ { className: 'tt-field', style: { marginTop: 0 } },
516
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.description', 'Description')),
517
+ h('textarea', {
518
+ className: 'tt-modal-textarea',
519
+ value: description,
520
+ placeholder: t('tasks.descPlaceholder', 'Detailed task description\u2026'),
521
+ onChange: (e) => setDescription(e.target.value),
522
+ }),
523
+ ),
524
+ h(
525
+ 'div',
526
+ { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
527
+ h(
528
+ 'div',
529
+ { className: 'tt-field', style: { marginTop: 0 } },
530
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.folder', 'Folder')),
531
+ h(
532
+ 'select',
533
+ {
534
+ className: 'tt-modal-input',
535
+ value: folder,
536
+ onChange: (e) => setFolder(e.target.value),
537
+ },
538
+ folders.map((f) =>
539
+ h('option', { key: f.name, value: f.name }, (f.icon || '\uD83D\uDCC1') + ' ' + f.name),
540
+ ),
541
+ ),
542
+ ),
543
+ h(
544
+ 'div',
545
+ { className: 'tt-field', style: { marginTop: 0 } },
546
+ h('label', { className: 'tt-sub', style: { marginLeft: 0 } }, t('tasks.priority', 'Priority')),
547
+ h(
548
+ 'select',
549
+ {
550
+ className: 'tt-modal-input',
551
+ value: priority,
552
+ onChange: (e) => setPriority(Number(e.target.value)),
553
+ },
554
+ h('option', { value: 0 }, t('tasks.priorityNone', 'Normal')),
555
+ h('option', { value: 1 }, t('tasks.priorityLow', 'Low')),
556
+ h('option', { value: 2 }, t('tasks.priorityMedium', 'Medium')),
557
+ h('option', { value: 3 }, t('tasks.priorityHigh', 'High')),
558
+ ),
559
+ ),
560
+ ),
561
+ ),
562
+ h(
563
+ 'div',
564
+ { className: 'tt-modal-foot' },
565
+ h('button', {
566
+ type: 'button',
567
+ className: 'tt-btn-secondary',
568
+ onClick: onClose,
569
+ }, t('tasks.cancel', 'Cancel')),
570
+ h('div', { style: { display: 'flex', gap: 8 } },
571
+ h('button', {
572
+ type: 'button',
573
+ className: 'tt-btn-secondary',
574
+ onClick: handleCreateOnly,
575
+ }, t('tasks.create', '+ Create')),
576
+ h('button', {
577
+ type: 'button',
578
+ className: 'tt-start-chat-btn',
579
+ onClick: handleCreateAndChat,
580
+ }, t('tasks.startChatWithAgent', '\uD83D\uDCAC Start chat with agent')),
581
+ ),
582
+ ),
583
+ ),
584
+ )
585
+ }
586
+
587
+ function BottomFolderShelf(props) {
588
+ const ctx = props.ctx
589
+ const folders = props.folders || []
590
+ const activeFolder = props.activeFolder
591
+ const onSelectFolder = props.onSelectFolder
592
+ const onCreateFolder = props.onCreateFolder
593
+ const onUpdateFolderIcon = props.onUpdateFolderIcon
594
+ const onMoveTaskToFolder = props.onMoveTaskToFolder
595
+
596
+ const t = (() => {
597
+ try {
598
+ return ctx?.locale?.bind ? ctx.locale.bind(NS) : (k, fb) => fb || k
599
+ } catch (_) {
600
+ return (k, fb) => fb || k
601
+ }
602
+ })()
603
+
604
+ const [pickerFolder, setPickerFolder] = React.useState(null)
605
+ const [dragOverFolder, setDragOverFolder] = React.useState(null)
606
+
607
+ const handleAddFolder = () => {
608
+ const name = window.prompt ? window.prompt(t('tasks.newFolderPrompt', 'New folder name:')) : null
609
+ if (name && name.trim()) {
610
+ onCreateFolder(name.trim(), '\uD83D\uDCC1')
611
+ }
612
+ }
613
+
614
+ return h(
615
+ 'div',
616
+ { className: 'tt-bottom-shelf' },
617
+ h(
618
+ 'div',
619
+ { className: 'tt-shelf-top' },
620
+ h('span', { style: { fontSize: 12, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } },
621
+ t('tasks.folders', 'Folders') + ' (' + folders.length + ')'
622
+ ),
623
+ activeFolder
624
+ ? h('button', {
625
+ type: 'button',
626
+ className: 'tt-btn-secondary',
627
+ style: { padding: '2px 8px', fontSize: 11, height: 22 },
628
+ onClick: () => onSelectFolder(null),
629
+ }, t('tasks.showAllFolders', 'Show all folders'))
630
+ : null,
631
+ ),
632
+ h(
633
+ 'div',
634
+ { className: 'tt-folders-grid' },
635
+ folders.map((folder) => {
636
+ const isActive = activeFolder === folder.name
637
+ const isDragOver = dragOverFolder === folder.name
638
+ const isPickerOpen = pickerFolder === folder.name
639
+
640
+ return h(
641
+ 'div',
642
+ {
643
+ key: folder.name,
644
+ className: 'tt-folder-tile' + (isActive ? ' tt-folder-tile-active' : '') + (isDragOver ? ' tt-folder-tile-dragover' : ''),
645
+ title: folder.name + ' (' + (folder.count || 0) + ' ' + t('tasks.tasksCountSuffix', 'tasks') + ')',
646
+ onClick: () => onSelectFolder(isActive ? null : folder.name),
647
+ onDragOver: (e) => {
648
+ e.preventDefault()
649
+ e.stopPropagation()
650
+ e.dataTransfer.dropEffect = 'move'
651
+ if (dragOverFolder !== folder.name) {
652
+ setDragOverFolder(folder.name)
653
+ }
654
+ },
655
+ onDragLeave: (e) => {
656
+ e.preventDefault()
657
+ e.stopPropagation()
658
+ if (dragOverFolder === folder.name) {
659
+ setDragOverFolder(null)
660
+ }
661
+ },
662
+ onDrop: (e) => {
663
+ e.preventDefault()
664
+ e.stopPropagation()
665
+ setDragOverFolder(null)
666
+ const taskId = e.dataTransfer.getData('text/plain')
667
+ if (taskId && onMoveTaskToFolder) {
668
+ onMoveTaskToFolder(taskId, folder.name)
669
+ }
670
+ },
671
+ },
672
+ h(
673
+ 'button',
674
+ {
675
+ type: 'button',
676
+ className: 'tt-folder-icon-btn',
677
+ title: t('tasks.changeIcon', 'Change icon'),
678
+ onClick: (e) => {
679
+ e.stopPropagation()
680
+ setPickerFolder(isPickerOpen ? null : folder.name)
681
+ },
682
+ },
683
+ folder.icon || '\uD83D\uDCC1',
684
+ ),
685
+ isPickerOpen
686
+ ? h(
687
+ 'div',
688
+ {
689
+ className: 'tt-icon-picker',
690
+ onClick: (e) => e.stopPropagation(),
691
+ },
692
+ POPULAR_EMOJIS.map((emoji) =>
693
+ h(
694
+ 'button',
695
+ {
696
+ key: emoji,
697
+ type: 'button',
698
+ className: 'tt-emoji-item',
699
+ onClick: (e) => {
700
+ e.stopPropagation()
701
+ setPickerFolder(null)
702
+ if (onUpdateFolderIcon) {
703
+ onUpdateFolderIcon(folder.name, emoji)
704
+ }
705
+ },
706
+ },
707
+ emoji,
708
+ ),
709
+ ),
710
+ )
711
+ : null,
712
+ h('span', { className: 'tt-folder-title-box' }, folder.name),
713
+ h('span', { className: 'tt-folder-badge' }, String(folder.count || 0)),
714
+ )
715
+ }),
716
+ h(
717
+ 'button',
718
+ {
719
+ type: 'button',
720
+ className: 'tt-add-folder-btn',
721
+ onClick: handleAddFolder,
722
+ title: t('tasks.createFolder', 'Create folder'),
723
+ },
724
+ h('span', { style: { fontSize: 14, fontWeight: 700 } }, '+'),
725
+ t('tasks.folderBtn', 'Folder'),
726
+ ),
727
+ ),
728
+ )
729
+ }
730
+
731
+ function TasksScreen(props) {
732
+ const ctx = props.ctx
733
+ const toggle = props.toggle
734
+
735
+ const t = (() => {
736
+ try {
737
+ return ctx?.locale?.bind ? ctx.locale.bind(NS) : (k, fb) => fb || k
738
+ } catch (_) {
739
+ return (k, fb) => fb || k
740
+ }
741
+ })()
742
+
743
+ const [tasks, setTasks] = React.useState([])
744
+ const [folders, setFolders] = React.useState([])
745
+ const [activeFolder, setActiveFolder] = React.useState(null)
746
+ const [filter, setFilter] = React.useState('all')
747
+ const [searchQuery, setSearchQuery] = React.useState('')
748
+ const [quickAddTitle, setQuickAddTitle] = React.useState('')
749
+ const [selectedTask, setSelectedTask] = React.useState(null)
750
+ const [isCreateOpen, setIsCreateOpen] = React.useState(false)
751
+ const [loading, setLoading] = React.useState(true)
752
+
753
+ const loadData = React.useCallback(async () => {
754
+ try {
755
+ const [taskList, folderData] = await Promise.all([
756
+ apiRequest('/tasks'),
757
+ apiRequest('/tasks/folders?details=1'),
758
+ ])
759
+ if (Array.isArray(taskList)) setTasks(taskList)
760
+ if (folderData && Array.isArray(folderData.details)) {
761
+ setFolders(folderData.details)
762
+ } else if (folderData && Array.isArray(folderData.folders)) {
763
+ setFolders(folderData.folders.map((f) => (typeof f === 'string' ? { name: f, icon: '\uD83D\uDCC1', count: 0 } : f)))
764
+ }
765
+ } catch (err) {
766
+ console.warn('[dsh-task-tracker] failed to load tasks/folders:', err)
767
+ } finally {
768
+ setLoading(false)
769
+ }
770
+ }, [])
771
+
772
+ React.useEffect(() => {
773
+ loadData()
774
+ }, [loadData])
775
+
776
+ React.useEffect(() => {
777
+ if (!toggle || typeof toggle.subscribe !== 'function') return
778
+ const unsub = toggle.subscribe((isOpen) => {
779
+ if (isOpen) {
780
+ loadData()
781
+ }
782
+ })
783
+ return unsub
784
+ }, [toggle, loadData])
785
+
786
+ React.useEffect(() => {
787
+ const onDataChanged = () => {
788
+ if (toggle && typeof toggle.isOpen === 'function' && toggle.isOpen()) {
789
+ loadData()
790
+ }
791
+ }
792
+ document.addEventListener(DATA_CHANGE_EVENT, onDataChanged)
793
+ return () => {
794
+ document.removeEventListener(DATA_CHANGE_EVENT, onDataChanged)
795
+ }
796
+ }, [toggle, loadData])
797
+
798
+ const handleToggleComplete = async (task) => {
799
+ try {
800
+ const isCurrentlyDone = Boolean(task.completed || task.status === 'completed')
801
+ const nextDone = !isCurrentlyDone
802
+ const updated = await apiRequest('/tasks/' + encodeURIComponent(task.id), {
803
+ method: 'PATCH',
804
+ body: {
805
+ completed: nextDone,
806
+ status: nextDone ? 'completed' : 'pending',
807
+ },
808
+ })
809
+ setTasks((prev) => prev.map((tItem) => (tItem.id === task.id ? updated : tItem))); notifyDataChanged()
810
+ } catch (err) {
811
+ console.warn('[dsh-task-tracker] toggle complete error:', err)
812
+ }
813
+ }
814
+
815
+ const handleCreateTask = async (taskData) => {
816
+ try {
817
+ const created = await apiRequest('/tasks', {
818
+ method: 'POST',
819
+ body: taskData,
820
+ })
821
+ setTasks((prev) => [created, ...prev])
822
+ loadData()
823
+ notifyDataChanged()
824
+ return created
825
+ } catch (err) {
826
+ console.warn('[dsh-task-tracker] create task error:', err)
827
+ }
828
+ }
829
+
830
+ const handleQuickAdd = async () => {
831
+ if (!quickAddTitle.trim()) return
832
+ const val = quickAddTitle.trim()
833
+ setQuickAddTitle('')
834
+ await handleCreateTask({
835
+ title: val,
836
+ description: '',
837
+ folder: activeFolder || 'Inbox',
838
+ priority: 0,
839
+ })
840
+ }
841
+
842
+ const handleSaveTask = async (updatedTask) => {
843
+ try {
844
+ const saved = await apiRequest('/tasks/' + encodeURIComponent(updatedTask.id), {
845
+ method: 'PATCH',
846
+ body: updatedTask,
847
+ })
848
+ setTasks((prev) => prev.map((tItem) => (tItem.id === saved.id ? saved : tItem)))
849
+ loadData()
850
+ notifyDataChanged()
851
+ } catch (err) {
852
+ console.warn('[dsh-task-tracker] save task error:', err)
853
+ }
854
+ }
855
+
856
+ const handleDeleteTask = async (id) => {
857
+ try {
858
+ await apiRequest('/tasks/' + encodeURIComponent(id), { method: 'DELETE' })
859
+ setTasks((prev) => prev.filter((tItem) => tItem.id !== id))
860
+ if (selectedTask && selectedTask.id === id) setSelectedTask(null)
861
+ loadData()
862
+ notifyDataChanged()
863
+ } catch (err) {
864
+ console.warn('[dsh-task-tracker] delete task error:', err)
865
+ }
866
+ }
867
+
868
+ const handleCreateFolder = async (name, icon) => {
869
+ try {
870
+ await apiRequest('/tasks/folders', {
871
+ method: 'POST',
872
+ body: { name, icon: icon || '\uD83D\uDCC1' },
873
+ })
874
+ loadData()
875
+ } catch (err) {
876
+ console.warn('[dsh-task-tracker] create folder error:', err)
877
+ }
878
+ }
879
+
880
+ const handleUpdateFolderIcon = async (name, icon) => {
881
+ try {
882
+ await apiRequest('/tasks/folders/' + encodeURIComponent(name), {
883
+ method: 'PATCH',
884
+ body: { icon },
885
+ })
886
+ loadData()
887
+ } catch (err) {
888
+ console.warn('[dsh-task-tracker] update folder icon error:', err)
889
+ }
890
+ }
891
+
892
+ const handleMoveTaskToFolder = async (taskId, folderName) => {
893
+ try {
894
+ const updated = await apiRequest('/tasks/' + encodeURIComponent(taskId), {
895
+ method: 'PATCH',
896
+ body: { folder: folderName },
897
+ })
898
+ setTasks((prev) => prev.map((tItem) => (tItem.id === taskId ? updated : tItem)))
899
+ loadData()
900
+ } catch (err) {
901
+ console.warn('[dsh-task-tracker] move task error:', err)
902
+ }
903
+ }
904
+
905
+ const filteredTasks = tasks.filter((tItem) => {
906
+ const taskFolder = tItem.folder || 'Inbox'
907
+ if (activeFolder && taskFolder.toLowerCase() !== activeFolder.toLowerCase()) return false
908
+ const isDone = Boolean(tItem.completed || tItem.status === 'completed')
909
+ if (filter === 'active' && isDone) return false
910
+ if (filter === 'completed' && !isDone) return false
911
+ if (searchQuery.trim()) {
912
+ const q = searchQuery.toLowerCase()
913
+ const inTitle = tItem.title && tItem.title.toLowerCase().includes(q)
914
+ const inDesc = tItem.description && tItem.description.toLowerCase().includes(q)
915
+ if (!inTitle && !inDesc) return false
916
+ }
917
+ return true
918
+ })
919
+
920
+ return h(
921
+ 'div',
922
+ { className: 'tt-screen-container' },
923
+ h('style', null, CSS),
924
+ h(
925
+ 'div',
926
+ { className: 'tt-screen-head' },
927
+ h(
928
+ 'button',
929
+ {
930
+ type: 'button',
931
+ className: 'tt-back-btn',
932
+ title: t('tasks.backToChat', 'To the chat'),
933
+ onClick: () => toggle && toggle.set(false),
934
+ },
935
+ h('span', { dangerouslySetInnerHTML: { __html: BACK_ICON } }),
936
+ ' ' + t('tasks.backToChat', 'To the chat'),
937
+ ),
938
+ h(
939
+ 'div',
940
+ { className: 'tt-screen-title' },
941
+ h('span', null, t('tasks.title', 'Tasks')),
942
+ activeFolder
943
+ ? h('span', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)', fontWeight: 400 } }, ' / ' + activeFolder)
944
+ : null,
945
+ ),
946
+ h(
947
+ 'div',
948
+ { className: 'tt-filter-group' },
949
+ h(
950
+ 'button',
951
+ {
952
+ type: 'button',
953
+ className: 'tt-filter-btn' + (filter === 'all' ? ' tt-filter-btn-active' : ''),
954
+ onClick: () => setFilter('all'),
955
+ },
956
+ t('tasks.all', 'All'),
957
+ ),
958
+ h(
959
+ 'button',
960
+ {
961
+ type: 'button',
962
+ className: 'tt-filter-btn' + (filter === 'active' ? ' tt-filter-btn-active' : ''),
963
+ onClick: () => setFilter('active'),
964
+ },
965
+ t('tasks.active', 'Active'),
966
+ ),
967
+ h(
968
+ 'button',
969
+ {
970
+ type: 'button',
971
+ className: 'tt-filter-btn' + (filter === 'completed' ? ' tt-filter-btn-active' : ''),
972
+ onClick: () => setFilter('completed'),
973
+ },
974
+ t('tasks.completed', 'Done'),
975
+ ),
976
+ ),
977
+ h('div', { style: { flex: 1 } }),
978
+ h('input', {
979
+ type: 'text',
980
+ className: 'tt-search-input',
981
+ placeholder: t('tasks.search', 'Search tasks\u2026'),
982
+ value: searchQuery,
983
+ onChange: (e) => setSearchQuery(e.target.value),
984
+ }),
985
+ h(
986
+ 'button',
987
+ {
988
+ type: 'button',
989
+ className: 'tt-btn-secondary tt-refresh-btn',
990
+ style: { height: 32, padding: '0 10px', display: 'flex', alignItems: 'center', gap: 4 },
991
+ title: t('tasks.refresh', 'Refresh'),
992
+ onClick: () => loadData(),
993
+ disabled: loading,
994
+ },
995
+ h('span', { style: { display: 'inline-block' } }, '↻'),
996
+ t('tasks.refresh', 'Refresh'),
997
+ ),
998
+ h(
999
+ 'button',
1000
+ {
1001
+ type: 'button',
1002
+ className: 'tt-add-main-btn',
1003
+ onClick: () => setIsCreateOpen(true),
1004
+ },
1005
+ t('tasks.addTask', '+ Add Task'),
1006
+ ),
1007
+ h(
1008
+ 'button',
1009
+ {
1010
+ type: 'button',
1011
+ className: 'tt-close-btn',
1012
+ title: t('tasks.close', 'Close'),
1013
+ onClick: () => toggle && toggle.set(false),
1014
+ },
1015
+ '\u2715',
1016
+ ),
1017
+ ),
1018
+ h(
1019
+ 'div',
1020
+ { style: { padding: '8px 16px 0' } },
1021
+ h(
1022
+ 'div',
1023
+ { className: 'tt-quick-add-row' },
1024
+ h('input', {
1025
+ type: 'text',
1026
+ className: 'tt-quick-add-input',
1027
+ placeholder: t('tasks.quickAddPlaceholder', 'What needs to be done? Press Enter to add'),
1028
+ value: quickAddTitle,
1029
+ onChange: (e) => setQuickAddTitle(e.target.value),
1030
+ onKeyDown: (e) => {
1031
+ if (e.key === 'Enter' && !e.shiftKey) {
1032
+ e.preventDefault()
1033
+ handleQuickAdd()
1034
+ }
1035
+ },
1036
+ }),
1037
+ h(
1038
+ 'button',
1039
+ {
1040
+ type: 'button',
1041
+ className: 'tt-quick-add-btn',
1042
+ onClick: handleQuickAdd,
1043
+ },
1044
+ t('tasks.add', '+ Add'),
1045
+ ),
1046
+ ),
1047
+ ),
1048
+ h(
1049
+ 'div',
1050
+ { className: 'tt-tasks-main' },
1051
+ loading
1052
+ ? h('div', { style: { textAlign: 'center', padding: '40px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, t('tasks.loadingTasks', 'Loading tasks\u2026'))
1053
+ : filteredTasks.length === 0
1054
+ ? h(
1055
+ 'div',
1056
+ { className: 'tt-empty-box' },
1057
+ h('div', { style: { fontSize: 32, marginBottom: 8 } }, '\uD83D\uDCCB'),
1058
+ h(
1059
+ 'div',
1060
+ { style: { fontSize: 14, fontWeight: 500, color: 'var(--dsw-alias-label-secondary)' } },
1061
+ activeFolder ? t('tasks.emptyInFolder', 'No tasks in folder "%s"').replace('%s', activeFolder) : t('tasks.empty', 'No tasks found'),
1062
+ ),
1063
+ h(
1064
+ 'button',
1065
+ {
1066
+ type: 'button',
1067
+ className: 'tt-add-main-btn',
1068
+ style: { marginTop: 8 },
1069
+ onClick: () => setIsCreateOpen(true),
1070
+ },
1071
+ t('tasks.addTask', '+ Add Task'),
1072
+ ),
1073
+ )
1074
+ : filteredTasks.map((tItem) =>
1075
+ h(
1076
+ 'div',
1077
+ {
1078
+ key: tItem.id,
1079
+ className: 'tt-task-row',
1080
+ draggable: true,
1081
+ onDragStart: (e) => {
1082
+ e.dataTransfer.setData('text/plain', tItem.id)
1083
+ e.dataTransfer.effectAllowed = 'move'
1084
+ },
1085
+ onClick: () => setSelectedTask(tItem),
1086
+ },
1087
+ h(
1088
+ 'button',
1089
+ {
1090
+ type: 'button',
1091
+ className: 'tt-task-check' + (tItem.completed ? ' tt-task-check-checked' : ''),
1092
+ title: tItem.completed ? t('tasks.markIncomplete', 'Mark as incomplete') : t('tasks.markComplete', 'Mark as completed'),
1093
+ onClick: (e) => {
1094
+ e.stopPropagation()
1095
+ handleToggleComplete(tItem)
1096
+ },
1097
+ },
1098
+ tItem.completed ? '\u2713' : null,
1099
+ ),
1100
+ h(
1101
+ 'span',
1102
+ {
1103
+ className: 'tt-task-title' + (tItem.completed ? ' tt-task-title-done' : ''),
1104
+ title: tItem.title,
1105
+ },
1106
+ tItem.title,
1107
+ ),
1108
+ tItem.priority > 0
1109
+ ? h(
1110
+ 'span',
1111
+ {
1112
+ className: 'tt-task-priority-badge tt-priority-' + tItem.priority,
1113
+ },
1114
+ tItem.priority === 3 ? t('tasks.priorityHigh', 'High') : tItem.priority === 2 ? t('tasks.priorityMedium', 'Medium') : t('tasks.priorityLow', 'Low'),
1115
+ )
1116
+ : null,
1117
+ h(
1118
+ 'button',
1119
+ {
1120
+ type: 'button',
1121
+ className: 'tt-task-action-btn',
1122
+ title: t('tasks.delete', 'Delete'),
1123
+ onClick: (e) => {
1124
+ e.stopPropagation()
1125
+ handleDeleteTask(tItem.id)
1126
+ },
1127
+ },
1128
+ '\u2715',
1129
+ ),
1130
+ ),
1131
+ ),
1132
+ ),
1133
+ h(BottomFolderShelf, {
1134
+ ctx,
1135
+ folders,
1136
+ activeFolder,
1137
+ onSelectFolder: (name) => setActiveFolder(name),
1138
+ onCreateFolder: handleCreateFolder,
1139
+ onUpdateFolderIcon: handleUpdateFolderIcon,
1140
+ onMoveTaskToFolder: handleMoveTaskToFolder,
1141
+ }),
1142
+ selectedTask
1143
+ ? h(TaskDetailModal, {
1144
+ ctx,
1145
+ task: selectedTask,
1146
+ folders,
1147
+ onClose: () => setSelectedTask(null),
1148
+ onSave: handleSaveTask,
1149
+ onDelete: handleDeleteTask,
1150
+ onStartChat: (task) => startChatWithAgent(ctx, toggle, task),
1151
+ })
1152
+ : null,
1153
+ isCreateOpen
1154
+ ? h(TaskCreateModal, {
1155
+ ctx,
1156
+ defaultFolder: activeFolder || 'Inbox',
1157
+ folders,
1158
+ onClose: () => setIsCreateOpen(false),
1159
+ onCreate: handleCreateTask,
1160
+ onStartChat: (task) => startChatWithAgent(ctx, toggle, task),
1161
+ })
1162
+ : null,
1163
+ )
1164
+ }
1165
+
1166
+ function TaskChip(props) {
1167
+ const toggle = props.toggle
1168
+ const [count, setCount] = React.useState(0)
1169
+
1170
+ React.useEffect(() => {
1171
+ let alive = true
1172
+ const check = async () => {
1173
+ try {
1174
+ const stats = await apiRequest('/tasks/stats')
1175
+ if (alive && stats && typeof stats.pending === 'number') {
1176
+ setCount(stats.pending)
1177
+ }
1178
+ } catch (err) {
1179
+ console.warn('[dsh-task-tracker] failed to fetch task stats:', err)
1180
+ }
1181
+ }
1182
+ check()
1183
+ const timer = setInterval(check, 10000)
1184
+ const onDataChanged = () => {
1185
+ if (alive) check()
1186
+ }
1187
+ document.addEventListener(DATA_CHANGE_EVENT, onDataChanged)
1188
+ return () => {
1189
+ alive = false
1190
+ clearInterval(timer)
1191
+ document.removeEventListener(DATA_CHANGE_EVENT, onDataChanged)
1192
+ }
1193
+ }, [])
1194
+
1195
+ return h(
1196
+ 'button',
1197
+ {
1198
+ type: 'button',
1199
+ className: 'tt-chip-btn',
1200
+ title: 'Task Tracker',
1201
+ onClick: () => toggle && toggle.toggle(),
1202
+ },
1203
+ h('span', { dangerouslySetInnerHTML: { __html: ICON_TASK } }),
1204
+ count > 0 ? h('span', { className: 'tt-chip-badge' }, String(count)) : null,
1205
+ )
1206
+ }
1207
+
1208
+ function mountTasksScreen(ctx, toggle) {
1209
+ if (typeof document === 'undefined') return () => {}
1210
+
1211
+ let root = null
1212
+ let container = null
1213
+ let observer = null
1214
+
1215
+ const ensure = () => {
1216
+ if (container !== null) return
1217
+ const column = document.querySelector(COLUMN_SELECTOR)
1218
+ if (column === null) return
1219
+
1220
+ container = document.createElement('div')
1221
+ container.setAttribute('data-dsh-plugin', 'dsh-task-tracker')
1222
+ container.setAttribute('data-dsh-task-tracker-view', '')
1223
+ container.style.display = toggle.isOpen() ? 'flex' : 'none'
1224
+ column.appendChild(container)
1225
+
1226
+ if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
1227
+ root = ReactDOM.createRoot(container)
1228
+ root.render(h(TasksScreen, { ctx, toggle }))
1229
+ }
1230
+
1231
+ if (observer) {
1232
+ observer.disconnect()
1233
+ observer = null
1234
+ }
1235
+ }
1236
+
1237
+ const applyActive = () => {
1238
+ if (toggle.isOpen()) {
1239
+ if (container) container.style.display = 'flex'
1240
+ document.documentElement.setAttribute('data-dsh-task-tracker-open', '')
1241
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_ID }))
1242
+ } else {
1243
+ if (container) container.style.display = 'none'
1244
+ document.documentElement.removeAttribute('data-dsh-task-tracker-open')
1245
+ }
1246
+ }
1247
+
1248
+ const onOtherActivate = (e) => {
1249
+ if (e.detail !== PANEL_ID && toggle.isOpen()) {
1250
+ toggle.set(false)
1251
+ }
1252
+ }
1253
+
1254
+ const onKeyDown = (e) => {
1255
+ if (e.key === 'Escape' && toggle.isOpen()) {
1256
+ toggle.set(false)
1257
+ }
1258
+ }
1259
+
1260
+ observer = new MutationObserver(ensure)
1261
+ observer.observe(document.body, { childList: true, subtree: true })
1262
+ document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
1263
+ document.addEventListener('keydown', onKeyDown)
1264
+ const unsub = toggle.subscribe(applyActive)
1265
+ ensure()
1266
+
1267
+ return () => {
1268
+ if (observer) {
1269
+ observer.disconnect()
1270
+ observer = null
1271
+ }
1272
+ document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
1273
+ document.removeEventListener('keydown', onKeyDown)
1274
+ unsub()
1275
+ document.documentElement.removeAttribute('data-dsh-task-tracker-open')
1276
+ if (root && typeof root.unmount === 'function') root.unmount()
1277
+ if (container && container.remove) container.remove()
1278
+ }
1279
+ }
1280
+
1281
+ function mountSidebarEntry(toggle, label) {
1282
+ if (typeof document === 'undefined') return () => {}
1283
+
1284
+ let entry = null
1285
+ let observer = null
1286
+
1287
+ const tryPlace = () => {
1288
+ if (document.querySelector('[data-dsh-part="task-tracker-sidebar"]')) {
1289
+ if (observer) {
1290
+ observer.disconnect()
1291
+ observer = null
1292
+ }
1293
+ return
1294
+ }
1295
+ const nodes = document.querySelectorAll('button[class*="newSession"]')
1296
+ let target = null
1297
+ for (const n of nodes) {
1298
+ if (String(n.className).indexOf('newSessionLabel') === -1) {
1299
+ target = n
1300
+ break
1301
+ }
1302
+ }
1303
+ if (!target) return
1304
+
1305
+ entry = target.cloneNode(true)
1306
+ entry.className = String(target.className) + ' tt-sidebar-entry'
1307
+ entry.setAttribute('data-dsh-plugin', 'dsh-task-tracker')
1308
+ entry.setAttribute('data-dsh-part', 'task-tracker-sidebar')
1309
+ entry.setAttribute('aria-label', label)
1310
+ entry.removeAttribute('id')
1311
+ entry.innerHTML = ''
1312
+
1313
+ const icon = document.createElement('span')
1314
+ icon.innerHTML = ICON_TASK
1315
+ entry.appendChild(icon)
1316
+
1317
+ const text = document.createElement('span')
1318
+ text.style.marginLeft = '8px'
1319
+ text.textContent = label
1320
+ entry.appendChild(text)
1321
+
1322
+ entry.addEventListener('click', (e) => {
1323
+ e.preventDefault()
1324
+ e.stopPropagation()
1325
+ toggle.toggle()
1326
+ })
1327
+
1328
+ target.insertAdjacentElement('afterend', entry)
1329
+ if (observer) {
1330
+ observer.disconnect()
1331
+ observer = null
1332
+ }
1333
+ }
1334
+
1335
+ observer = new MutationObserver(tryPlace)
1336
+ observer.observe(document.body, { childList: true, subtree: true })
1337
+ tryPlace()
1338
+
1339
+ return () => {
1340
+ if (observer) {
1341
+ observer.disconnect()
1342
+ observer = null
1343
+ }
1344
+ if (entry && entry.remove) entry.remove()
1345
+ }
1346
+ }
1347
+
1348
+ function PluginCard(props) {
1349
+ const ctx = props.ctx
1350
+ const [expanded, setExpanded] = React.useState(false)
1351
+ const [snap, setSnap] = React.useState({
1352
+ status: 'loading',
1353
+ value: null,
1354
+ writable: true,
1355
+ })
1356
+ const [draft, setDraft] = React.useState({
1357
+ filePath: '',
1358
+ })
1359
+ const [saving, setSaving] = React.useState(false)
1360
+ const [msg, setMsg] = React.useState('')
1361
+
1362
+ const [updateState, setUpdateState] = React.useState({
1363
+ checking: true,
1364
+ updating: false,
1365
+ currentVersion: '',
1366
+ latestVersion: '',
1367
+ updateAvailable: false,
1368
+ error: '',
1369
+ notice: '',
1370
+ })
1371
+
1372
+ const initializedRef = React.useRef(false)
1373
+
1374
+ React.useEffect(() => {
1375
+ let off = null
1376
+ try {
1377
+ const scope = ctx.configForms && ctx.configForms.get
1378
+ ? ctx.configForms.get(NS)
1379
+ : null
1380
+ if (!scope) {
1381
+ setSnap({ status: 'unavailable', value: null, writable: true })
1382
+ return
1383
+ }
1384
+ const update = () => {
1385
+ const s = scope.getSnapshot
1386
+ ? scope.getSnapshot()
1387
+ : { status: 'unavailable', value: null }
1388
+ setSnap(s)
1389
+ if (s.status === 'ready' && s.value && !initializedRef.current) {
1390
+ initializedRef.current = true
1391
+ const v = s.value
1392
+ // Never echo secret pass back into the form.
1393
+ setDraft({
1394
+ filePath: v.filePath ?? '',
1395
+ })
1396
+ }
1397
+ }
1398
+ update()
1399
+ if (scope.subscribe) off = scope.subscribe(update)
1400
+ } catch (_) {
1401
+ setSnap({ status: 'unavailable', value: null, writable: true })
1402
+ }
1403
+ return () => {
1404
+ try {
1405
+ off && off()
1406
+ } catch (_) {
1407
+ /* bestEffort: cleanup */
1408
+ }
1409
+ }
1410
+ }, [ctx])
1411
+
1412
+ const t = (() => {
1413
+ try {
1414
+ return ctx.locale && ctx.locale.bind ? ctx.locale.bind(NS) : (k, fb) => fb || k
1415
+ } catch (_) {
1416
+ return (k, fb) => fb || k
1417
+ }
1418
+ })()
1419
+
1420
+ React.useEffect(() => {
1421
+ let alive = true
1422
+ ;(async () => {
1423
+ try {
1424
+ const res = await fetch('/dsh-task-tracker/api/updater')
1425
+ if (!res.ok) throw new Error('HTTP ' + res.status)
1426
+ const data = await res.json()
1427
+ if (!alive) return
1428
+ setUpdateState((s) => ({
1429
+ ...s,
1430
+ checking: false,
1431
+ currentVersion: data.currentVersion || '',
1432
+ latestVersion: data.latestVersion || '',
1433
+ updateAvailable: Boolean(data.updateAvailable),
1434
+ }))
1435
+ } catch (_) {
1436
+ if (!alive) return
1437
+ setUpdateState((s) => ({
1438
+ ...s,
1439
+ checking: false,
1440
+ error: t('updater.checkFailed', 'Failed to check updates'),
1441
+ }))
1442
+ }
1443
+ })()
1444
+ return () => { alive = false }
1445
+ }, [])
1446
+
1447
+ const onTriggerUpdate = async () => {
1448
+ if (updateState.updating) return
1449
+ setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
1450
+ try {
1451
+ const res = await fetch('/dsh-task-tracker/api/updater', {
1452
+ method: 'POST',
1453
+ headers: { 'x-dsh-plugin-update': '1' },
1454
+ })
1455
+ const data = await res.json()
1456
+ if (!res.ok || data.error) throw new Error(data.error || ('HTTP ' + res.status))
1457
+ const newVer = data.updatedVersion || updateState.latestVersion
1458
+ setUpdateState((s) => ({
1459
+ ...s,
1460
+ updating: false,
1461
+ updateAvailable: false,
1462
+ currentVersion: newVer,
1463
+ notice: (t('updater.updated', 'Successfully updated to v{v}. Restart harness to apply.')).replace('{v}', newVer),
1464
+ }))
1465
+ } catch (err) {
1466
+ setUpdateState((s) => ({
1467
+ ...s,
1468
+ updating: false,
1469
+ error: (t('updater.updateFailed', 'Update failed') + ': ' + (err?.message || err)),
1470
+ }))
1471
+ }
1472
+ }
1473
+
1474
+ const onSave = async () => {
1475
+ setSaving(true)
1476
+ setMsg('')
1477
+ try {
1478
+ if (!ctx.configForms || !ctx.configForms.get) {
1479
+ setMsg(t('unavailable', 'Settings scope unavailable'))
1480
+ setSaving(false)
1481
+ return
1482
+ }
1483
+ const scope = ctx.configForms.get(NS)
1484
+ const errs = []
1485
+ try {
1486
+ await scope.set('filePath', draft.filePath ?? '')
1487
+ } catch (e) {
1488
+ errs.push('filePath: ' + (e && e.message ? e.message : String(e)))
1489
+ }
1490
+ setMsg(errs.length ? errs.join('; ') : t('saved', 'Saved'))
1491
+ } catch (e) {
1492
+ setMsg(String(e && e.message ? e.message : e))
1493
+ }
1494
+ setSaving(false)
1495
+ }
1496
+
1497
+ // Row seat (plugins.row.config): the host page draws title/icon/crumb and the
1498
+ // padding, so the summary is a one-liner and the page drops our card chrome.
1499
+ if (props && props.view === 'summary') {
1500
+ return h('span', { className: 'tt-sub' }, t('subtitle', 'Local JSON todo store (~/.dsh/task-tracker/tasks.json)'))
1501
+ }
1502
+ const page = !!(props && props.view === 'page')
1503
+
1504
+ return h(
1505
+ page ? 'div' : 'li',
1506
+ { className: page ? 'tt-page' : 'tt-card' },
1507
+ h('style', null, CSS),
1508
+ h(
1509
+ 'button',
1510
+ {
1511
+ type: 'button',
1512
+ className: 'tt-head',
1513
+ style: page ? { display: 'none' } : undefined,
1514
+ onClick: () => setExpanded(!expanded),
1515
+ 'aria-expanded': page ? true : expanded,
1516
+ },
1517
+ h('span', { className: 'tt-title' }, t('title', 'Task Tracker')),
1518
+ h('span', { className: 'tt-sub' }, t('subtitle', 'Local JSON todo store (~/.dsh/task-tracker/tasks.json)')),
1519
+ h(Chevron, {
1520
+ className: 'tt-chev' + (expanded ? ' tt-chev-open' : ''),
1521
+ }),
1522
+ ),
1523
+ page || expanded
1524
+ ? h(
1525
+ 'div',
1526
+ { className: 'tt-body' },
1527
+ h(
1528
+ 'div',
1529
+ { className: 'tt-updater-block' },
1530
+ h(
1531
+ 'div',
1532
+ { className: 'tt-updater-info' },
1533
+ h('span', null, t('updater.version', 'Version') + ': ' + (updateState.currentVersion || (updateState.checking ? '\u2026' : '\u2014'))),
1534
+ updateState.checking ? h('span', null, t('updater.checking', 'Checking for updates\u2026')) : null,
1535
+ !updateState.checking && updateState.updateAvailable
1536
+ ? h('span', { className: 'tt-badge-warn' }, (t('updater.available', 'Update available: v{v}')).replace('{v}', updateState.latestVersion))
1537
+ : null,
1538
+ !updateState.checking && !updateState.updateAvailable && !updateState.error
1539
+ ? h('span', { className: 'tt-badge-ok' }, '\u2713 ' + t('updater.upToDate', 'Up to date'))
1540
+ : null,
1541
+ updateState.error ? h('span', { className: 'tt-badge-err' }, updateState.error) : null,
1542
+ ),
1543
+ updateState.updateAvailable
1544
+ ? h(
1545
+ 'button',
1546
+ {
1547
+ type: 'button',
1548
+ className: 'tt-save',
1549
+ onClick: onTriggerUpdate,
1550
+ disabled: updateState.updating,
1551
+ },
1552
+ updateState.updating ? t('updater.updating', 'Updating\u2026') : t('updater.updateNow', 'Update now'),
1553
+ )
1554
+ : null,
1555
+ ),
1556
+ updateState.notice
1557
+ ? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-button-primary-fill)' } }, updateState.notice)
1558
+ : null,
1559
+ snap.status === 'loading'
1560
+ ? h(
1561
+ 'div',
1562
+ { className: 'tt-field', style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } },
1563
+ t('loading', 'Loading\u2026'),
1564
+ )
1565
+ : null,
1566
+ snap.status === 'unavailable'
1567
+ ? h(
1568
+ 'div',
1569
+ { className: 'tt-field', style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } },
1570
+ t('unavailable', 'Settings scope unavailable'),
1571
+ )
1572
+ : null,
1573
+ snap.status === 'ready'
1574
+ ? h(
1575
+ 'div',
1576
+ null,
1577
+ h(
1578
+ 'div',
1579
+ { className: 'tt-field' },
1580
+ h('label', { className: 'tt-sub' }, t('field.filePath', 'Custom tasks JSON storage path (optional)')),
1581
+ h('input', {
1582
+ className: 'tt-input',
1583
+ type: 'text',
1584
+ value: draft.filePath,
1585
+ placeholder: t('field.filePathPlaceholder', '~/.dsh/task-tracker/tasks.json'),
1586
+ onChange: (e) => setDraft({ ...draft, filePath: e.target.value }),
1587
+ disabled: snap.writable === false,
1588
+ }),
1589
+ ),
1590
+ h(
1591
+ 'div',
1592
+ { className: 'tt-foot' },
1593
+ msg
1594
+ ? h(
1595
+ 'span',
1596
+ {
1597
+ style: {
1598
+ fontSize: 12,
1599
+ color: 'var(--dsw-alias-label-secondary)',
1600
+ marginRight: 'auto',
1601
+ },
1602
+ },
1603
+ msg,
1604
+ )
1605
+ : null,
1606
+ h(
1607
+ 'button',
1608
+ {
1609
+ type: 'button',
1610
+ className: 'tt-save',
1611
+ onClick: onSave,
1612
+ disabled: saving || snap.writable === false,
1613
+ },
1614
+ saving ? t('saving', 'Saving\u2026') : t('save', 'Save'),
1615
+ ),
1616
+ ),
1617
+ )
1618
+ : null,
1619
+ )
1620
+ : null,
1621
+ )
1622
+ }
1623
+
1624
+ module.exports.inject = ['slots', 'locale', 'configForms']
1625
+ module.exports.apply = function apply(ctx) {
1626
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
1627
+ const registerLocale = () =>
1628
+ ctx.locale.register(NS, {
1629
+ en: {
1630
+ title: 'Task Tracker',
1631
+ subtitle: 'Local JSON todo store (~/.dsh/task-tracker/tasks.json)',
1632
+ 'board.back': 'To the chat',
1633
+ 'board.backHint': 'Close task tracker and return to chat',
1634
+ 'field.filePath': 'Custom tasks JSON storage path (optional)',
1635
+ 'field.filePathPlaceholder': '~/.dsh/task-tracker/tasks.json',
1636
+ loading: 'Loading\u2026',
1637
+ unavailable: 'Settings scope unavailable',
1638
+ save: 'Save',
1639
+ saving: 'Saving\u2026',
1640
+ saved: 'Saved',
1641
+ 'field.base': 'API base URL',
1642
+ 'field.user': 'Basic auth user',
1643
+ 'field.pass': 'Basic auth password',
1644
+ 'field.passFile': 'Password file (optional)',
1645
+ passPlaceholder: 'Leave blank to keep current',
1646
+ passHint: 'Secret field: the browser never reads the stored password back.',
1647
+ 'tasks.title': 'Tasks',
1648
+ 'tasks.all': 'All',
1649
+ 'tasks.active': 'Active',
1650
+ 'tasks.completed': 'Done',
1651
+ 'tasks.refresh': 'Refresh',
1652
+ 'tasks.search': 'Search tasks\u2026',
1653
+ 'tasks.newPlaceholder': 'What needs to be done?',
1654
+ 'tasks.add': 'Add Task',
1655
+ 'tasks.addTask': '+ Add Task',
1656
+ 'tasks.empty': 'No tasks found',
1657
+ 'tasks.emptyInFolder': 'No tasks in folder "%s"',
1658
+ 'tasks.delete': 'Delete task',
1659
+ 'tasks.close': 'Close',
1660
+ 'tasks.backToChat': 'To the chat',
1661
+ 'tasks.quickAddPlaceholder': 'What needs to be done? Press Enter to add',
1662
+ 'tasks.loadingTasks': 'Loading tasks\u2026',
1663
+ 'tasks.markIncomplete': 'Mark as incomplete',
1664
+ 'tasks.markComplete': 'Mark as completed',
1665
+ 'tasks.priorityNone': 'Normal',
1666
+ 'tasks.priorityLow': 'Low',
1667
+ 'tasks.priorityMedium': 'Medium',
1668
+ 'tasks.priorityHigh': 'High',
1669
+ 'tasks.newTask': 'New Task',
1670
+ 'tasks.titleLabel': 'Title *',
1671
+ 'tasks.description': 'Description',
1672
+ 'tasks.descPlaceholder': 'Detailed task description\u2026',
1673
+ 'tasks.folder': 'Folder',
1674
+ 'tasks.priority': 'Priority',
1675
+ 'tasks.cancel': 'Cancel',
1676
+ 'tasks.create': '+ Create',
1677
+ 'tasks.startChatWithAgent': '\uD83D\uDCAC Start chat with agent',
1678
+ 'tasks.newFolderPrompt': 'New folder name:',
1679
+ 'tasks.folders': 'Folders',
1680
+ 'tasks.showAllFolders': 'Show all folders',
1681
+ 'tasks.tasksCountSuffix': 'tasks',
1682
+ 'tasks.changeIcon': 'Change icon',
1683
+ 'tasks.createFolder': 'Create folder',
1684
+ 'tasks.folderBtn': 'Folder',
1685
+ 'tasks.taskDetails': 'Task Details',
1686
+ 'tasks.status': 'Status',
1687
+ 'tasks.statusCompleted': 'Completed',
1688
+ 'tasks.statusActive': 'Active',
1689
+ 'tasks.promptTemplate': 'Prompt template',
1690
+ 'tasks.openAgentChat': '\uD83D\uDCAC Chat with agent about this task',
1691
+ 'tasks.confirmDelete': 'Delete this task?',
1692
+ 'updater.version': 'Version',
1693
+ 'updater.checking': 'Checking for updates\u2026',
1694
+ 'updater.available': 'Update available: v{v}',
1695
+ 'updater.upToDate': 'Up to date',
1696
+ 'updater.updateNow': 'Update now',
1697
+ 'updater.updating': 'Updating\u2026',
1698
+ 'updater.updated': 'Successfully updated to v{v}. Restart harness to apply.',
1699
+ 'updater.checkFailed': 'Failed to check updates',
1700
+ 'updater.updateFailed': 'Update failed',
1701
+ },
1702
+ zh: {
1703
+ title: '\u4efb\u52a1\u8ffd\u8e2a\u5668',
1704
+ subtitle: '\u672c\u5730 JSON \u5f85\u529e\u5b58\u50a8 (~/.dsh/task-tracker/tasks.json)',
1705
+ 'board.back': '\u8fd4\u56de\u804a\u5929',
1706
+ 'board.backHint': '\u5173\u95ed\u4efb\u52a1\u8ffd\u8e2a\u5668\u5e76\u8fd4\u56de\u804a\u5929',
1707
+ 'field.filePath': '\u81ea\u5b9a\u4e49\u4efb\u52a1 JSON \u5b58\u50a8\u8def\u5f84\uff08\u53ef\u9009\uff09',
1708
+ 'field.filePathPlaceholder': '~/.dsh/task-tracker/tasks.json',
1709
+ loading: '\u52a0\u8f7d\u4e2d\u2026',
1710
+ unavailable: '\u8bbe\u7f6e\u4f5c\u7528\u57df\u4e0d\u53ef\u7528',
1711
+ save: '\u4fdd\u5b58',
1712
+ saving: '\u4fdd\u5b58\u4e2d\u2026',
1713
+ saved: '\u5df2\u4fdd\u5b58',
1714
+ 'field.base': 'API \u57fa\u7840 URL',
1715
+ 'field.user': 'Basic \u8ba4\u8bc1\u7528\u6237\u540d',
1716
+ 'field.pass': 'Basic \u8ba4\u8bc1\u5bc6\u7801',
1717
+ 'field.passFile': '\u5bc6\u7801\u6587\u4ef6\uff08\u53ef\u9009\uff09',
1718
+ passPlaceholder: '\u7559\u7a7a\u4ee5\u4fdd\u6301\u5f53\u524d\u5bc6\u7801',
1719
+ passHint: '\u673a\u5bc6\u5b57\u6bb5\uff1a\u6d4f\u89c8\u5668\u4e0d\u4f1a\u56de\u663e\u5df2\u5b58\u50a8\u7684\u5bc6\u7801\u3002',
1720
+ 'tasks.title': '\u4efb\u52a1',
1721
+ 'tasks.all': '\u5168\u90e8',
1722
+ 'tasks.active': '\u8fdb\u884c\u4e2d',
1723
+ 'tasks.completed': '\u5df2\u5b8c\u6210',
1724
+ 'tasks.refresh': '\u5237\u65b0',
1725
+ 'tasks.search': '\u641c\u7d22\u4efb\u52a1\u2026',
1726
+ 'tasks.newPlaceholder': '\u9700\u8981\u505a\u4ec0\u4e48\uff1f',
1727
+ 'tasks.add': '\u6dfb\u52a0\u4efb\u52a1',
1728
+ 'tasks.addTask': '+ \u6dfb\u52a0\u4efb\u52a1',
1729
+ 'tasks.empty': '\u6682\u65e0\u4efb\u52a1',
1730
+ 'tasks.emptyInFolder': '\u6587\u4ef6\u5939\u300c%s\u300d\u4e2d\u6ca1\u6709\u4efb\u52a1',
1731
+ 'tasks.delete': '\u5220\u9664\u4efb\u52a1',
1732
+ 'tasks.close': '\u5173\u95ed',
1733
+ 'tasks.backToChat': '\u8fd4\u56de\u804a\u5929',
1734
+ 'tasks.quickAddPlaceholder': '\u9700\u8981\u505a\u4ec0\u4e48\uff1f\u6309\u56de\u8f66\u6dfb\u52a0',
1735
+ 'tasks.loadingTasks': '\u6b63\u5728\u52a0\u8f7d\u4efb\u52a1\u2026',
1736
+ 'tasks.markIncomplete': '\u6807\u8bb0\u4e3a\u672a\u5b8c\u6210',
1737
+ 'tasks.markComplete': '\u6807\u8bb0\u4e3a\u5df2\u5b8c\u6210',
1738
+ 'tasks.priorityNone': '\u666e\u901a',
1739
+ 'tasks.priorityLow': '\u4f4e',
1740
+ 'tasks.priorityMedium': '\u4e2d',
1741
+ 'tasks.priorityHigh': '\u9ad8',
1742
+ 'tasks.newTask': '\u65b0\u5efa\u4efb\u52a1',
1743
+ 'tasks.titleLabel': '\u6807\u9898 *',
1744
+ 'tasks.description': '\u63cf\u8ff0',
1745
+ 'tasks.descPlaceholder': '\u4efb\u52a1\u8be6\u7ec6\u63cf\u8ff0\u2026',
1746
+ 'tasks.folder': '\u6587\u4ef6\u5939',
1747
+ 'tasks.priority': '\u4f18\u5148\u7ea7',
1748
+ 'tasks.cancel': '\u53d6\u6d88',
1749
+ 'tasks.create': '+ \u521b\u5efa',
1750
+ 'tasks.startChatWithAgent': '\uD83D\uDCAC \u4e0e\u667a\u80fd\u4f53\u5f00\u59cb\u5bf9\u8bdd',
1751
+ 'tasks.newFolderPrompt': '\u65b0\u6587\u4ef6\u5939\u540d\u79f0\uff1a',
1752
+ 'tasks.folders': '\u6587\u4ef6\u5939',
1753
+ 'tasks.showAllFolders': '\u663e\u793a\u6240\u6709\u6587\u4ef6\u5939',
1754
+ 'tasks.tasksCountSuffix': '\u4e2a\u4efb\u52a1',
1755
+ 'tasks.changeIcon': '\u66f4\u6362\u56fe\u6807',
1756
+ 'tasks.createFolder': '\u65b0\u5efa\u6587\u4ef6\u5939',
1757
+ 'tasks.folderBtn': '\u6587\u4ef6\u5939',
1758
+ 'tasks.taskDetails': '\u4efb\u52a1\u8be6\u60c5',
1759
+ 'tasks.status': '\u72b6\u6001',
1760
+ 'tasks.statusCompleted': '\u5df2\u5b8c\u6210',
1761
+ 'tasks.statusActive': '\u8fdb\u884c\u4e2d',
1762
+ 'tasks.promptTemplate': '\u63d0\u793a\u8bcd\u6a21\u677f',
1763
+ 'tasks.openAgentChat': '\uD83D\uDCAC \u5c31\u6b64\u4efb\u52a1\u4e0e\u667a\u80fd\u4f53\u5bf9\u8bdd',
1764
+ 'tasks.confirmDelete': '\u5220\u9664\u6b64\u4efb\u52a1\uff1f',
1765
+ 'updater.version': '\u7248\u672c',
1766
+ 'updater.checking': '\u6b63\u5728\u68c0\u67e5\u66f4\u65b0\u2026',
1767
+ 'updater.available': '\u53d1\u73b0\u65b0\u7248\u672c\uff1av{v}',
1768
+ 'updater.upToDate': '\u5df2\u662f\u6700\u65b0\u7248\u672c',
1769
+ 'updater.updateNow': '\u7acb\u5373\u66f4\u65b0',
1770
+ 'updater.updating': '\u6b63\u5728\u66f4\u65b0\u2026',
1771
+ 'updater.updated': '\u5df2\u6210\u529f\u66f4\u65b0\u81f3 v{v}\u3002\u91cd\u542f harness \u751f\u6548\u3002',
1772
+ 'updater.checkFailed': '\u68c0\u67e5\u66f4\u65b0\u5931\u8d25',
1773
+ 'updater.updateFailed': '\u66f4\u65b0\u5931\u8d25',
1774
+ },
1775
+ })
1776
+
1777
+ if (typeof ctx.effect === 'function') {
1778
+ ctx.effect(registerLocale, 'dsh-task-tracker: locale dictionaries')
1779
+ } else {
1780
+ registerLocale()
1781
+ }
1782
+ }
1783
+
1784
+ if (ctx.slots && typeof ctx.slots.inject === 'function') {
1785
+ const injectCard = (slotName, key) => () => {
1786
+ try {
1787
+ return ctx.slots.inject(slotName, () => {
1788
+ try {
1789
+ return ctx.slots.register(
1790
+ {
1791
+ name: slotName,
1792
+ key,
1793
+ locale: NS,
1794
+ inject: () => ({ ctx }),
1795
+ },
1796
+ PluginCard,
1797
+ )
1798
+ } catch (regErr) {
1799
+ console.warn('[dsh-task-tracker] failed to register PluginCard in ' + slotName + ':', regErr && regErr.message)
1800
+ throw regErr
1801
+ }
1802
+ })
1803
+ } catch (err) {
1804
+ console.warn('[dsh-task-tracker] failed to inject ' + slotName + ':', err && err.message)
1805
+ throw err
1806
+ }
1807
+ }
1808
+
1809
+ // Plugin-list seat (plugins.item): the seat the current core (0.1.6-alpha.2)
1810
+ // renders as the plugin's own page with its configuration. It takes an id and a
1811
+ // static label instead of a key, so it is mounted on its own; the label must not
1812
+ // read the locale, because it is resolved while the page renders.
1813
+ const seatItem = () => {
1814
+ try {
1815
+ return ctx.slots.inject('plugins.item', () => {
1816
+ try {
1817
+ return ctx.slots.register(
1818
+ {
1819
+ name: 'plugins.item',
1820
+ id: ROW_ID,
1821
+ order: 60,
1822
+ label: () => 'Task Tracker',
1823
+ locale: NS,
1824
+ inject: () => ({ ctx }),
1825
+ },
1826
+ PluginCard,
1827
+ )
1828
+ } catch (regErr) {
1829
+ console.warn('[dsh-task-tracker] failed to register PluginCard in plugins.item:', regErr && regErr.message)
1830
+ throw regErr
1831
+ }
1832
+ })
1833
+ } catch (err) {
1834
+ console.warn('[dsh-task-tracker] failed to inject plugins.item:', err && err.message)
1835
+ throw err
1836
+ }
1837
+ }
1838
+
1839
+ // Plugin-list seat first, then the row seat and the legacy seat as fallbacks.
1840
+ const seatRow = injectCard('plugins.row.config', ROW_CONFIG_KEY)
1841
+ const seatLegacy = injectCard('settings.plugin.item', NS)
1842
+
1843
+ if (typeof ctx.effect === 'function') {
1844
+ ctx.effect(seatItem, 'dsh-task-tracker: settings list seat')
1845
+ ctx.effect(seatRow, 'dsh-task-tracker: settings row seat')
1846
+ ctx.effect(seatLegacy, 'dsh-task-tracker: settings card')
1847
+ } else {
1848
+ seatItem()
1849
+ seatRow()
1850
+ seatLegacy()
1851
+ }
1852
+ }
1853
+
1854
+ const toggle = createToggle()
1855
+
1856
+ if (ctx.slots && typeof ctx.slots.inject === 'function') {
1857
+ const injectChip = () =>
1858
+ ctx.slots.inject('conversation.session.header.utilities', () =>
1859
+ ctx.slots.register(
1860
+ {
1861
+ name: 'conversation.session.header.utilities',
1862
+ id: '@goodandready/dsh-task-tracker.chip',
1863
+ order: 28,
1864
+ locale: NS,
1865
+ inject: () => ({ ctx, toggle }),
1866
+ },
1867
+ TaskChip,
1868
+ ),
1869
+ )
1870
+
1871
+ if (typeof ctx.effect === 'function') {
1872
+ ctx.effect(injectChip, 'dsh-task-tracker: chip')
1873
+ } else {
1874
+ injectChip()
1875
+ }
1876
+ }
1877
+
1878
+ const mountOverlay = () => {
1879
+ const off = [
1880
+ mountSidebarEntry(toggle, 'Tasks'),
1881
+ mountTasksScreen(ctx, toggle),
1882
+ ]
1883
+ return () => {
1884
+ for (const fn of off) {
1885
+ try { fn() } catch (_) {
1886
+ /* bestEffort: cleanup */
1887
+ }
1888
+ }
1889
+ }
1890
+ }
1891
+
1892
+ if (typeof ctx.effect === 'function') {
1893
+ ctx.effect(mountOverlay, 'dsh-task-tracker: overlay')
1894
+ } else {
1895
+ mountOverlay()
1896
+ }
1897
+ }
1898
+
1899
+ return module.exports
1900
+ },
1901
+ })