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

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