@veewo/dsh-better-tasks 0.0.0-stage → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -2
- package/cordis.patch.yml +32 -0
- package/docs/dsh-0.2.2-rc.3-support.md +68 -0
- package/docs/install-rc3.md +94 -0
- package/docs/install.md +156 -0
- package/docs/rc2-slash-submit-catch.md +58 -0
- package/docs/release.md +67 -0
- package/docs/rollback.md +80 -0
- package/docs/sidebar-layout.md +72 -0
- package/docs/task-order-persistence.md +25 -0
- package/docs/task-session-activity.md +13 -0
- package/lib/client.js +3176 -0
- package/package.json +79 -4
- package/src/client.js +2358 -0
- package/src/final-message.mjs +75 -0
- package/src/index.js +220 -0
- package/src/native-pin-host.mjs +261 -0
- package/src/pin-client.mjs +193 -0
- package/src/pin-model.mjs +118 -0
- package/src/project-appearance.mjs +147 -0
- package/src/project-icon-catalog.mjs +4 -0
- package/src/skill-picker.mjs +26 -0
- package/src/task-actions.mjs +80 -0
- package/src/task-execution.mjs +37 -0
- package/src/task-input-surface.mjs +152 -0
- package/src/task-model.mjs +235 -0
- package/src/task-preferences.mjs +139 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,3176 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@veewo/dsh-better-tasks",
|
|
3
|
+
factory: requireModule => {
|
|
4
|
+
const EMPTY_QUEUE = Object.freeze({ schemaVersion: 1, revision: 0, sessionIds: Object.freeze([]), updatedAt: '' })
|
|
5
|
+
|
|
6
|
+
function validQueue(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && value.schemaVersion === 1 &&
|
|
8
|
+
Number.isSafeInteger(value.revision) && value.revision >= 0 && Array.isArray(value.sessionIds) &&
|
|
9
|
+
value.sessionIds.every(id => typeof id === 'string' && id !== '') &&
|
|
10
|
+
new Set(value.sessionIds).size === value.sessionIds.length && typeof value.updatedAt === 'string'
|
|
11
|
+
}
|
|
12
|
+
function ownedQueue(value) {
|
|
13
|
+
if (!validQueue(value)) throw new Error('better-tasks: Host returned an invalid task order')
|
|
14
|
+
return Object.freeze({ ...value, sessionIds: Object.freeze([...value.sessionIds]) })
|
|
15
|
+
}
|
|
16
|
+
function createClientSource(initial) {
|
|
17
|
+
let snapshot = initial
|
|
18
|
+
const listeners = new Set()
|
|
19
|
+
return { getSnapshot: () => snapshot, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) },
|
|
20
|
+
set(next) { snapshot = next; for (const fn of [...listeners]) fn() } }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Rank is metadata only: stale rank can never resurrect a native-unpinned Session. */
|
|
24
|
+
function projectNativeTaskQueue(rank, native) {
|
|
25
|
+
const archived = new Set(native?.archivedSessionIds ?? [])
|
|
26
|
+
const pins = [...new Set(native?.pinnedSessionIds ?? [])].filter(id => !archived.has(id))
|
|
27
|
+
const members = new Set(pins), ranked = new Set(rank.sessionIds)
|
|
28
|
+
return Object.freeze({ ...rank, sessionIds: Object.freeze([
|
|
29
|
+
...pins.filter(id => !ranked.has(id)), ...rank.sessionIds.filter(id => members.has(id)),
|
|
30
|
+
]) })
|
|
31
|
+
}
|
|
32
|
+
async function pinRequest(method, body, signal) {
|
|
33
|
+
const response = await fetch('/api/better-tasks/pins', { method, signal,
|
|
34
|
+
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
|
|
35
|
+
body: body === undefined ? undefined : JSON.stringify(body) })
|
|
36
|
+
let payload
|
|
37
|
+
try { payload = await response.json() } catch { payload = {} }
|
|
38
|
+
return { response, payload }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Observe official membership; own only rank refresh, pending feedback and errors. */
|
|
42
|
+
function createTaskPinsClient({ request = pinRequest, schedule = setTimeout, cancel = clearTimeout } = {}) {
|
|
43
|
+
const source = createClientSource(Object.freeze({ phase: 'loading', queue: EMPTY_QUEUE,
|
|
44
|
+
pendingSessionIds: Object.freeze([]), error: null, writable: false, migration: null }))
|
|
45
|
+
const pending = new Map(), pinFlights = new Map()
|
|
46
|
+
const lifetime = new AbortController()
|
|
47
|
+
let rank = EMPTY_QUEUE, native, binding, disconnect, tail = Promise.resolve()
|
|
48
|
+
let started = false, disposed = false, generation = 0, loaded = false, writable = false, hasStableRank = false
|
|
49
|
+
let orderError = null, actionError = null, warnings = [], migration = null
|
|
50
|
+
let refreshPromise = null, refreshAgain = false, timer, polls = 0
|
|
51
|
+
const nativeReady = () => native?.phase === 'ready'
|
|
52
|
+
const publish = () => {
|
|
53
|
+
if (disposed) return
|
|
54
|
+
const nativeError = native?.error?.message ?? (native?.state === 'error' ? 'Native Pin service is unavailable' : null)
|
|
55
|
+
source.set(Object.freeze({ phase: nativeReady() ? 'ready' : 'loading',
|
|
56
|
+
queue: projectNativeTaskQueue(rank, native), pendingSessionIds: Object.freeze([...pending.keys()]),
|
|
57
|
+
error: actionError ?? orderError ?? nativeError ?? (warnings.length ? warnings.join('; ') : null),
|
|
58
|
+
writable: nativeReady() && loaded && writable, migration: migration?.status ?? null }))
|
|
59
|
+
}
|
|
60
|
+
const countPending = (id, delta) => {
|
|
61
|
+
const count = (pending.get(id) ?? 0) + delta
|
|
62
|
+
if (count > 0) pending.set(id, count); else pending.delete(id)
|
|
63
|
+
publish()
|
|
64
|
+
}
|
|
65
|
+
const enqueue = work => {
|
|
66
|
+
const next = tail.then(() => { if (disposed) throw new Error('Task pins are disposed'); return work() })
|
|
67
|
+
tail = next.catch(() => {})
|
|
68
|
+
return next
|
|
69
|
+
}
|
|
70
|
+
const stopTimer = () => { if (timer !== undefined) { cancel(timer); timer = undefined } }
|
|
71
|
+
const armMigrationRefresh = () => {
|
|
72
|
+
stopTimer()
|
|
73
|
+
const restoring = ['pending', 'initializing', 'running'].includes(migration?.status)
|
|
74
|
+
const saving = loaded && !writable && ['done', 'partial'].includes(migration?.status)
|
|
75
|
+
if (!started || disposed || !(restoring || saving)) { polls = 0; return }
|
|
76
|
+
if (polls >= 60) return // Native follow/focus still refresh; never poll indefinitely after a stuck migration.
|
|
77
|
+
polls++
|
|
78
|
+
timer = schedule(() => { timer = undefined; void refresh().catch(() => {}) }, 1000)
|
|
79
|
+
}
|
|
80
|
+
const adopt = payload => {
|
|
81
|
+
const incoming = ownedQueue(payload.queue)
|
|
82
|
+
const initializing = payload.migration?.status === 'initializing'
|
|
83
|
+
if (!initializing || !hasStableRank) rank = incoming
|
|
84
|
+
if (!initializing) hasStableRank = true
|
|
85
|
+
loaded = true; writable = payload.writable !== false
|
|
86
|
+
warnings = Array.isArray(payload.warnings) ? payload.warnings.filter(x => typeof x === 'string') : []
|
|
87
|
+
migration = payload.migration ?? null
|
|
88
|
+
orderError = null
|
|
89
|
+
publish(); armMigrationRefresh()
|
|
90
|
+
return source.getSnapshot().queue
|
|
91
|
+
}
|
|
92
|
+
const readOrder = async () => {
|
|
93
|
+
const token = generation
|
|
94
|
+
try {
|
|
95
|
+
const { response, payload } = await request('GET', undefined, lifetime.signal)
|
|
96
|
+
if (disposed || token !== generation) return source.getSnapshot().queue
|
|
97
|
+
if (!response.ok) throw new Error(payload.message ?? `Task order read failed (${response.status})`)
|
|
98
|
+
return adopt(payload)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!disposed && token === generation) { orderError = String(error?.message ?? error); writable = false; publish() }
|
|
101
|
+
throw error
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const refresh = () => {
|
|
105
|
+
if (disposed) return Promise.reject(new Error('Task pins are disposed'))
|
|
106
|
+
if (binding === undefined) return Promise.resolve(source.getSnapshot().queue)
|
|
107
|
+
if (refreshPromise !== null) { refreshAgain = true; return refreshPromise }
|
|
108
|
+
refreshPromise = enqueue(readOrder).finally(() => {
|
|
109
|
+
refreshPromise = null
|
|
110
|
+
if (refreshAgain && !disposed && started && binding !== undefined) { refreshAgain = false; void refresh().catch(() => {}) }
|
|
111
|
+
})
|
|
112
|
+
return refreshPromise
|
|
113
|
+
}
|
|
114
|
+
const connectNative = (next) => {
|
|
115
|
+
disconnect?.()
|
|
116
|
+
if (disposed) return () => {}
|
|
117
|
+
if (next?.source?.getSnapshot === undefined || typeof next?.source?.subscribe !== 'function' ||
|
|
118
|
+
typeof next.pinSession !== 'function' || typeof next.unpinSession !== 'function') {
|
|
119
|
+
orderError = 'Native Pin service is unavailable'; publish(); return () => {}
|
|
120
|
+
}
|
|
121
|
+
binding = next; generation++
|
|
122
|
+
let signature, closed = false
|
|
123
|
+
const observe = () => {
|
|
124
|
+
if (closed || disposed) return
|
|
125
|
+
const snapshot = next.source.getSnapshot()
|
|
126
|
+
native = { ...snapshot, pinnedSessionIds: [...(snapshot.pinnedSessionIds ?? [])], archivedSessionIds: [...(snapshot.archivedSessionIds ?? [])] }
|
|
127
|
+
const value = JSON.stringify([native.pinnedSessionIds, native.archivedSessionIds, native.phase, native.state])
|
|
128
|
+
const changed = signature !== value
|
|
129
|
+
signature = value; publish()
|
|
130
|
+
if (changed && nativeReady() && started) void refresh().catch(() => {})
|
|
131
|
+
}
|
|
132
|
+
const off = next.source.subscribe(observe)
|
|
133
|
+
disconnect = () => {
|
|
134
|
+
if (closed) return
|
|
135
|
+
closed = true; off(); stopTimer(); generation++
|
|
136
|
+
if (binding === next) { binding = undefined; native = undefined; loaded = false; writable = false; publish() }
|
|
137
|
+
}
|
|
138
|
+
observe()
|
|
139
|
+
return disconnect
|
|
140
|
+
}
|
|
141
|
+
const setPinned = (sessionId, pinned) => {
|
|
142
|
+
if (disposed || binding === undefined) return Promise.reject(new Error('Native Pin service is unavailable'))
|
|
143
|
+
const prior = pinFlights.get(sessionId)
|
|
144
|
+
if (prior?.pinned === pinned) return prior.promise
|
|
145
|
+
if (!pending.has(sessionId) && nativeReady() && source.getSnapshot().queue.sessionIds.includes(sessionId) === pinned) {
|
|
146
|
+
return Promise.resolve(source.getSnapshot().queue)
|
|
147
|
+
}
|
|
148
|
+
const target = binding
|
|
149
|
+
actionError = null; countPending(sessionId, 1)
|
|
150
|
+
const promise = Promise.resolve().then(() => (pinned ? target.pinSession : target.unpinSession)(sessionId)).then(() => {
|
|
151
|
+
// Native model/follow owns the result. Never force the requested boolean or
|
|
152
|
+
// retry the Pin just because the separate rank service is unavailable.
|
|
153
|
+
publish(); if (started) void refresh().catch(() => {})
|
|
154
|
+
return source.getSnapshot().queue
|
|
155
|
+
}).catch(error => { actionError = String(error?.message ?? error); publish(); throw error }).finally(() => {
|
|
156
|
+
if (pinFlights.get(sessionId)?.promise === promise) pinFlights.delete(sessionId)
|
|
157
|
+
countPending(sessionId, -1)
|
|
158
|
+
})
|
|
159
|
+
pinFlights.set(sessionId, { pinned, promise })
|
|
160
|
+
return promise
|
|
161
|
+
}
|
|
162
|
+
const moveBefore = (sessionId, beforeSessionId) => {
|
|
163
|
+
actionError = null; countPending(sessionId, 1)
|
|
164
|
+
return enqueue(async () => {
|
|
165
|
+
if (!loaded || !writable) await readOrder()
|
|
166
|
+
if (!writable) throw new Error('Task order is not ready or has not been saved; official Pin remains usable')
|
|
167
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
168
|
+
const queue = source.getSnapshot().queue
|
|
169
|
+
if (!queue.sessionIds.includes(sessionId) || beforeSessionId != null && !queue.sessionIds.includes(beforeSessionId)) throw new Error('Task is no longer pinned')
|
|
170
|
+
const { response, payload } = await request('POST', {
|
|
171
|
+
action: 'move', sessionId, beforeSessionId: beforeSessionId ?? null, expectedRevision: rank.revision,
|
|
172
|
+
}, lifetime.signal)
|
|
173
|
+
if (disposed) throw new Error('Task pins are disposed')
|
|
174
|
+
if (response.ok) return adopt(payload)
|
|
175
|
+
const hasCurrentOrder = validQueue(payload.queue)
|
|
176
|
+
if (hasCurrentOrder) adopt(payload)
|
|
177
|
+
if (response.status === 409 && hasCurrentOrder) { if (!writable) break; continue }
|
|
178
|
+
throw new Error(payload.message ?? `Task order write failed (${response.status})`)
|
|
179
|
+
}
|
|
180
|
+
throw new Error('Task order changed again; retry the move')
|
|
181
|
+
}).catch(error => { actionError = String(error?.message ?? error); publish(); throw error }).finally(() => countPending(sessionId, -1))
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
source, connectNative, refresh, setPinned, pinCreated: sessionId => setPinned(sessionId, true), moveBefore,
|
|
185
|
+
start() {
|
|
186
|
+
started = true
|
|
187
|
+
const onFocus = () => { polls = 0; void refresh().catch(() => {}) }
|
|
188
|
+
if (typeof window !== 'undefined') window.addEventListener('focus', onFocus)
|
|
189
|
+
if (binding !== undefined && nativeReady()) void refresh().catch(() => {})
|
|
190
|
+
return () => {
|
|
191
|
+
disposed = true; started = false; stopTimer(); disconnect?.(); lifetime.abort()
|
|
192
|
+
if (typeof window !== 'undefined') window.removeEventListener('focus', onFocus)
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const TASK_SETTINGS_NAMESPACE = 'better-tasks-ui'
|
|
199
|
+
// 0.2 dsh-settings.describe emits ns=entry.options.id and write resolves that
|
|
200
|
+
// same options.id (lib/index.js:443,502 in 0.2.0-rc.2). Inspect's qualified
|
|
201
|
+
// loader id (include:better-tasks-ui) is NOT the settings namespace.
|
|
202
|
+
const TASK_SETTINGS_ENTRY_ID = TASK_SETTINGS_NAMESPACE
|
|
203
|
+
const TASK_DETAIL_FONT_MIN = 11
|
|
204
|
+
const TASK_DETAIL_FONT_MAX = 16
|
|
205
|
+
const TASK_COLUMN_LAYOUTS = Object.freeze(['auto', 'single', 'double'])
|
|
206
|
+
const TASK_TITLE_EMPHASES = Object.freeze(['project', 'task'])
|
|
207
|
+
/** Sidebar body the browser shows: the native session list or the task swimlane. */
|
|
208
|
+
const TASK_SIDEBAR_VIEWS = Object.freeze(['native', 'tasks'])
|
|
209
|
+
const DEFAULT_TASK_PREFERENCES = Object.freeze({
|
|
210
|
+
detailFontSize: 13,
|
|
211
|
+
defaultTodoExpanded: false,
|
|
212
|
+
defaultGoalExpanded: false,
|
|
213
|
+
defaultFinalExpanded: true,
|
|
214
|
+
defaultInputExpanded: false,
|
|
215
|
+
columnLayout: 'auto',
|
|
216
|
+
titleEmphasis: 'task',
|
|
217
|
+
sidebarView: 'native',
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const FIELDS = new Set(Object.keys(DEFAULT_TASK_PREFERENCES))
|
|
221
|
+
|
|
222
|
+
function decodeTaskPreferences(value) {
|
|
223
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
224
|
+
const detailFontSize = value.detailFontSize
|
|
225
|
+
const columnLayout = value.columnLayout
|
|
226
|
+
const titleEmphasis = value.titleEmphasis === undefined ? DEFAULT_TASK_PREFERENCES.titleEmphasis : value.titleEmphasis
|
|
227
|
+
if (!TASK_TITLE_EMPHASES.includes(titleEmphasis)) return undefined
|
|
228
|
+
// Absent reads as the default, like titleEmphasis: rows written before this
|
|
229
|
+
// preference existed must keep decoding.
|
|
230
|
+
const sidebarView = value.sidebarView === undefined ? DEFAULT_TASK_PREFERENCES.sidebarView : value.sidebarView
|
|
231
|
+
if (!TASK_SIDEBAR_VIEWS.includes(sidebarView)) return undefined
|
|
232
|
+
if (!Number.isInteger(detailFontSize) || detailFontSize < TASK_DETAIL_FONT_MIN || detailFontSize > TASK_DETAIL_FONT_MAX) return undefined
|
|
233
|
+
if (!TASK_COLUMN_LAYOUTS.includes(columnLayout)) return undefined
|
|
234
|
+
for (const field of ['defaultTodoExpanded', 'defaultGoalExpanded', 'defaultFinalExpanded', 'defaultInputExpanded']) {
|
|
235
|
+
if (typeof value[field] !== 'boolean') return undefined
|
|
236
|
+
}
|
|
237
|
+
return Object.freeze({
|
|
238
|
+
detailFontSize,
|
|
239
|
+
defaultTodoExpanded: value.defaultTodoExpanded,
|
|
240
|
+
defaultGoalExpanded: value.defaultGoalExpanded,
|
|
241
|
+
defaultFinalExpanded: value.defaultFinalExpanded,
|
|
242
|
+
defaultInputExpanded: value.defaultInputExpanded,
|
|
243
|
+
columnLayout,
|
|
244
|
+
titleEmphasis,
|
|
245
|
+
sidebarView,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fallbackSnapshot() {
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
status: 'unavailable',
|
|
252
|
+
value: DEFAULT_TASK_PREFERENCES,
|
|
253
|
+
revision: undefined,
|
|
254
|
+
writable: false,
|
|
255
|
+
mode: 'memory',
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function createTaskPreferencesStore() {
|
|
260
|
+
let snapshot = fallbackSnapshot()
|
|
261
|
+
let scope
|
|
262
|
+
let unsubscribe
|
|
263
|
+
const listeners = new Set()
|
|
264
|
+
|
|
265
|
+
const publish = (next) => {
|
|
266
|
+
if (snapshot.status === next.status && snapshot.value === next.value && snapshot.revision === next.revision && snapshot.writable === next.writable && snapshot.mode === next.mode) return
|
|
267
|
+
snapshot = Object.freeze(next)
|
|
268
|
+
for (const listener of listeners) listener()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const sync = () => {
|
|
272
|
+
if (scope === undefined) return publish(fallbackSnapshot())
|
|
273
|
+
const source = scope.getSnapshot()
|
|
274
|
+
const decoded = decodeTaskPreferences(source.value)
|
|
275
|
+
publish({
|
|
276
|
+
status: source.status,
|
|
277
|
+
value: decoded ?? DEFAULT_TASK_PREFERENCES,
|
|
278
|
+
revision: source.revision,
|
|
279
|
+
// ConfigFormController may report the document's writable=true even when
|
|
280
|
+
// this entry is absent. Never write defaults into an unserved namespace.
|
|
281
|
+
writable: source.status === 'ready' && decoded !== undefined && source.writable === true,
|
|
282
|
+
mode: source.mode,
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const disconnect = () => {
|
|
287
|
+
unsubscribe?.()
|
|
288
|
+
unsubscribe = undefined
|
|
289
|
+
scope = undefined
|
|
290
|
+
sync()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
source: Object.freeze({
|
|
295
|
+
getSnapshot: () => snapshot,
|
|
296
|
+
subscribe(listener) {
|
|
297
|
+
listeners.add(listener)
|
|
298
|
+
return () => listeners.delete(listener)
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
connect(nextScope) {
|
|
302
|
+
disconnect()
|
|
303
|
+
scope = nextScope
|
|
304
|
+
sync()
|
|
305
|
+
unsubscribe = nextScope.subscribe(sync)
|
|
306
|
+
return () => {
|
|
307
|
+
if (scope !== nextScope) return
|
|
308
|
+
disconnect()
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
set(field, value) {
|
|
312
|
+
if (!FIELDS.has(field)) return Promise.reject(new TypeError(`unknown Better Tasks setting: ${String(field)}`))
|
|
313
|
+
if (scope === undefined || !snapshot.writable) return Promise.reject(new Error('Better Tasks settings are not writable'))
|
|
314
|
+
const optimistic = decodeTaskPreferences({ ...snapshot.value, [field]: value })
|
|
315
|
+
if (optimistic === undefined) return Promise.reject(new TypeError(`invalid Better Tasks setting: ${field}`))
|
|
316
|
+
const writeScope = scope
|
|
317
|
+
publish({ ...snapshot, value: optimistic })
|
|
318
|
+
const rejected = (error) => {
|
|
319
|
+
if (scope === writeScope) sync()
|
|
320
|
+
throw error
|
|
321
|
+
}
|
|
322
|
+
let result
|
|
323
|
+
try {
|
|
324
|
+
result = writeScope.set(field, value)
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return Promise.reject(error).catch(rejected)
|
|
327
|
+
}
|
|
328
|
+
return Promise.resolve(result).then((accepted) => {
|
|
329
|
+
// 0.2 resolves false after a rejected mutation and recovery read; it does
|
|
330
|
+
// not reject. Legacy settings scopes may resolve void on success.
|
|
331
|
+
if (accepted === false) throw new Error('Better Tasks settings write was rejected by Host')
|
|
332
|
+
return accepted
|
|
333
|
+
}).catch(rejected)
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const SIDEBAR_CHILD_SLOTS = Object.freeze([
|
|
339
|
+
'sidebar.brand.mark',
|
|
340
|
+
'sidebar.brand.name',
|
|
341
|
+
'sidebar.panellist',
|
|
342
|
+
'sidebar.panellist.actions',
|
|
343
|
+
'sidebar.workspaces',
|
|
344
|
+
'sidebar.settings',
|
|
345
|
+
'sidebar.footer.action',
|
|
346
|
+
'sidebar.session.action',
|
|
347
|
+
])
|
|
348
|
+
|
|
349
|
+
function createSidebarChildDeclarations({ iconsInFactory = false } = {}) {
|
|
350
|
+
return Object.fromEntries(SIDEBAR_CHILD_SLOTS.filter(name => !iconsInFactory || name !== 'sidebar.panellist').map((name) => [name, {
|
|
351
|
+
kind: name === 'sidebar.session.action' ? 'hoc'
|
|
352
|
+
: name === 'sidebar.panellist' || name === 'sidebar.panellist.actions' || name === 'sidebar.footer.action' ? 'list' : 'single',
|
|
353
|
+
scope: 'root',
|
|
354
|
+
}]))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const ACTIVITY_RANGE_DAYS = Object.freeze({
|
|
358
|
+
all: null,
|
|
359
|
+
'1d': 1,
|
|
360
|
+
'3d': 3,
|
|
361
|
+
'7d': 7,
|
|
362
|
+
'14d': 14,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
366
|
+
|
|
367
|
+
function compareStableRecency(left, right) {
|
|
368
|
+
if (left.updatedAt !== right.updatedAt) return right.updatedAt - left.updatedAt
|
|
369
|
+
const leftId = String(left.id)
|
|
370
|
+
const rightId = String(right.id)
|
|
371
|
+
return leftId < rightId ? -1 : leftId > rightId ? 1 : 0
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function beforeIdForTaskDrop(orderedIds, overId, half) {
|
|
375
|
+
if (half !== 'before' && half !== 'after') throw new Error(`unsupported drop half: ${half}`)
|
|
376
|
+
const index = orderedIds.indexOf(overId)
|
|
377
|
+
if (index === -1) throw new Error(`unknown drop target: ${overId}`)
|
|
378
|
+
return half === 'before' ? overId : orderedIds[index + 1] ?? null
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function indexWorkspacesBySession(workspaces) {
|
|
382
|
+
const index = new Map()
|
|
383
|
+
for (const workspace of workspaces) {
|
|
384
|
+
for (const sessionId of workspace.sessionIds) {
|
|
385
|
+
if (!index.has(sessionId)) index.set(sessionId, workspace)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return index
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function loadProjectAppearances(fetchImpl, url = '/api/project-appearance') {
|
|
392
|
+
try {
|
|
393
|
+
const response = await fetchImpl(url, { cache: 'no-store' })
|
|
394
|
+
if (!response.ok) throw new Error(`appearance request failed: ${response.status}`)
|
|
395
|
+
const payload = await response.json()
|
|
396
|
+
if (!Array.isArray(payload.items) || !Array.isArray(payload.icons)) {
|
|
397
|
+
throw new Error('appearance response is missing items or icons')
|
|
398
|
+
}
|
|
399
|
+
return Object.freeze({
|
|
400
|
+
status: 'ready',
|
|
401
|
+
items: payload.items,
|
|
402
|
+
icons: payload.icons,
|
|
403
|
+
colorKeys: Array.isArray(payload.colorKeys) ? payload.colorKeys : [],
|
|
404
|
+
})
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return Object.freeze({
|
|
407
|
+
status: 'unavailable',
|
|
408
|
+
items: [],
|
|
409
|
+
icons: [],
|
|
410
|
+
colorKeys: [],
|
|
411
|
+
error: String(error?.message ?? error),
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function summarizeGoal(value) {
|
|
417
|
+
if (value === undefined) return Object.freeze({ availability: 'unknown' })
|
|
418
|
+
if (value === null || value.goal.phase === 'complete') return Object.freeze({ availability: 'none' })
|
|
419
|
+
const reason = value.goal.blockedReason
|
|
420
|
+
return Object.freeze({
|
|
421
|
+
availability: 'ready',
|
|
422
|
+
id: value.goal.id,
|
|
423
|
+
phase: value.goal.phase,
|
|
424
|
+
objective: value.goal.objective,
|
|
425
|
+
roundsStarted: value.roundsStarted,
|
|
426
|
+
maxGoalRounds: value.goal.maxGoalRounds,
|
|
427
|
+
blockedReason: reason === undefined ? undefined : Object.freeze({ code: reason.code, message: reason.message }),
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function summarizeTodos(value) {
|
|
432
|
+
if (value === undefined) return Object.freeze({ availability: 'unknown' })
|
|
433
|
+
if (value === null) return Object.freeze({ availability: 'none' })
|
|
434
|
+
const counts = { pending: 0, inProgress: 0, completed: 0, total: value.length }
|
|
435
|
+
const items = value.map((todo) => {
|
|
436
|
+
if (todo.status === 'pending') counts.pending += 1
|
|
437
|
+
else if (todo.status === 'in_progress') counts.inProgress += 1
|
|
438
|
+
else if (todo.status === 'completed') counts.completed += 1
|
|
439
|
+
return Object.freeze({ content: todo.content, status: todo.status })
|
|
440
|
+
})
|
|
441
|
+
return Object.freeze({ availability: 'ready', ...counts, items: Object.freeze(items) })
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Warning is presentation only: keep idle actions available for stopped tasks. */
|
|
445
|
+
function taskExecutionWarning(summary, pendingInteraction, connectionState, catalogReady = true) {
|
|
446
|
+
if (connectionState === 'disconnected' || connectionState === 'connecting') return { kind: 'disconnected', detail: '' }
|
|
447
|
+
if (pendingInteraction != null || summary.running || !catalogReady) return null
|
|
448
|
+
const execution = summary.projectionValues?.betterTasksExecution
|
|
449
|
+
if (execution == null) return null // No capability / not yet hydrated is not failure.
|
|
450
|
+
if (execution.phase === 'running') return { kind: 'interrupted', detail: '' }
|
|
451
|
+
if (execution.phase !== 'ended') return null
|
|
452
|
+
const kind = execution.outcome
|
|
453
|
+
if (!['error', 'aborted', 'interrupted', 'max-tokens'].includes(kind)) return null
|
|
454
|
+
return { kind, detail: typeof execution.detail === 'string' ? execution.detail : '' }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function summarizeSession(summary, pendingInteraction) {
|
|
458
|
+
if (pendingInteraction !== undefined) {
|
|
459
|
+
return Object.freeze({ status: 'block', interaction: pendingInteraction.kind })
|
|
460
|
+
}
|
|
461
|
+
if (summary.running) return Object.freeze({ status: 'running' })
|
|
462
|
+
return Object.freeze({ status: 'idle' })
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Fold native live status through native subagent catalog edges, not fork ancestry.
|
|
467
|
+
* Status events win over older list rows, including explicit false. Walk upward
|
|
468
|
+
* once so grandchildren count even while their direct parent is idle. No retain,
|
|
469
|
+
* polling, execution ownership, or inference from a persisted open turn.
|
|
470
|
+
*/
|
|
471
|
+
function runningSessionTreeIds(list, sessionStatuses = new Map()) {
|
|
472
|
+
const rows = list.byId
|
|
473
|
+
const projections = list.projectionsBySession ?? {}
|
|
474
|
+
const parents = new Map()
|
|
475
|
+
const ids = new Set([...Object.keys(rows), ...Object.keys(projections), ...sessionStatuses.keys()])
|
|
476
|
+
for (const id of ids) {
|
|
477
|
+
const catalog = projections[id]?.values?.subagentCatalog ?? rows[id]?.projectionValues?.subagentCatalog
|
|
478
|
+
if (!Array.isArray(catalog)) continue
|
|
479
|
+
for (const child of catalog) {
|
|
480
|
+
ids.add(child.id)
|
|
481
|
+
if (!parents.has(child.id)) parents.set(child.id, new Set())
|
|
482
|
+
parents.get(child.id).add(id)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const running = new Set([...ids].filter((id) => (sessionStatuses.get(id)?.running ?? rows[id]?.running) === true))
|
|
486
|
+
const queue = [...running]
|
|
487
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
488
|
+
for (const parent of parents.get(queue[index]) ?? []) {
|
|
489
|
+
if (running.has(parent)) continue // Also bounds malformed cyclic catalogs.
|
|
490
|
+
running.add(parent)
|
|
491
|
+
queue.push(parent)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return running
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function projectTask(summary, currentId, pendingInteraction) {
|
|
498
|
+
const projections = summary.projectionValues
|
|
499
|
+
return Object.freeze({
|
|
500
|
+
id: summary.id,
|
|
501
|
+
title: summary.displayTitle ?? summary.title ?? String(summary.id),
|
|
502
|
+
cwd: summary.cwd,
|
|
503
|
+
updatedAt: summary.updatedAt,
|
|
504
|
+
current: summary.id === currentId,
|
|
505
|
+
goal: summarizeGoal(projections?.goal),
|
|
506
|
+
todos: summarizeTodos(projections?.todos),
|
|
507
|
+
session: summarizeSession(summary, pendingInteraction),
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Match the native 0.2 browser's main-view selection, not Agent running state. */
|
|
512
|
+
function currentTaskSessionId(list, panelActive = false) {
|
|
513
|
+
if (panelActive) return undefined
|
|
514
|
+
const rows = Object.values(list.byId)
|
|
515
|
+
// 0.2's catalog removed list.current; retention is updated as the view switches.
|
|
516
|
+
// Never fall back to an obsolete current id once the new contract is present.
|
|
517
|
+
if (rows.some((row) => Object.hasOwn(row, 'retainedBy'))) {
|
|
518
|
+
return rows.find((row) => (row.retainedBy?.mainView ?? 0) > 0)?.id
|
|
519
|
+
}
|
|
520
|
+
return list.current // Retained 0.1.5 compatibility only.
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Derive task cards in the exact Host-owned pin order. */
|
|
524
|
+
function derivePinnedTasks({ list, pinnedSessionIds, pendingInteractions = new Map(), panelActive = false, sessionStatuses = new Map() }) {
|
|
525
|
+
if (!Array.isArray(pinnedSessionIds)) throw new Error('pinnedSessionIds must be an array')
|
|
526
|
+
const seen = new Set()
|
|
527
|
+
const tasks = []
|
|
528
|
+
const currentId = currentTaskSessionId(list, panelActive)
|
|
529
|
+
const runningIds = runningSessionTreeIds(list, sessionStatuses)
|
|
530
|
+
for (const id of pinnedSessionIds) {
|
|
531
|
+
if (seen.has(id)) continue
|
|
532
|
+
seen.add(id)
|
|
533
|
+
const summary = list.byId[id]
|
|
534
|
+
if (summary === undefined || summary.blank || summary.origin === 'subagent') continue
|
|
535
|
+
tasks.push(projectTask({ ...summary, running: runningIds.has(id) }, currentId, pendingInteractions.get(id)))
|
|
536
|
+
}
|
|
537
|
+
return tasks
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Legacy bounded recent projection retained for compatibility tests and rollback.
|
|
542
|
+
* The task UI no longer uses it as membership or ordering authority.
|
|
543
|
+
*/
|
|
544
|
+
function deriveRecentTasks({
|
|
545
|
+
list,
|
|
546
|
+
archivedSessionIds = [],
|
|
547
|
+
pendingInteractions = new Map(),
|
|
548
|
+
activityRange = 'all',
|
|
549
|
+
now = Date.now(),
|
|
550
|
+
limit = 10,
|
|
551
|
+
}) {
|
|
552
|
+
if (!Object.hasOwn(ACTIVITY_RANGE_DAYS, activityRange)) {
|
|
553
|
+
throw new Error(`unsupported activity range: ${activityRange}`)
|
|
554
|
+
}
|
|
555
|
+
if (!Number.isSafeInteger(limit) || limit < 0) throw new Error('limit must be a non-negative safe integer')
|
|
556
|
+
|
|
557
|
+
const archived = new Set(archivedSessionIds)
|
|
558
|
+
const days = ACTIVITY_RANGE_DAYS[activityRange]
|
|
559
|
+
const cutoff = days === null ? Number.NEGATIVE_INFINITY : now - days * DAY_MS
|
|
560
|
+
const visible = []
|
|
561
|
+
|
|
562
|
+
for (const id of list.ids) {
|
|
563
|
+
const summary = list.byId[id]
|
|
564
|
+
if (summary === undefined) continue
|
|
565
|
+
if (summary.blank || summary.origin === 'subagent' || archived.has(summary.id)) continue
|
|
566
|
+
if (summary.updatedAt < cutoff) continue
|
|
567
|
+
visible.push(summary)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
visible.sort(compareStableRecency)
|
|
571
|
+
return visible.slice(0, limit).map((summary) => projectTask(summary, list.current, pendingInteractions.get(summary.id)))
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Keep mutations on DSH's native Session, Conversation and Goal transports.
|
|
575
|
+
async function continueIdleTask({ binding, text, goals, submit, getStatus, signal }) {
|
|
576
|
+
if (typeof text !== 'string' || text.trim() === '') throw new Error('Prompt cannot be empty')
|
|
577
|
+
const session = binding?.session
|
|
578
|
+
if (session === undefined) throw new Error('Session is unavailable')
|
|
579
|
+
signal?.throwIfAborted()
|
|
580
|
+
const snapshot = session.getSnapshot()
|
|
581
|
+
if (snapshot.removed || snapshot.openState !== undefined && snapshot.openState !== 'open') throw new Error('Session is unavailable')
|
|
582
|
+
let pending
|
|
583
|
+
let running = snapshot.running
|
|
584
|
+
if (getStatus !== undefined) {
|
|
585
|
+
const statusSource = getStatus(session.sessionId)
|
|
586
|
+
const statuses = typeof statusSource?.getSnapshot === 'function' ? statusSource.getSnapshot() : statusSource
|
|
587
|
+
const status = typeof statuses?.get === 'function' ? statuses.get(session.sessionId) : statuses
|
|
588
|
+
if (status === undefined || status === null || typeof status !== 'object') throw new Error('Session status is unavailable')
|
|
589
|
+
running ||= status?.running === true
|
|
590
|
+
pending = status?.pendingInteraction
|
|
591
|
+
} else {
|
|
592
|
+
// Legacy callers only. Desktop surface callers always supply the live status map.
|
|
593
|
+
pending = snapshot.pendingInteraction
|
|
594
|
+
}
|
|
595
|
+
if (running || pending != null) throw new Error('Session is no longer idle')
|
|
596
|
+
const result = submit === undefined
|
|
597
|
+
? await session.prompt([{ type: 'text', text }], 'steer', signal)
|
|
598
|
+
: await submit({ session, text, signal })
|
|
599
|
+
const accepted = submit === undefined ? result?.ok === true : result?.kind === 'success'
|
|
600
|
+
if (!accepted) throw new Error(result?.error?.message ?? result?.text ?? 'Prompt submission failed')
|
|
601
|
+
|
|
602
|
+
// Admission has succeeded. Goal inspection/resume failures are warnings only;
|
|
603
|
+
// neither cancellation nor a CAS conflict can resend or roll back this prompt.
|
|
604
|
+
try {
|
|
605
|
+
let goal
|
|
606
|
+
if (typeof goals?.get === 'function') {
|
|
607
|
+
const current = await goals.get(session.sessionId)
|
|
608
|
+
if (!current?.ok) throw new Error(current?.error?.message ?? 'Goal lookup failed')
|
|
609
|
+
goal = current.value
|
|
610
|
+
} else {
|
|
611
|
+
// Keep the pre-0.2 direct-call contract; the surface manager supplies get().
|
|
612
|
+
goal = session.projections?.faceOf('goal')?.getSnapshot()?.goal
|
|
613
|
+
}
|
|
614
|
+
if (goal == null || goal.phase === 'complete' || goal.phase === 'active' && goal.activation === 'armed') return { warning: null }
|
|
615
|
+
if (Number.isFinite(goal.maxGoalRounds) && Number.isFinite(goal.roundsStarted) && goal.roundsStarted >= goal.maxGoalRounds) {
|
|
616
|
+
return { warning: 'Goal was not resumed: its automatic continuation round budget is exhausted' }
|
|
617
|
+
}
|
|
618
|
+
if (typeof goals?.resume !== 'function') throw new Error('Goal service is unavailable')
|
|
619
|
+
const resumed = await goals.resume(session.sessionId, { id: goal.id, revision: goal.revision })
|
|
620
|
+
if (!resumed?.ok) throw new Error(resumed?.error?.message ?? 'Goal resume failed')
|
|
621
|
+
return { warning: null }
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return { warning: String(error?.message ?? error) }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function renameTaskSession({ binding, title, sessions, sessionId, signal }) {
|
|
628
|
+
if (typeof title !== 'string' || title.trim() === '') throw new Error('Title cannot be empty')
|
|
629
|
+
const trimmed = title.trim()
|
|
630
|
+
const rename = async (current, requireOpen = false) => {
|
|
631
|
+
signal?.throwIfAborted()
|
|
632
|
+
if (current?.session === undefined) throw new Error('Session is unavailable')
|
|
633
|
+
const snapshot = current.session.getSnapshot()
|
|
634
|
+
if (snapshot.removed || requireOpen && snapshot.openState !== 'open') throw new Error('Session is unavailable')
|
|
635
|
+
const result = await current.session.rename(trimmed)
|
|
636
|
+
if (!result?.ok) throw new Error(result?.error?.message ?? 'Rename failed')
|
|
637
|
+
}
|
|
638
|
+
if (sessions !== undefined) {
|
|
639
|
+
if (typeof sessionId !== 'string' || sessionId === '') throw new Error('Session is unavailable')
|
|
640
|
+
const operation = async (reference) => {
|
|
641
|
+
const current = await reference.ready
|
|
642
|
+
if (sessions.binding(sessionId) !== current) throw new Error('Session generation changed')
|
|
643
|
+
return rename(current, true)
|
|
644
|
+
}
|
|
645
|
+
if (typeof sessions.using === 'function') {
|
|
646
|
+
return sessions.using(sessionId, { source: 'controllerOperation', signal }, operation)
|
|
647
|
+
}
|
|
648
|
+
if (typeof sessions.retain !== 'function') throw new Error('Session retention is unavailable')
|
|
649
|
+
const reference = sessions.retain(sessionId, { source: 'controllerOperation', signal })
|
|
650
|
+
try { return await operation(reference) } finally { reference.release() }
|
|
651
|
+
}
|
|
652
|
+
return rename(binding)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// The official skill source inserts '/name ' as a plain user-text gesture.
|
|
656
|
+
// Keep this contract separate from the command menu and from the textarea UI.
|
|
657
|
+
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
658
|
+
|
|
659
|
+
function insertSkillGesture(draft, name, start, end = start) {
|
|
660
|
+
if (typeof draft !== 'string' || typeof name !== 'string' || !SKILL_NAME.test(name)) {
|
|
661
|
+
throw new TypeError('invalid skill gesture')
|
|
662
|
+
}
|
|
663
|
+
const from = Math.max(0, Math.min(draft.length, Number.isInteger(start) ? start : draft.length))
|
|
664
|
+
const to = Math.max(from, Math.min(draft.length, Number.isInteger(end) ? end : from))
|
|
665
|
+
const before = draft.slice(0, from)
|
|
666
|
+
const after = draft.slice(to)
|
|
667
|
+
const separator = before !== '' && !/\s$/.test(before) ? ' ' : ''
|
|
668
|
+
const gesture = `/${name} `
|
|
669
|
+
return { text: `${before}${separator}${gesture}${after}`, caret: before.length + separator.length + gesture.length }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function listTaskSkills(catalog, sessionId, signal) {
|
|
673
|
+
if (typeof sessionId !== 'string' || sessionId === '' || typeof catalog?.list !== 'function') {
|
|
674
|
+
throw new TypeError('skill catalog unavailable for session')
|
|
675
|
+
}
|
|
676
|
+
const result = await catalog.list({ sessionId }, signal)
|
|
677
|
+
if (!result?.ok) throw new Error(result?.error?.message ?? 'skills/list failed')
|
|
678
|
+
if (!Array.isArray(result.value?.skills)) throw new TypeError('invalid skills/list response')
|
|
679
|
+
return result.value.skills.filter((skill) => skill && SKILL_NAME.test(skill.name))
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const taskInputAbortReason = (signal) => signal?.reason ?? new Error('Input surface creation aborted')
|
|
683
|
+
|
|
684
|
+
// A waiting caller can leave promptly without cancelling a predecessor's admitted
|
|
685
|
+
// operation. The predecessor/factory keeps its own completion cleanup attached.
|
|
686
|
+
function taskInputWaitWithSignal(promise, signal) {
|
|
687
|
+
if (signal?.aborted) return Promise.reject(taskInputAbortReason(signal))
|
|
688
|
+
if (signal === undefined) return promise
|
|
689
|
+
return new Promise((resolve, reject) => {
|
|
690
|
+
const onAbort = () => { cleanup(); reject(taskInputAbortReason(signal)) }
|
|
691
|
+
const cleanup = () => signal.removeEventListener('abort', onAbort)
|
|
692
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
693
|
+
Promise.resolve(promise).then(value => { cleanup(); resolve(value) }, error => { cleanup(); reject(error) })
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** One task-owned native surface per Session, never the main composer's binding. */
|
|
698
|
+
function createTaskInputManager({ getConversation, getSessions, getStatus, getGoals, isPinned }) {
|
|
699
|
+
const entries = new Map()
|
|
700
|
+
const drafts = new Map()
|
|
701
|
+
const lifetime = new AbortController()
|
|
702
|
+
let disposed = false
|
|
703
|
+
const pinned = id => isPinned === undefined || isPinned(id)
|
|
704
|
+
const freshGoals = {
|
|
705
|
+
get: id => {
|
|
706
|
+
const goals = getGoals?.()
|
|
707
|
+
if (typeof goals?.get !== 'function') throw new Error('Goal service is unavailable')
|
|
708
|
+
return goals.get(id)
|
|
709
|
+
},
|
|
710
|
+
resume: (id, ref) => {
|
|
711
|
+
const goals = getGoals?.()
|
|
712
|
+
if (typeof goals?.resume !== 'function') throw new Error('Goal service is unavailable')
|
|
713
|
+
return goals.resume(id, ref)
|
|
714
|
+
},
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function close(entry) {
|
|
718
|
+
if (entry.closing) return
|
|
719
|
+
entry.closing = true
|
|
720
|
+
// This signal closes UI/creation. The native surface retains any admitted
|
|
721
|
+
// operation; its attempt signal is deliberately not replaced by this one.
|
|
722
|
+
entry.controller.abort(new Error('Task input surface closed'))
|
|
723
|
+
entry.handle?.dispose()
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function create(sessionId, { signal, onWarning } = {}) {
|
|
727
|
+
if (typeof sessionId !== 'string' || sessionId === '') throw new Error('Session is unavailable')
|
|
728
|
+
const waitingSignal = signal === undefined ? lifetime.signal : AbortSignal.any([signal, lifetime.signal])
|
|
729
|
+
waitingSignal.throwIfAborted()
|
|
730
|
+
if (disposed) throw new Error('Task input manager is disposed')
|
|
731
|
+
while (entries.has(sessionId)) {
|
|
732
|
+
const previous = entries.get(sessionId)
|
|
733
|
+
if (!previous.closing) throw new Error('Task input surface is already active')
|
|
734
|
+
await taskInputWaitWithSignal(previous.finished, waitingSignal)
|
|
735
|
+
waitingSignal.throwIfAborted()
|
|
736
|
+
}
|
|
737
|
+
if (!pinned(sessionId)) { drafts.delete(sessionId); throw new Error('Task is no longer pinned') }
|
|
738
|
+
const conversation = getConversation?.()
|
|
739
|
+
if (typeof conversation?.input?.createSurface !== 'function') throw new Error('Native task input surface is unavailable')
|
|
740
|
+
const controller = new AbortController()
|
|
741
|
+
let resolveFinished
|
|
742
|
+
const entry = { controller, closing: false, handle: undefined, stop: undefined, done: false,
|
|
743
|
+
finished: new Promise(resolve => { resolveFinished = resolve }) }
|
|
744
|
+
entries.set(sessionId, entry)
|
|
745
|
+
const onAbort = () => close(entry)
|
|
746
|
+
waitingSignal.addEventListener('abort', onAbort, { once: true })
|
|
747
|
+
const save = () => {
|
|
748
|
+
if (entries.get(sessionId) !== entry) return
|
|
749
|
+
if (disposed || !pinned(sessionId)) { drafts.delete(sessionId); return }
|
|
750
|
+
const text = entry.handle?.state.getSnapshot().draft
|
|
751
|
+
if (typeof text === 'string') drafts.set(sessionId, text)
|
|
752
|
+
}
|
|
753
|
+
const finish = () => {
|
|
754
|
+
if (entry.done) return
|
|
755
|
+
entry.done = true
|
|
756
|
+
// Preserve late native failure-restoration before removing the subscriber.
|
|
757
|
+
try { save() } finally {
|
|
758
|
+
try { entry.stop?.() } finally {
|
|
759
|
+
waitingSignal.removeEventListener('abort', onAbort)
|
|
760
|
+
if (entries.get(sessionId) === entry) entries.delete(sessionId)
|
|
761
|
+
resolveFinished()
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const creation = Promise.resolve().then(() => {
|
|
766
|
+
controller.signal.throwIfAborted()
|
|
767
|
+
return conversation.input.createSurface({
|
|
768
|
+
sessionId, signal: controller.signal,
|
|
769
|
+
...(drafts.has(sessionId) ? { initialDraft: drafts.get(sessionId) } : {}),
|
|
770
|
+
defaultSink: async ({ binding, text, signal: attemptSignal }) => {
|
|
771
|
+
const sessions = getSessions?.()
|
|
772
|
+
if (binding?.session?.sessionId !== sessionId || sessions?.binding(sessionId) !== binding) {
|
|
773
|
+
throw new Error('Session generation changed')
|
|
774
|
+
}
|
|
775
|
+
const sender = getConversation?.()
|
|
776
|
+
if (typeof sender?.sendSession !== 'function') throw new Error('Native Session sender is unavailable')
|
|
777
|
+
const { warning } = await continueIdleTask({
|
|
778
|
+
binding, text, goals: freshGoals, getStatus, signal: attemptSignal,
|
|
779
|
+
submit: ({ session, text: message, signal: sendSignal }) => sender.sendSession(session, message, [], 'steer', sendSignal),
|
|
780
|
+
})
|
|
781
|
+
if (warning !== null) {
|
|
782
|
+
// The prompt is already accepted, including when the task UI closed
|
|
783
|
+
// during flight. Keep the existing global warning channel alive.
|
|
784
|
+
try { entry.handle?.input.notify('info', warning) } catch { /* warning delivery cannot fail admission */ }
|
|
785
|
+
try { onWarning?.(warning) } catch { /* caller notification cannot fail admission */ }
|
|
786
|
+
}
|
|
787
|
+
return { kind: 'success' }
|
|
788
|
+
},
|
|
789
|
+
})
|
|
790
|
+
}).then(handle => {
|
|
791
|
+
entry.handle = handle
|
|
792
|
+
try {
|
|
793
|
+
entry.stop = handle.state.subscribe(save)
|
|
794
|
+
save()
|
|
795
|
+
} catch (error) {
|
|
796
|
+
close(entry)
|
|
797
|
+
Promise.resolve(handle.closed).then(finish, finish).catch(() => {})
|
|
798
|
+
throw error
|
|
799
|
+
}
|
|
800
|
+
Promise.resolve(handle.closed).then(finish, finish).catch(() => {})
|
|
801
|
+
if (entry.closing || waitingSignal.aborted || disposed) {
|
|
802
|
+
handle.dispose()
|
|
803
|
+
throw taskInputAbortReason(waitingSignal.aborted ? waitingSignal : controller.signal)
|
|
804
|
+
}
|
|
805
|
+
return { ...handle, dispose: () => close(entry) }
|
|
806
|
+
}, error => {
|
|
807
|
+
finish()
|
|
808
|
+
throw error
|
|
809
|
+
})
|
|
810
|
+
// Race only the caller's creation wait. A late factory success is still
|
|
811
|
+
// disposed above and its final restored draft collected after closed.
|
|
812
|
+
return taskInputWaitWithSignal(creation, waitingSignal)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return {
|
|
816
|
+
create,
|
|
817
|
+
rename(sessionId, title) {
|
|
818
|
+
if (disposed) return Promise.reject(new Error('Task input manager is disposed'))
|
|
819
|
+
const sessions = getSessions?.()
|
|
820
|
+
if (sessions === undefined) return Promise.reject(new Error('Session retention is unavailable'))
|
|
821
|
+
return renameTaskSession({ sessions, sessionId, title })
|
|
822
|
+
},
|
|
823
|
+
dispose() {
|
|
824
|
+
if (disposed) return
|
|
825
|
+
disposed = true
|
|
826
|
+
lifetime.abort(new Error('Task input manager is disposed'))
|
|
827
|
+
for (const entry of entries.values()) close(entry)
|
|
828
|
+
drafts.clear()
|
|
829
|
+
},
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const name = 'dsh-better-tasks'
|
|
834
|
+
// The shared question surface is optional: older/native question packages keep
|
|
835
|
+
// their composer behavior without delaying sidebar, Session, or Workspace startup.
|
|
836
|
+
const inject = ['slots', 'layout', 'locale']
|
|
837
|
+
|
|
838
|
+
const NS = 'better-tasks-sidebar'
|
|
839
|
+
const STYLE_ID = 'dsh-better-tasks/sidebar'
|
|
840
|
+
const TASK_GRID_TWO_COLUMN_THRESHOLD = 528
|
|
841
|
+
const FINAL_CACHE_KEY = 'dsh-better-tasks.final-message-cache.v1'
|
|
842
|
+
/**
|
|
843
|
+
* Build revision, injected by scripts/build-client.mjs as the sha256 of this
|
|
844
|
+
* plugin's client-side sources. Every render-probe payload carries it: the
|
|
845
|
+
* browser's localStorage is an append log, so records from earlier builds stay
|
|
846
|
+
* forever and only payloads whose revision matches the delivered build count.
|
|
847
|
+
*/
|
|
848
|
+
const BUILD_REVISION = 'da11927b17db72c2a819ac8fc74933039cf592c8ba30b2b9efc45149c0b9c0ca'
|
|
849
|
+
/** Render probe: on by default, so a delivered build can prove the column rendered. */
|
|
850
|
+
const RENDER_PROBE = true
|
|
851
|
+
/** Per-render payload; rewritten on every invocation. */
|
|
852
|
+
const RENDER_PROBE_KEY = 'dbt-render-probe'
|
|
853
|
+
/** One-shot environment facts for the same build. */
|
|
854
|
+
const RENDER_ENV_KEY = 'dbt-render-env'
|
|
855
|
+
/** Cumulative call-mix ledger; survives the per-frame payload being overwritten. */
|
|
856
|
+
const RENDER_LEDGER_KEY = 'dbt-render-ledger'
|
|
857
|
+
/** The inner component's own throw, captured in the same fiber before it escapes. */
|
|
858
|
+
const RENDER_ERROR_KEY = 'dbt-render-error'
|
|
859
|
+
/** The view this plugin kept in localStorage before D1 moved it to the Config. */
|
|
860
|
+
const LEGACY_VIEW_KEY = 'dsh-better-tasks.view'
|
|
861
|
+
/** Set once the legacy localStorage view has been folded into the preference. */
|
|
862
|
+
const VIEW_MIGRATED_KEY = 'dsh-better-tasks.view-migrated'
|
|
863
|
+
|
|
864
|
+
let renderProbeCount = 0
|
|
865
|
+
let renderBareCount = 0
|
|
866
|
+
let renderReactCount = 0
|
|
867
|
+
let firstBareStack = null
|
|
868
|
+
|
|
869
|
+
/** Diagnostics must never break a render. */
|
|
870
|
+
function writeStorage(key, value) {
|
|
871
|
+
try { localStorage.setItem(key, value) } catch {}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Capture the call site of a bare invocation.
|
|
876
|
+
*
|
|
877
|
+
* Taken while this frame is still the top of the stack, so the frames above it are
|
|
878
|
+
* the caller's. This is the only way to name the path that invokes the registered
|
|
879
|
+
* component outside React.
|
|
880
|
+
* @returns the first 25 frames, truncated to 1500 characters.
|
|
881
|
+
*/
|
|
882
|
+
function captureCallStack() {
|
|
883
|
+
try {
|
|
884
|
+
return String(new Error().stack ?? '').split('\n').slice(0, 25).join('\n').slice(0, 1500)
|
|
885
|
+
} catch {
|
|
886
|
+
return ''
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Write one render-probe payload. Called at the very top of the guarded root,
|
|
892
|
+
* BEFORE any dry-run short circuit: a guard that returns first would hide the
|
|
893
|
+
* bare call this probe exists to observe, and the A/B question would stay
|
|
894
|
+
* unanswered forever while looking like "did not crash".
|
|
895
|
+
* @param input - the frame's observations.
|
|
896
|
+
*/
|
|
897
|
+
function writeRenderProbe(input) {
|
|
898
|
+
renderProbeCount += 1
|
|
899
|
+
if (input.zeroArg) renderBareCount += 1
|
|
900
|
+
if (input.hookOk && !input.zeroArg) renderReactCount += 1
|
|
901
|
+
if (input.zeroArg && firstBareStack === null) firstBareStack = input.stack ?? ''
|
|
902
|
+
const via = input.zeroArg && !input.hookOk ? 'bare' : (input.hookOk ? 'react' : 'unknown')
|
|
903
|
+
writeStorage(RENDER_PROBE_KEY, JSON.stringify({
|
|
904
|
+
revision: BUILD_REVISION,
|
|
905
|
+
rendered: renderProbeCount,
|
|
906
|
+
zeroArg: input.zeroArg,
|
|
907
|
+
isElement: input.isElement,
|
|
908
|
+
propKeys: input.propKeys,
|
|
909
|
+
durationMs: input.durationMs,
|
|
910
|
+
// The A/B criterion itself: a hook is legal here only on a React render
|
|
911
|
+
// stack, so a failed probe means this call site is not React rendering.
|
|
912
|
+
hookOk: input.hookOk,
|
|
913
|
+
via,
|
|
914
|
+
zeroArgCount: renderBareCount,
|
|
915
|
+
// Only on a bare call, and only from the true call site.
|
|
916
|
+
...(input.zeroArg ? { stack: input.stack ?? '' } : {}),
|
|
917
|
+
}))
|
|
918
|
+
// One overwritten key cannot show a MIX of callers: the last frame wins, so
|
|
919
|
+
// "all six were bare" and "five bare then one React render" look identical.
|
|
920
|
+
// The ledger keeps the totals, making `react: 0` a provable fact.
|
|
921
|
+
writeRenderLedger()
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Rewrite the cumulative ledger. Shared by the per-frame probe and the inner
|
|
926
|
+
* component's throw capture, so the error count is visible in the same record.
|
|
927
|
+
*/
|
|
928
|
+
function writeRenderLedger() {
|
|
929
|
+
writeStorage(RENDER_LEDGER_KEY, JSON.stringify({
|
|
930
|
+
revision: BUILD_REVISION,
|
|
931
|
+
total: renderProbeCount,
|
|
932
|
+
react: renderReactCount,
|
|
933
|
+
bare: renderBareCount,
|
|
934
|
+
zeroArgCount: renderBareCount,
|
|
935
|
+
firstBareStack,
|
|
936
|
+
}))
|
|
937
|
+
}
|
|
938
|
+
const dictionaries = {
|
|
939
|
+
zh: {
|
|
940
|
+
'brand.fallback': 'DSH',
|
|
941
|
+
'session.new': '新会话',
|
|
942
|
+
'session.new.label': '新建会话',
|
|
943
|
+
'toggle.open': '打开侧边栏',
|
|
944
|
+
'toggle.collapse': '收起侧边栏',
|
|
945
|
+
'panels.label': '全局面板',
|
|
946
|
+
'panels.more': '更多',
|
|
947
|
+
'panels.unpin': '取消固定 {name}',
|
|
948
|
+
'view.native': '会话',
|
|
949
|
+
'view.tasks': '任务',
|
|
950
|
+
'tasks.goal': 'Goal',
|
|
951
|
+
'tasks.todo': 'Todo',
|
|
952
|
+
'tasks.todoCompleted': '已完成',
|
|
953
|
+
'tasks.todoInProgress': '进行中',
|
|
954
|
+
'tasks.todoPending': '待处理',
|
|
955
|
+
'tasks.empty': '还没有置顶会话',
|
|
956
|
+
'tasks.orderRestoring': '正在恢复任务顺序…',
|
|
957
|
+
'tasks.pinMigration': '正在合并已有置顶任务…',
|
|
958
|
+
'tasks.expandGoal': '展开 Goal',
|
|
959
|
+
'tasks.collapseGoal': '收起 Goal',
|
|
960
|
+
'tasks.expandTodo': '展开 Todo',
|
|
961
|
+
'tasks.collapseTodo': '收起 Todo',
|
|
962
|
+
'tasks.final': 'Final',
|
|
963
|
+
'tasks.expandFinal': '展开 Final message',
|
|
964
|
+
'tasks.collapseFinal': '收起 Final message',
|
|
965
|
+
'tasks.finalLoading': '正在读取 Final message…',
|
|
966
|
+
'tasks.finalNone': '没有 Final message',
|
|
967
|
+
'tasks.finalError': 'Final message 读取失败',
|
|
968
|
+
'tasks.finalRetry': '重试',
|
|
969
|
+
'tasks.finalTruncated': '内容已截断',
|
|
970
|
+
'tasks.copyCode': '复制代码',
|
|
971
|
+
'tasks.copiedCode': '已复制',
|
|
972
|
+
'tasks.footnotes': '脚注',
|
|
973
|
+
'tasks.unpin': '从任务中移除',
|
|
974
|
+
'tasks.continue': '继续任务',
|
|
975
|
+
'tasks.continueDefault': '继续',
|
|
976
|
+
'tasks.inputLoading': '正在打开输入框…',
|
|
977
|
+
'tasks.inputSearch': '搜索技能或指令…',
|
|
978
|
+
'tasks.inputEmpty': '未找到匹配的技能或指令',
|
|
979
|
+
'tasks.inputCatalogError': '部分项目加载失败,请收起后重试',
|
|
980
|
+
'tasks.retryInput': '重试',
|
|
981
|
+
'tasks.expandInput': '展开续行输入栏',
|
|
982
|
+
'tasks.collapseInput': '收起续行输入栏',
|
|
983
|
+
'tasks.goalResumeWarning': '消息已发送,但 Goal 恢复失败',
|
|
984
|
+
'tasks.send': '发送',
|
|
985
|
+
'tasks.add': '添加',
|
|
986
|
+
'tasks.skillSearch': '搜索技能',
|
|
987
|
+
'tasks.skillLoading': '正在加载…',
|
|
988
|
+
'tasks.skillEmpty': '没有匹配的技能',
|
|
989
|
+
'tasks.skillUnavailable': '技能列表暂不可用',
|
|
990
|
+
'tasks.rename': '重命名会话',
|
|
991
|
+
'tasks.renameAction': '重命名',
|
|
992
|
+
'tasks.actionUnavailable': '当前会话状态已变化,请重试',
|
|
993
|
+
'tasks.runningAria': '运行中',
|
|
994
|
+
'tasks.blockAria': '等待交互',
|
|
995
|
+
'tasks.answer': '去回答',
|
|
996
|
+
'tasks.questionLoading': '正在加载问题选项…',
|
|
997
|
+
'tasks.answerHint': '打开该会话的问题面板',
|
|
998
|
+
'tasks.answerFailed': '没能打开问题面板,请打开会话作答',
|
|
999
|
+
'tasks.idleAria': '空闲',
|
|
1000
|
+
'tasks.execution.disconnected': '连接已断开 · 任务状态未知',
|
|
1001
|
+
'tasks.execution.error': '执行失败',
|
|
1002
|
+
'tasks.execution.aborted': '执行已停止',
|
|
1003
|
+
'tasks.execution.interrupted': '执行已中断',
|
|
1004
|
+
'tasks.execution.max-tokens': '执行已中断 · 达到输出上限',
|
|
1005
|
+
'tasks.appearanceUnavailable': '项目外观暂不可用',
|
|
1006
|
+
'tasks.unknown': '未知',
|
|
1007
|
+
'tasks.none': '无',
|
|
1008
|
+
'tasks.editAppearance': '设置项目图标和颜色',
|
|
1009
|
+
'tasks.appearanceTitle': '项目外观',
|
|
1010
|
+
'tasks.iconSearch': '搜索图标',
|
|
1011
|
+
'tasks.cancel': '取消',
|
|
1012
|
+
'tasks.save': '保存',
|
|
1013
|
+
'settings.nav': '任务视图',
|
|
1014
|
+
'settings.title': '任务视图',
|
|
1015
|
+
'settings.description': '自定义任务卡投影的显示方式。修改会立即生效。',
|
|
1016
|
+
'settings.fontSize': '详情正文字号',
|
|
1017
|
+
'settings.fontSizeHint': '影响 Todo、Goal、Final 的展开正文及任务卡问题区。',
|
|
1018
|
+
'settings.titleEmphasis': '任务卡名称强调',
|
|
1019
|
+
'settings.titleEmphasis.project': '强调项目名',
|
|
1020
|
+
'settings.titleEmphasis.task': '强调任务名',
|
|
1021
|
+
'settings.titleEmphasisHint': '仅调整项目名与任务名的字号、字重和主次色。',
|
|
1022
|
+
'settings.defaultExpansion': '默认展开',
|
|
1023
|
+
'settings.todoExpanded': 'Todo',
|
|
1024
|
+
'settings.goalExpanded': 'Goal',
|
|
1025
|
+
'settings.finalExpanded': 'Final',
|
|
1026
|
+
'settings.inputExpanded': '快速消息栏',
|
|
1027
|
+
'settings.expansionHint': '默认值适用于未手动调整的内容。Goal 换目标、Todo 换目标或清单内容、Final 和快速消息栏进入新空闲周期时恢复默认;仅进度变化不重置 Todo。当前正在浏览的任务进入新空闲周期时不自动展开 Final;手动展开仍有效。快速消息栏仅在空闲卡片的 Final 展开时显示。',
|
|
1028
|
+
'settings.columns': '任务列布局',
|
|
1029
|
+
'settings.columns.auto': '自动',
|
|
1030
|
+
'settings.columns.single': '单列',
|
|
1031
|
+
'settings.columns.double': '双列',
|
|
1032
|
+
'settings.columnsHint': '自动模式在侧边栏宽度大于 528px 时使用双列。',
|
|
1033
|
+
'settings.loading': '正在读取设置…',
|
|
1034
|
+
'settings.unavailable': '设置暂不可用,当前使用默认值。',
|
|
1035
|
+
'settings.readOnly': '当前连接只读。',
|
|
1036
|
+
'settings.writeFailed': '保存失败,已恢复 Host 设置。',
|
|
1037
|
+
'time.now': '刚刚',
|
|
1038
|
+
'time.minutes': '{n}分钟',
|
|
1039
|
+
'time.hours': '{n}小时',
|
|
1040
|
+
'time.days': '{n}天',
|
|
1041
|
+
'time.months': '{n}个月',
|
|
1042
|
+
'time.years': '{n}年',
|
|
1043
|
+
},
|
|
1044
|
+
en: {
|
|
1045
|
+
'brand.fallback': 'DSH',
|
|
1046
|
+
'session.new': 'New Session',
|
|
1047
|
+
'session.new.label': 'New session',
|
|
1048
|
+
'toggle.open': 'Open sidebar',
|
|
1049
|
+
'toggle.collapse': 'Collapse sidebar',
|
|
1050
|
+
'panels.label': 'Global panels',
|
|
1051
|
+
'panels.more': 'More',
|
|
1052
|
+
'panels.unpin': 'Unpin {name}',
|
|
1053
|
+
'view.native': 'Sessions',
|
|
1054
|
+
'view.tasks': 'Tasks',
|
|
1055
|
+
'tasks.goal': 'Goal',
|
|
1056
|
+
'tasks.todo': 'Todo',
|
|
1057
|
+
'tasks.todoCompleted': 'Completed',
|
|
1058
|
+
'tasks.todoInProgress': 'In progress',
|
|
1059
|
+
'tasks.todoPending': 'Pending',
|
|
1060
|
+
'tasks.empty': 'No pinned sessions yet',
|
|
1061
|
+
'tasks.orderRestoring': 'Restoring task order…',
|
|
1062
|
+
'tasks.pinMigration': 'Importing existing pinned tasks…',
|
|
1063
|
+
'tasks.expandGoal': 'Expand Goal',
|
|
1064
|
+
'tasks.collapseGoal': 'Collapse Goal',
|
|
1065
|
+
'tasks.expandTodo': 'Expand Todo',
|
|
1066
|
+
'tasks.collapseTodo': 'Collapse Todo',
|
|
1067
|
+
'tasks.final': 'Final',
|
|
1068
|
+
'tasks.expandFinal': 'Expand final message',
|
|
1069
|
+
'tasks.collapseFinal': 'Collapse final message',
|
|
1070
|
+
'tasks.finalLoading': 'Loading final message…',
|
|
1071
|
+
'tasks.finalNone': 'No final message',
|
|
1072
|
+
'tasks.finalError': 'Failed to load final message',
|
|
1073
|
+
'tasks.finalRetry': 'Retry',
|
|
1074
|
+
'tasks.finalTruncated': 'Content truncated',
|
|
1075
|
+
'tasks.copyCode': 'Copy code',
|
|
1076
|
+
'tasks.copiedCode': 'Copied',
|
|
1077
|
+
'tasks.footnotes': 'Footnotes',
|
|
1078
|
+
'tasks.unpin': 'Unpin task',
|
|
1079
|
+
'tasks.continue': 'Continue task',
|
|
1080
|
+
'tasks.continueDefault': 'Continue',
|
|
1081
|
+
'tasks.inputLoading': 'Opening input…',
|
|
1082
|
+
'tasks.inputSearch': 'Search skills or commands…',
|
|
1083
|
+
'tasks.inputEmpty': 'No matching skills or commands',
|
|
1084
|
+
'tasks.inputCatalogError': 'Some items could not load. Close and reopen to retry.',
|
|
1085
|
+
'tasks.retryInput': 'Retry',
|
|
1086
|
+
'tasks.expandInput': 'Expand follow-up input',
|
|
1087
|
+
'tasks.collapseInput': 'Collapse follow-up input',
|
|
1088
|
+
'tasks.goalResumeWarning': 'Message sent, but Goal resume failed',
|
|
1089
|
+
'tasks.send': 'Send',
|
|
1090
|
+
'tasks.add': 'Add',
|
|
1091
|
+
'tasks.skillSearch': 'Search skills',
|
|
1092
|
+
'tasks.skillLoading': 'Loading…',
|
|
1093
|
+
'tasks.skillEmpty': 'No matching skills',
|
|
1094
|
+
'tasks.skillUnavailable': 'Skills are unavailable',
|
|
1095
|
+
'tasks.rename': 'Rename session',
|
|
1096
|
+
'tasks.renameAction': 'Rename',
|
|
1097
|
+
'tasks.actionUnavailable': 'Session state changed; please try again',
|
|
1098
|
+
'tasks.runningAria': 'Running',
|
|
1099
|
+
'tasks.blockAria': 'Waiting for interaction',
|
|
1100
|
+
'tasks.answer': 'Go answer',
|
|
1101
|
+
'tasks.questionLoading': 'Loading question options…',
|
|
1102
|
+
'tasks.answerHint': 'Open this Session’s question panel',
|
|
1103
|
+
'tasks.answerFailed': 'Could not open the question panel — open the Session to answer',
|
|
1104
|
+
'tasks.idleAria': 'Idle',
|
|
1105
|
+
'tasks.execution.disconnected': 'Connection lost · task status unknown',
|
|
1106
|
+
'tasks.execution.error': 'Execution failed',
|
|
1107
|
+
'tasks.execution.aborted': 'Execution stopped',
|
|
1108
|
+
'tasks.execution.interrupted': 'Execution interrupted',
|
|
1109
|
+
'tasks.execution.max-tokens': 'Execution stopped · token limit reached',
|
|
1110
|
+
'tasks.appearanceUnavailable': 'Project appearance is unavailable',
|
|
1111
|
+
'tasks.unknown': 'Unknown',
|
|
1112
|
+
'tasks.none': 'None',
|
|
1113
|
+
'tasks.editAppearance': 'Set project icon and color',
|
|
1114
|
+
'tasks.appearanceTitle': 'Project appearance',
|
|
1115
|
+
'tasks.iconSearch': 'Search icons',
|
|
1116
|
+
'tasks.cancel': 'Cancel',
|
|
1117
|
+
'tasks.save': 'Save',
|
|
1118
|
+
'settings.nav': 'Task View',
|
|
1119
|
+
'settings.title': 'Task View',
|
|
1120
|
+
'settings.description': 'Customize how task-card projections are displayed. Changes apply immediately.',
|
|
1121
|
+
'settings.fontSize': 'Detail text size',
|
|
1122
|
+
'settings.fontSizeHint': 'Affects expanded Todo, Goal, and Final text, plus task-card questions.',
|
|
1123
|
+
'settings.titleEmphasis': 'Task card name emphasis',
|
|
1124
|
+
'settings.titleEmphasis.project': 'Emphasize project',
|
|
1125
|
+
'settings.titleEmphasis.task': 'Emphasize task',
|
|
1126
|
+
'settings.titleEmphasisHint': 'Changes only the project and task name size, weight, and color.',
|
|
1127
|
+
'settings.defaultExpansion': 'Default expansion',
|
|
1128
|
+
'settings.todoExpanded': 'Todo',
|
|
1129
|
+
'settings.goalExpanded': 'Goal',
|
|
1130
|
+
'settings.finalExpanded': 'Final',
|
|
1131
|
+
'settings.inputExpanded': 'Quick message bar',
|
|
1132
|
+
'settings.expansionHint': 'Defaults apply where no manual choice exists. Goal resets for a new goal; Todo for a new goal or list contents; Final and the quick message bar for a new idle cycle. Progress-only changes keep Todo choices. The currently browsed task does not automatically open Final for a new idle cycle; manual opening still works. The quick message bar appears only on idle cards with Final expanded.',
|
|
1133
|
+
'settings.columns': 'Task columns',
|
|
1134
|
+
'settings.columns.auto': 'Auto',
|
|
1135
|
+
'settings.columns.single': 'Single',
|
|
1136
|
+
'settings.columns.double': 'Double',
|
|
1137
|
+
'settings.columnsHint': 'Auto uses two columns when the sidebar is wider than 528px.',
|
|
1138
|
+
'settings.loading': 'Loading settings…',
|
|
1139
|
+
'settings.unavailable': 'Settings are unavailable; defaults are active.',
|
|
1140
|
+
'settings.readOnly': 'This connection is read-only.',
|
|
1141
|
+
'settings.writeFailed': 'Save failed; Host settings were restored.',
|
|
1142
|
+
'time.now': 'now',
|
|
1143
|
+
'time.minutes': '{n}min',
|
|
1144
|
+
'time.hours': '{n}h',
|
|
1145
|
+
'time.days': '{n}d',
|
|
1146
|
+
'time.months': '{n}mo',
|
|
1147
|
+
'time.years': '{n}y',
|
|
1148
|
+
},
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Single numeric source for sidebar presentation; upstream semantic tokens stay native.
|
|
1152
|
+
const SIDEBAR_LAYOUT = Object.freeze({
|
|
1153
|
+
newSessionHeight: 38, newSessionFontSize: 14, newSessionLineHeight: 22,
|
|
1154
|
+
iconTarget: 34, railTarget: 36, iconSize: 18, unpinTarget: 18,
|
|
1155
|
+
navigationGap: 4, maxNavigationRows: 2, tooltipDelayMs: 500,
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
const css = `
|
|
1159
|
+
.dbt-sidebar{--dbt-pad:12px;--dbt-nav-target:${SIDEBAR_LAYOUT.iconTarget}px;--dbt-rail-target:${SIDEBAR_LAYOUT.railTarget}px;--dbt-nav-gap:${SIDEBAR_LAYOUT.navigationGap}px;--dbt-nav-rows:${SIDEBAR_LAYOUT.maxNavigationRows};--dbt-new-height:${SIDEBAR_LAYOUT.newSessionHeight}px;--dbt-new-font:${SIDEBAR_LAYOUT.newSessionFontSize}px;--dbt-new-line-height:${SIDEBAR_LAYOUT.newSessionLineHeight}px;--dbt-unpin-target:${SIDEBAR_LAYOUT.unpinTarget}px;box-sizing:border-box;width:100%;height:100%;min-height:0;display:flex;flex-direction:column;gap:8px;padding:10px var(--dbt-pad);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);overflow:hidden}
|
|
1160
|
+
.dbt-sidebar[data-wide="false"]{--dbt-pad:8px;align-items:center;padding-inline:8px}
|
|
1161
|
+
.dbt-brand-row{width:100%;height:32px;display:flex;align-items:center;gap:6px;flex:none}
|
|
1162
|
+
.dbt-brand,.dbt-icon-button,.dbt-new-session,.dbt-panel-row{border:0;font:inherit;color:inherit;cursor:pointer}
|
|
1163
|
+
.dbt-brand{min-width:0;display:flex;align-items:center;gap:8px;flex:1;background:transparent;padding:0 4px;text-align:left}
|
|
1164
|
+
.dbt-brand-mark,.dbt-panel-glyph,.dbt-rail-mark{display:inline-flex;align-items:center;justify-content:center;flex:none}
|
|
1165
|
+
.dbt-brand-name,.dbt-panel-label,.dbt-new-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1166
|
+
.dbt-brand-name{font-weight:650}
|
|
1167
|
+
.dbt-icon-button{corner-shape:round;width:28px;height:28px;color:var(--dsw-alias-label-secondary);border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;padding:0;background:transparent;flex:none}
|
|
1168
|
+
.dbt-icon-button:hover,.dbt-panel-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1169
|
+
.dbt-sidebar[data-wide="false"] .dbt-brand-row{height:36px}
|
|
1170
|
+
.dbt-sidebar[data-wide="false"] .dbt-icon-button{width:36px;height:36px;color:var(--dsw-alias-label-primary)}
|
|
1171
|
+
.dbt-sidebar[data-wide="false"] .dbt-toggle .dbt-panel-icon{display:none}
|
|
1172
|
+
.dbt-sidebar[data-wide="false"] .dbt-toggle:hover .dbt-panel-icon{display:inline}
|
|
1173
|
+
.dbt-sidebar[data-wide="false"] .dbt-toggle:hover .dbt-rail-mark{display:none}
|
|
1174
|
+
/* Desktop collapses this column to zero, not to the browser's icon rail.
|
|
1175
|
+
Match the native sidebar's Windows caption affordances instead of changing
|
|
1176
|
+
the layout owner or forcing a phantom rail width. */
|
|
1177
|
+
html[data-windows-titlebar] .dbt-sidebar .dbt-toggle{position:fixed;top:calc((var(--dsh-windows-titlebar-height) - 28px) / 2);left:12px;z-index:30;width:28px;height:28px;-webkit-app-region:no-drag}
|
|
1178
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-brand-row{height:0}
|
|
1179
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"]{padding:0}
|
|
1180
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-toggle .dbt-panel-icon{display:inline}
|
|
1181
|
+
html[data-windows-titlebar] .dbt-sidebar .dbt-toggle .dbt-rail-mark{display:none}
|
|
1182
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-new-session{position:fixed;top:calc((var(--dsh-windows-titlebar-height) - 28px) / 2);left:48px;z-index:30;width:28px;height:28px;margin:0;padding:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary);-webkit-app-region:no-drag}
|
|
1183
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-new-session:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1184
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-panel-list,html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-workspaces,html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-foot{display:none}
|
|
1185
|
+
html[data-windows-titlebar]:has(.dbt-sidebar[data-wide="false"]){--dsh-windows-menu-start:84px}
|
|
1186
|
+
.dbt-shell-leading{display:flex;align-items:center;gap:8px;-webkit-app-region:no-drag}
|
|
1187
|
+
/* Native New Session tokens; the rc.3 HOC anchor is display:contents.
|
|
1188
|
+
The row owns spacing, so optional actions sit beside (not inside) the button. */
|
|
1189
|
+
.dbt-new-session-row{width:calc(100% - 8px);min-width:0;display:flex;align-items:center;gap:6px;flex:none;margin:0 4px}
|
|
1190
|
+
.dbt-new-session{container-type:inline-size;position:relative;flex:1 1 auto;min-width:0;display:flex;align-items:center;justify-content:center;gap:0;width:auto;height:var(--dbt-new-height);padding:8px 12px;margin:0;box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-button-elevated-fill);color:var(--dsw-alias-label-primary);font-size:var(--dbt-new-font);font-weight:500;line-height:var(--dbt-new-line-height);cursor:pointer;overflow:hidden}
|
|
1191
|
+
.dbt-new-session:hover{background:var(--dsw-alias-button-floating-hover)}
|
|
1192
|
+
.dbt-new-label-mask{flex:1;min-width:0}
|
|
1193
|
+
.dbt-new-content{display:flex;width:100cqw;align-items:center;justify-content:center;gap:6px}
|
|
1194
|
+
.dbt-new-content>svg{flex:none}
|
|
1195
|
+
.dbt-new-label{max-width:200px;overflow:hidden;white-space:nowrap}
|
|
1196
|
+
.dbt-new-shortcut{opacity:0;display:inline-flex;flex:none;pointer-events:none;font-weight:400}
|
|
1197
|
+
.dbt-new-session:is(:hover,:focus-visible) .dbt-new-shortcut{opacity:1}
|
|
1198
|
+
.dbt-new-session:is(:hover,:focus-visible) .dbt-new-label-mask:has(+.dbt-new-shortcut){overflow:hidden;mask-image:linear-gradient(to right,black calc(100% - 16px),transparent)}
|
|
1199
|
+
html[data-platform="darwin"] .dbt-new-session{background:var(--dsw-specific-sidebar-glass-fill)}
|
|
1200
|
+
html[data-platform="darwin"] .dbt-new-session:hover{background:var(--dsw-specific-sidebar-glass-hover)}
|
|
1201
|
+
.dbt-sidebar[data-wide="false"] .dbt-new-session-row{width:36px;flex-direction:column;align-items:center;gap:4px;margin:0}
|
|
1202
|
+
.dbt-sidebar[data-wide="false"] .dbt-new-session{flex:none;width:36px;height:36px;justify-content:center;padding:0;margin:0;border-color:transparent;border-radius:var(--dsw-radius-md);background:transparent}
|
|
1203
|
+
.dbt-sidebar[data-wide="false"] .dbt-new-session:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1204
|
+
.dbt-panel-list{width:100%;display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--dbt-nav-gap);flex:none;max-height:calc(var(--dbt-nav-target)*var(--dbt-nav-rows) + var(--dbt-nav-gap)*(var(--dbt-nav-rows) - 1));overflow-y:auto;overflow-x:hidden;padding-block:3px;scrollbar-width:thin}
|
|
1205
|
+
.dbt-panel-row{width:var(--dbt-nav-target);height:var(--dbt-nav-target);flex:none;border:0;border-radius:var(--dsw-radius-md);display:inline-flex;align-items:center;justify-content:center;padding:0;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
1206
|
+
.dbt-panel-row:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1207
|
+
.dbt-panel-row[aria-current="page"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-brand-primary)}
|
|
1208
|
+
.dbt-panel-glyph{display:inline-flex;align-items:center;justify-content:center}
|
|
1209
|
+
.dbt-panel-entry{position:relative;display:inline-flex;flex:none}
|
|
1210
|
+
.dbt-panel-unpin{position:absolute;top:-3px;right:-3px;z-index:1;width:var(--dbt-unpin-target);height:var(--dbt-unpin-target);border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;padding:0;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;pointer-events:none}
|
|
1211
|
+
.dbt-panel-entry:is(:hover,:focus-within) .dbt-panel-unpin{opacity:1;pointer-events:auto}
|
|
1212
|
+
.dbt-panel-unpin:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
1213
|
+
.dbt-panel-more-anchor{display:inline-flex;flex:none}
|
|
1214
|
+
.dbt-panel-more-menu{min-width:220px}
|
|
1215
|
+
.dbt-sidebar[data-wide="false"] .dbt-panel-list{flex-direction:column;flex-wrap:nowrap;max-height:30%}
|
|
1216
|
+
.dbt-sidebar[data-wide="false"] .dbt-panel-row{width:var(--dbt-rail-target);height:var(--dbt-rail-target)}
|
|
1217
|
+
.dbt-panel-row:focus-visible,.dbt-panel-unpin:focus-visible,.dbt-new-session:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
1218
|
+
.dbt-workspaces{width:100%;min-height:0;display:flex;flex:1;overflow:hidden}
|
|
1219
|
+
.dbt-foot{width:100%;display:flex;flex-direction:column;gap:4px;flex:none}
|
|
1220
|
+
.dbt-footer-actions,.dbt-settings{width:100%}
|
|
1221
|
+
.dbt-sidebar[data-wide="false"] .dbt-footer-actions,.dbt-sidebar[data-wide="false"] .dbt-settings{display:flex;justify-content:center}
|
|
1222
|
+
.dbt-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
1223
|
+
.dbt-region-shell{width:100%;min-height:0;display:flex;flex:1;flex-direction:column;overflow:hidden}
|
|
1224
|
+
.dbt-view-tabs{display:grid;grid-template-columns:1fr 1fr;gap:2px;padding:0 4px;border-bottom:1px solid var(--dsw-alias-border-l1);background:transparent;flex:none}
|
|
1225
|
+
.dbt-view-tab{height:30px;margin:0 0 -1px;border:1px solid transparent;border-radius:8px 8px 0 0;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;font-size:12px}
|
|
1226
|
+
.dbt-view-tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
1227
|
+
.dbt-view-tab[aria-selected="true"]{border-color:var(--dsw-alias-border-l1);border-bottom-color:var(--dsw-alias-bg-layer-2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:600;box-shadow:none}
|
|
1228
|
+
.dbt-region-panel{min-width:0;min-height:0;display:flex;flex-direction:column;flex:1;overflow:hidden;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-top:0;border-radius:0 0 10px 10px;padding:6px}
|
|
1229
|
+
.dbt-task-view{min-height:0;display:flex;flex:1;flex-direction:column;gap:7px;overflow:hidden}
|
|
1230
|
+
.dbt-task-list{min-height:0;flex:1;overflow-y:auto;overflow-x:hidden;display:grid;grid-template-columns:minmax(0,1fr);grid-auto-rows:max-content;align-content:start;align-items:start;gap:7px;padding:2px}
|
|
1231
|
+
.dbt-task-list[data-columns="2"]{display:flex;align-items:flex-start}
|
|
1232
|
+
.dbt-task-column{min-width:0;flex:1;display:flex;flex-direction:column;gap:7px}
|
|
1233
|
+
.dbt-task-card{--dbt-task-tone:var(--dsw-alias-label-primary);--dbt-task-bg:var(--dsw-alias-bg-layer-1);--dbt-unpin-color:color-mix(in srgb,var(--dbt-task-bg) 80%,var(--dbt-task-tone));--dbt-task-border:color-mix(in srgb,currentColor 10%,transparent);--dbt-task-border-selected:color-mix(in srgb,var(--dsw-alias-brand-primary) 72%,var(--dsw-alias-label-primary));position:relative;box-sizing:border-box;width:100%;min-width:0;border:1px solid var(--dbt-task-border);border-radius:11px;background:var(--dbt-task-bg);padding:9px;display:flex;flex-direction:column;gap:8px;cursor:pointer;box-shadow:none}
|
|
1234
|
+
.dbt-task-card[data-state="idle"]{--dbt-task-border:color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent);--dbt-task-border-selected:color-mix(in srgb,var(--dsw-alias-label-primary) 34%,transparent);--dbt-task-bg:color-mix(in srgb,var(--dsw-alias-label-primary) 3%,var(--dsw-alias-bg-layer-1))}
|
|
1235
|
+
.dbt-task-card[data-state="running"]{--dbt-task-tone:var(--dsw-alias-success,#1f9d62);--dbt-task-border:color-mix(in srgb,var(--dsw-alias-success,#1f9d62) 24%,transparent);--dbt-task-border-selected:color-mix(in srgb,var(--dsw-alias-success,#1f9d62) 58%,transparent);--dbt-task-bg:color-mix(in srgb,var(--dsw-alias-success,#1f9d62) 12%,var(--dsw-alias-bg-layer-1))}
|
|
1236
|
+
.dbt-task-card[data-state="block"]{--dbt-block-yellow:#d2aa00;--dbt-task-tone:var(--dbt-block-yellow);--dbt-task-border:color-mix(in srgb,var(--dbt-block-yellow) 34%,transparent);--dbt-task-border-selected:color-mix(in srgb,var(--dbt-block-yellow) 72%,var(--dsw-alias-label-primary));--dbt-task-bg:color-mix(in srgb,var(--dbt-block-yellow) 18%,var(--dsw-alias-bg-layer-1))}
|
|
1237
|
+
.dbt-task-card[data-current="true"]{border-color:var(--dbt-task-border-selected);box-shadow:none;--dbt-task-bg:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,var(--dsw-alias-bg-layer-1))}
|
|
1238
|
+
.dbt-task-card[data-current="true"][data-state="running"]{--dbt-task-bg:color-mix(in srgb,var(--dsw-alias-success,#1f9d62) 17%,var(--dsw-alias-bg-layer-1))}
|
|
1239
|
+
.dbt-task-card[data-current="true"][data-state="block"]{--dbt-task-bg:color-mix(in srgb,var(--dbt-block-yellow) 23%,var(--dsw-alias-bg-layer-1))}
|
|
1240
|
+
.dbt-task-card[data-execution-warning]{--dbt-task-tone:var(--dsw-alias-state-error-primary);--dbt-task-border:color-mix(in srgb,var(--dsw-alias-state-error-primary) 24%,transparent);--dbt-task-border-selected:color-mix(in srgb,var(--dsw-alias-state-error-primary) 58%,transparent);--dbt-task-bg:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,var(--dsw-alias-bg-layer-1))}
|
|
1241
|
+
.dbt-task-card[data-current="true"][data-execution-warning]{--dbt-task-bg:color-mix(in srgb,var(--dsw-alias-state-error-primary) 17%,var(--dsw-alias-bg-layer-1))}
|
|
1242
|
+
.dbt-task-error-icon{flex:none;color:var(--dsw-alias-state-error-primary)}
|
|
1243
|
+
.dbt-task-card[data-drop="before"]:before,.dbt-task-card[data-drop="after"]:after{content:"";position:absolute;z-index:2;left:8px;right:8px;height:2px;border-radius:2px;background:var(--dsw-alias-brand-primary);pointer-events:none}
|
|
1244
|
+
.dbt-task-card[data-drop="before"]:before{top:1px}.dbt-task-card[data-drop="after"]:after{bottom:1px}
|
|
1245
|
+
.dbt-task-list[data-columns="2"] .dbt-task-card[data-drop="before"]:before,.dbt-task-list[data-columns="2"] .dbt-task-card[data-drop="after"]:after{top:8px;bottom:8px;width:2px;height:auto}
|
|
1246
|
+
.dbt-task-list[data-columns="2"] .dbt-task-card[data-drop="before"]:before{left:1px;right:auto}.dbt-task-list[data-columns="2"] .dbt-task-card[data-drop="after"]:after{left:auto;right:1px}
|
|
1247
|
+
.dbt-task-title-row{display:flex;align-items:center;gap:7px;min-width:0}
|
|
1248
|
+
.dbt-task-open{min-width:0;flex:1;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;padding:0}
|
|
1249
|
+
.dbt-task-project{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:680;line-height:19px}
|
|
1250
|
+
.dbt-task-session-line{display:flex;align-items:center;gap:5px;min-width:0;margin-top:1px}
|
|
1251
|
+
.dbt-task-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
1252
|
+
.dbt-task-rename{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;text-align:left;cursor:pointer;padding:0}
|
|
1253
|
+
.dbt-task-list[data-title-emphasis="task"] .dbt-task-project{font-size:11px;font-weight:400;line-height:16px;color:var(--dsw-alias-label-secondary)}
|
|
1254
|
+
.dbt-task-list[data-title-emphasis="task"] .dbt-task-rename{font-size:14px;font-weight:680;line-height:19px;color:var(--dsw-alias-label-primary)}
|
|
1255
|
+
.dbt-task-rename:hover{text-decoration:underline;color:var(--dsw-alias-label-primary)}
|
|
1256
|
+
.dbt-task-card[data-current="true"] .dbt-task-rename{color:var(--dsw-alias-brand-primary)}
|
|
1257
|
+
.dbt-task-list[data-title-emphasis="task"] .dbt-task-card[data-current="true"] .dbt-task-rename{color:var(--dsw-alias-brand-primary)}
|
|
1258
|
+
.dbt-continue-shell,.dbt-continue-form{cursor:text}
|
|
1259
|
+
.dbt-continue-form{box-sizing:border-box;display:flex;flex-direction:column;min-width:0;width:100%;border:1px solid var(--dsw-alias-border-l1);border-radius:18px;background:var(--dsw-specific-input-major);box-shadow:var(--dsw-elevation-soft)}
|
|
1260
|
+
.dbt-continue-form:focus-within{border-color:var(--dsw-alias-brand-primary)}
|
|
1261
|
+
.dbt-continue-form .dbt-action-input{min-width:0;min-height:32px;max-height:120px;resize:none;overflow-y:hidden;padding:7px 11px 5px;font-size:12px;line-height:18px;background:transparent;border:0;border-radius:18px 18px 0 0;outline:none}
|
|
1262
|
+
.dbt-continue-actions{box-sizing:border-box;display:flex;justify-content:space-between;align-items:center;min-height:36px;padding:1px 6px 6px}
|
|
1263
|
+
.dbt-continue-add{display:grid;place-items:center;flex:none;width:27px;height:27px;padding:0;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
1264
|
+
.dbt-continue-add:hover,.dbt-continue-add[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1265
|
+
.dbt-continue-add:disabled{opacity:.4;cursor:default}
|
|
1266
|
+
.dbt-continue-add:focus-visible,.dbt-skill-picker button:focus-visible,.dbt-skill-picker input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
1267
|
+
.dbt-skill-picker{margin:0 7px 4px;padding:7px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);cursor:default}
|
|
1268
|
+
.dbt-skill-picker input{box-sizing:border-box;width:100%;height:29px;padding:4px 7px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}
|
|
1269
|
+
.dbt-skill-results{max-height:180px;overflow-y:auto;margin-top:5px;display:flex;flex-direction:column;gap:2px}
|
|
1270
|
+
.dbt-skill-picker button{min-width:0;display:flex;flex-direction:column;gap:1px;width:100%;padding:5px 7px;border:0;border-radius:6px;text-align:left;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}
|
|
1271
|
+
.dbt-skill-picker button:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1272
|
+
.dbt-skill-picker small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;color:var(--dsw-alias-label-tertiary);font-size:11px}
|
|
1273
|
+
.dbt-skill-status{padding:6px;color:var(--dsw-alias-label-secondary);font-size:11px}
|
|
1274
|
+
.dbt-continue-send{corner-shape:round;flex:none;display:grid;place-items:center;box-sizing:border-box;width:28px;height:28px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;transition:background-color .1s}
|
|
1275
|
+
.dbt-continue-send:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}
|
|
1276
|
+
.dbt-continue-send:disabled{opacity:.4;cursor:default}
|
|
1277
|
+
.dbt-continue-send:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
1278
|
+
.dbt-continue-error{margin-top:-4px}
|
|
1279
|
+
.dbt-task-time{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary)}
|
|
1280
|
+
.dbt-project-icon{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;flex:none;border:0;border-radius:8px;background:var(--dsw-alias-bg-layer-2);cursor:pointer}
|
|
1281
|
+
.dbt-project-icon:disabled{cursor:default;opacity:.72}
|
|
1282
|
+
.dbt-task-unpin{width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex:none;padding:0;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--dbt-unpin-color);cursor:pointer;opacity:0;pointer-events:none;transition:border-color .12s ease}
|
|
1283
|
+
.dbt-task-unpin svg{width:14px;height:14px;flex:none}
|
|
1284
|
+
.dbt-task-card:hover .dbt-task-unpin,.dbt-task-card:focus-within .dbt-task-unpin,.dbt-task-unpin:focus-visible{opacity:1;pointer-events:auto}
|
|
1285
|
+
.dbt-task-unpin:hover{border-color:color-mix(in srgb,var(--dbt-unpin-color) 40%,transparent)}
|
|
1286
|
+
.dbt-task-unpin:disabled{cursor:default;opacity:.45}
|
|
1287
|
+
.dbt-task-projection-bar{display:flex;align-items:center;gap:5px}
|
|
1288
|
+
.dbt-projection-toggle{height:27px;min-width:34px;display:inline-flex;align-items:center;justify-content:center;gap:4px;border:0;border-radius:7px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
1289
|
+
.dbt-projection-toggle:hover,.dbt-projection-toggle[aria-expanded="true"]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
1290
|
+
.dbt-projection-toggle:disabled{cursor:default;opacity:.38;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}
|
|
1291
|
+
.dbt-todo-count{font-size:10px;font-variant-numeric:tabular-nums}
|
|
1292
|
+
.dbt-projection-details{display:flex;flex-direction:column;gap:6px;padding-top:1px}
|
|
1293
|
+
.dbt-projection-detail{display:grid;grid-template-columns:18px minmax(0,1fr);gap:6px;align-items:start;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 86%,transparent);padding:7px;font-size:var(--dbt-projection-font-size,13px);line-height:1.5}
|
|
1294
|
+
.dbt-projection-detail.dbt-final-detail{display:block;background:var(--dsw-alias-bg-layer-1);padding:10px;transition:background-color .34s ease}
|
|
1295
|
+
.dbt-projection-detail.dbt-final-detail[data-feedback="pending"]{background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,var(--dsw-alias-bg-layer-1));transition-duration:.22s}
|
|
1296
|
+
@media (prefers-reduced-motion:reduce){.dbt-projection-detail.dbt-final-detail{transition:none}}
|
|
1297
|
+
.dbt-final-input-toggle{box-sizing:border-box;width:calc(100% + 6px);margin:-3px;padding:8px;border:1px solid transparent;border-radius:8px;cursor:pointer;outline:none}
|
|
1298
|
+
.dbt-final-input-toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
1299
|
+
.dbt-projection-detail[data-tone="blocked"]{color:var(--dsw-alias-error,#d14343)}
|
|
1300
|
+
.dbt-projection-copy{min-width:0;overflow-wrap:anywhere}
|
|
1301
|
+
.dbt-final-copy{min-width:0;overflow-wrap:anywhere}
|
|
1302
|
+
/* Rebind the consumed Markdown shorthands locally: BODY-derived theme tokens do
|
|
1303
|
+
not recompute merely because a descendant changes the content-size input. */
|
|
1304
|
+
.dbt-final-copy,.dbt-question-surface{--dbt-text-size:var(--dbt-projection-font-size,13px);--dbt-secondary-size:min(calc(var(--dbt-text-size) - 1px),max(13px,calc(var(--dbt-text-size) - 2px)));--dsw-font-markdown-base:400 var(--dbt-text-size)/calc(var(--dbt-text-size) + 10px) var(--dsw-font-family);--dsw-font-markdown-base-strong:600 var(--dbt-text-size)/calc(var(--dbt-text-size) + 10px) var(--dsw-font-family);--dsw-font-markdown-h1:700 calc(var(--dbt-text-size) + 7px)/calc(var(--dbt-text-size) + 16px) var(--dsw-font-family);--dsw-font-markdown-h2:700 calc(var(--dbt-text-size) + 5px)/calc(var(--dbt-text-size) + 14px) var(--dsw-font-family);--dsw-font-markdown-h3:700 calc(var(--dbt-text-size) + 4px)/calc(var(--dbt-text-size) + 12px) var(--dsw-font-family);--dsw-font-markdown-h4:600 var(--dbt-text-size)/calc(var(--dbt-text-size) + 10px) var(--dsw-font-family);--dsw-font-markdown-table:400 var(--dbt-secondary-size)/calc(var(--dbt-secondary-size) + 9px) var(--dsw-font-family);--dsw-font-markdown-table-head:500 var(--dbt-secondary-size)/calc(var(--dbt-secondary-size) + 9px) var(--dsw-font-family);--dsw-font-markdown-code:400 calc(var(--dbt-text-size) * 11 / 14)/calc(var(--dbt-text-size) * 19 / 14) var(--ds-font-family-code);--dsw-font-markdown-code-block:400 calc(var(--dbt-text-size) * 11 / 14)/calc(var(--dbt-text-size) * 19 / 14) var(--ds-font-family-code)}
|
|
1305
|
+
.dbt-final-copy .md-code-block,.dbt-question-surface .md-code-block{--dsl-code-block-banner-font:400 calc(var(--dbt-text-size) * 11 / 14)/calc(var(--dbt-text-size) * 18 / 14) var(--ds-font-family-code)}
|
|
1306
|
+
.dbt-final-copy [data-code-block-banner] > :first-child,.dbt-question-surface [data-code-block-banner] > :first-child{font-size:inherit;line-height:inherit}
|
|
1307
|
+
.dbt-final-copy pre,.dbt-final-copy table{max-width:100%;overflow-x:auto}
|
|
1308
|
+
.dbt-final-copy img{max-width:100%;height:auto}
|
|
1309
|
+
.dbt-final-meta{margin-top:5px;color:var(--dsw-alias-label-tertiary);font-size:10px}
|
|
1310
|
+
.dbt-final-retry{margin-top:5px;border:0;border-radius:6px;padding:3px 7px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer}
|
|
1311
|
+
.dbt-question-surface{min-width:0;max-width:100%;overflow:hidden;font-size:var(--dbt-projection-font-size,13px);line-height:1.5}
|
|
1312
|
+
.dbt-question-notice{padding:6px 8px;color:var(--dsw-alias-warning,#a26900);font:inherit}
|
|
1313
|
+
.dbt-question-surface *{min-width:0}
|
|
1314
|
+
.dbt-task-answer{align-self:flex-start;margin:2px 0 0;padding:3px 10px;border-radius:6px;cursor:pointer;font:inherit;font-size:var(--dbt-projection-font-size,13px);line-height:1.5;color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dbt-block-yellow,#d2aa00) 18%,transparent);border:1px solid color-mix(in srgb,var(--dbt-block-yellow,#d2aa00) 45%,transparent)}
|
|
1315
|
+
.dbt-task-answer:hover{background:color-mix(in srgb,var(--dbt-block-yellow,#d2aa00) 28%,transparent)}
|
|
1316
|
+
.dbt-task-answer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
1317
|
+
.dbt-todo-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:5px}
|
|
1318
|
+
.dbt-todo-item{display:grid;grid-template-columns:16px minmax(0,1fr);gap:5px;align-items:start}
|
|
1319
|
+
.dbt-todo-glyph{display:grid;place-items:center;flex:none;width:16px;height:16px}
|
|
1320
|
+
.dbt-task-empty,.dbt-task-warning{padding:10px;border-radius:9px;font-size:11px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}
|
|
1321
|
+
.dbt-task-warning{padding:6px 8px;color:var(--dsw-alias-warning,#a26900)}
|
|
1322
|
+
.dbt-dialog-backdrop{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;background:color-mix(in srgb,#000 38%,transparent);padding:20px}
|
|
1323
|
+
.dbt-dialog{width:min(680px,92vw);max-height:min(720px,90vh);overflow:auto;border-radius:14px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 18px 60px rgba(0,0,0,.28);padding:18px;display:flex;flex-direction:column;gap:12px}
|
|
1324
|
+
.dbt-dialog h2{font-size:16px;margin:0}
|
|
1325
|
+
.dbt-dialog input{height:34px;border:1px solid color-mix(in srgb,currentColor 18%,transparent);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:0 10px}
|
|
1326
|
+
.dbt-action-input{box-sizing:border-box;width:100%;min-height:92px;resize:vertical;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);padding:9px 10px;font:inherit}
|
|
1327
|
+
.dbt-action-input.dbt-rename-input{min-height:44px;height:44px;border:.5px solid var(--dsw-alias-border-l4);border-radius:22px;background:transparent;padding:7px 14px;font-size:14px;font-weight:400;line-height:22px;resize:none}
|
|
1328
|
+
.dbt-icon-grid{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:6px;max-height:360px;overflow:auto}
|
|
1329
|
+
.dbt-icon-choice{aspect-ratio:1;border:1px solid color-mix(in srgb,currentColor 15%,transparent);border-radius:7px;background:transparent;color:inherit;cursor:pointer;display:grid;place-items:center}
|
|
1330
|
+
.dbt-icon-choice[data-selected="true"]{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
1331
|
+
.dbt-color-row{display:flex;flex-wrap:wrap;gap:7px}
|
|
1332
|
+
.dbt-color-choice{width:25px;height:25px;border-radius:50%;border:2px solid transparent;cursor:pointer}
|
|
1333
|
+
.dbt-color-choice[data-selected="true"]{outline:2px solid currentColor;outline-offset:2px}
|
|
1334
|
+
.dbt-dialog-actions{display:flex;justify-content:flex-end;gap:8px}
|
|
1335
|
+
.dbt-dialog-actions button{height:32px;border:0;border-radius:8px;padding:0 12px;cursor:pointer}
|
|
1336
|
+
.dbt-dialog-save{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-static-white,#fff)}
|
|
1337
|
+
.dbt-settings-section{width:min(680px,100%);display:flex;flex-direction:column;gap:18px;color:var(--dsw-alias-label-primary)}
|
|
1338
|
+
.dbt-settings-heading{display:flex;flex-direction:column;gap:5px}.dbt-settings-heading h2{margin:0;font-size:18px;line-height:1.3}.dbt-settings-heading p,.dbt-settings-hint{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
|
1339
|
+
.dbt-settings-group{display:flex;flex-direction:column;gap:10px}.dbt-settings-group-title{font-size:14px;font-weight:650}
|
|
1340
|
+
.dbt-settings-card{display:flex;flex-direction:column;gap:10px;border:1px solid color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px}
|
|
1341
|
+
.dbt-settings-font-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center}.dbt-settings-font-options{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:4px}.dbt-settings-font-option{height:32px;border:0;border-radius:7px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:pointer;font-variant-numeric:tabular-nums}.dbt-settings-font-option[aria-pressed="true"]{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-brand-primary);font-weight:650}.dbt-settings-font-option:disabled{cursor:default;opacity:.5}.dbt-settings-font-value{min-width:44px;text-align:right;font-variant-numeric:tabular-nums;font-weight:650}
|
|
1342
|
+
.dbt-settings-switch-row{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:30px}.dbt-settings-switch{position:relative;width:38px;height:22px;flex:none;border:0;border-radius:999px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-interactive-bg-hover));cursor:pointer;padding:0}.dbt-settings-switch:after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-static-white,#fff);box-shadow:0 1px 3px rgba(0,0,0,.25);transition:transform .14s ease}.dbt-settings-switch[aria-checked="true"]{background:var(--dsw-alias-brand-primary)}.dbt-settings-switch[aria-checked="true"]:after{transform:translateX(16px)}.dbt-settings-switch:disabled{cursor:default;opacity:.5}
|
|
1343
|
+
.dbt-settings-segments{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:4px;padding:3px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dbt-settings-segments[data-options="2"]{grid-template-columns:repeat(2,minmax(0,1fr))}.dbt-settings-segment{height:32px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.dbt-settings-segment[aria-pressed="true"]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px rgba(0,0,0,.12)}.dbt-settings-segment:disabled{cursor:default;opacity:.5}
|
|
1344
|
+
.dbt-settings-status{border-radius:8px;padding:8px 10px;font-size:12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}.dbt-settings-status[data-error="true"]{color:var(--dsw-alias-error,#d14343)}
|
|
1345
|
+
@media (prefers-reduced-motion:no-preference){.dbt-sidebar,.dbt-panel-row,.dbt-icon-button,.dbt-task-card{transition:background-color .15s ease,color .15s ease,border-color .15s ease}}
|
|
1346
|
+
`
|
|
1347
|
+
|
|
1348
|
+
function createSnapshotStore(initial) {
|
|
1349
|
+
let snapshot = initial
|
|
1350
|
+
const listeners = new Set()
|
|
1351
|
+
return {
|
|
1352
|
+
getSnapshot: () => snapshot,
|
|
1353
|
+
subscribe(listener) {
|
|
1354
|
+
listeners.add(listener)
|
|
1355
|
+
return () => listeners.delete(listener)
|
|
1356
|
+
},
|
|
1357
|
+
set(next) {
|
|
1358
|
+
snapshot = next
|
|
1359
|
+
for (const listener of listeners) listener()
|
|
1360
|
+
},
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Same transient owner/lifetime as native ui-sidebar: panel launchers are not Session Pins.
|
|
1365
|
+
function createPanelLauncherPins() {
|
|
1366
|
+
const source = createSnapshotStore(Object.freeze([]))
|
|
1367
|
+
return {
|
|
1368
|
+
source,
|
|
1369
|
+
setPinned(id, pinned) {
|
|
1370
|
+
if (typeof id !== 'string' || id === '' || typeof pinned !== 'boolean' || id === 'plugins') return
|
|
1371
|
+
const unpinned = source.getSnapshot()
|
|
1372
|
+
if (pinned !== unpinned.includes(id)) return
|
|
1373
|
+
source.set(Object.freeze(pinned ? unpinned.filter(value => value !== id) : [...unpinned, id]))
|
|
1374
|
+
},
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function SidebarPanelIcon({ id, size, active, renderSlot }) {
|
|
1379
|
+
return renderSlot('sidebar.panellist', { size, active }, { only: id })
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function resolveLabel(label, fallback) {
|
|
1383
|
+
if (typeof label === 'function') return String(label())
|
|
1384
|
+
return typeof label === 'string' && label !== '' ? label : fallback
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function installStyles() {
|
|
1388
|
+
if (typeof document === 'undefined') return () => {}
|
|
1389
|
+
const previous = document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`)
|
|
1390
|
+
if (previous !== null) return () => {}
|
|
1391
|
+
const tag = document.createElement('style')
|
|
1392
|
+
tag.dataset.plugin = name
|
|
1393
|
+
tag.dataset.pluginCss = STYLE_ID
|
|
1394
|
+
tag.textContent = css
|
|
1395
|
+
document.head.appendChild(tag)
|
|
1396
|
+
return () => tag.remove()
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function element(type, props, ...children) {
|
|
1400
|
+
return requireModule('react').createElement(type, props, ...children)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const COLOR_HEX = Object.freeze({
|
|
1404
|
+
blue: '#3b82f6', cyan: '#06b6d4', green: '#22c55e', orange: '#f97316',
|
|
1405
|
+
pink: '#ec4899', purple: '#8b5cf6', red: '#ef4444', yellow: '#eab308',
|
|
1406
|
+
teal: '#14b8a6', indigo: '#6366f1', amber: '#f59e0b', slate: '#64748b', black: '#111827',
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
function useStoredPreference(key, fallback, allowed) {
|
|
1410
|
+
const React = requireModule('react')
|
|
1411
|
+
const [value, setValue] = React.useState(() => {
|
|
1412
|
+
try {
|
|
1413
|
+
const stored = localStorage.getItem(key)
|
|
1414
|
+
return stored !== null && allowed.includes(stored) ? stored : fallback
|
|
1415
|
+
} catch {
|
|
1416
|
+
return fallback
|
|
1417
|
+
}
|
|
1418
|
+
})
|
|
1419
|
+
React.useEffect(() => {
|
|
1420
|
+
writeStorage(key, value)
|
|
1421
|
+
}, [key, value])
|
|
1422
|
+
return [value, setValue]
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function isProjectionExpanded(value, key, defaultExpanded) {
|
|
1426
|
+
return typeof value[key] === 'boolean' ? value[key] : defaultExpanded
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/** Native activity is lastPromptAt; cold cache misses may fall back to createdAt. */
|
|
1430
|
+
function isNewFinalIdleCycle(cycles, task) {
|
|
1431
|
+
if (!Number.isFinite(task.updatedAt) || task.updatedAt < 0) return false
|
|
1432
|
+
const consumed = cycles[task.id]
|
|
1433
|
+
// A cold/partial/older snapshot is not evidence of another completed cycle.
|
|
1434
|
+
// Never lower the persisted watermark and then reset again on hydration.
|
|
1435
|
+
return !Number.isFinite(consumed) || task.updatedAt > consumed
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function expandFinalForIdleCycles(cycles, idleTasks, value) {
|
|
1439
|
+
const nextCycles = { ...cycles }
|
|
1440
|
+
const nextExpanded = { ...value }
|
|
1441
|
+
let cyclesChanged = false
|
|
1442
|
+
let expandedChanged = false
|
|
1443
|
+
for (const task of idleTasks) {
|
|
1444
|
+
if (!isNewFinalIdleCycle(nextCycles, task)) continue
|
|
1445
|
+
nextCycles[task.id] = task.updatedAt
|
|
1446
|
+
cyclesChanged = true
|
|
1447
|
+
// Consume every new cycle, including the currently browsed Session. Its
|
|
1448
|
+
// suppression is remembered so navigation/refresh cannot open it later.
|
|
1449
|
+
const finalKey = `${task.id}:final`
|
|
1450
|
+
if (task.current === true) {
|
|
1451
|
+
// Explicit manual expansion wins; suppress only the automatic default.
|
|
1452
|
+
if (nextExpanded[finalKey] !== true && nextExpanded[finalKey] !== false) {
|
|
1453
|
+
nextExpanded[finalKey] = false
|
|
1454
|
+
expandedChanged = true
|
|
1455
|
+
}
|
|
1456
|
+
} else if (Object.hasOwn(nextExpanded, finalKey)) {
|
|
1457
|
+
delete nextExpanded[finalKey]
|
|
1458
|
+
expandedChanged = true
|
|
1459
|
+
}
|
|
1460
|
+
// Input remains subordinate to Final and starts a new disclosure cycle;
|
|
1461
|
+
// neither this reset nor suppression touches drafts, Todo, or Goal.
|
|
1462
|
+
const inputKey = `${task.id}:input`
|
|
1463
|
+
if (Object.hasOwn(nextExpanded, inputKey)) {
|
|
1464
|
+
delete nextExpanded[inputKey]
|
|
1465
|
+
expandedChanged = true
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
cycles: cyclesChanged ? nextCycles : cycles,
|
|
1470
|
+
expanded: expandedChanged ? nextExpanded : value,
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function reconcileGoalExpansionCycles(cycles, goals, endedSessionIds, value) {
|
|
1475
|
+
const nextCycles = { ...cycles }
|
|
1476
|
+
const nextExpanded = { ...value }
|
|
1477
|
+
let cyclesChanged = false
|
|
1478
|
+
let expandedChanged = false
|
|
1479
|
+
for (const sessionId of endedSessionIds) {
|
|
1480
|
+
if (Object.hasOwn(nextCycles, sessionId)) {
|
|
1481
|
+
delete nextCycles[sessionId]
|
|
1482
|
+
cyclesChanged = true
|
|
1483
|
+
}
|
|
1484
|
+
const key = `${sessionId}:goal`
|
|
1485
|
+
if (Object.hasOwn(nextExpanded, key)) {
|
|
1486
|
+
delete nextExpanded[key]
|
|
1487
|
+
expandedChanged = true
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
for (const goal of goals) {
|
|
1491
|
+
if (nextCycles[goal.id] === goal.goalId) continue
|
|
1492
|
+
nextCycles[goal.id] = goal.goalId
|
|
1493
|
+
cyclesChanged = true
|
|
1494
|
+
const key = `${goal.id}:goal`
|
|
1495
|
+
if (!Object.hasOwn(nextExpanded, key)) continue
|
|
1496
|
+
delete nextExpanded[key]
|
|
1497
|
+
expandedChanged = true
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
cycles: cyclesChanged ? nextCycles : cycles,
|
|
1501
|
+
expanded: expandedChanged ? nextExpanded : value,
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/** Progress-only changes do not start a new Todo disclosure lifecycle. */
|
|
1506
|
+
function todoExpansionCycle(task) {
|
|
1507
|
+
if (task.todos.availability === 'unknown' || task.goal.availability === 'unknown') return undefined
|
|
1508
|
+
return JSON.stringify([
|
|
1509
|
+
task.goal.availability === 'ready' ? task.goal.id : null,
|
|
1510
|
+
task.todos.availability === 'ready' ? task.todos.items.map((todo) => todo.content) : null,
|
|
1511
|
+
])
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function reconcileTodoExpansionCycles(cycles, tasks, value) {
|
|
1515
|
+
const nextCycles = { ...cycles }
|
|
1516
|
+
const nextExpanded = { ...value }
|
|
1517
|
+
let cyclesChanged = false
|
|
1518
|
+
let expandedChanged = false
|
|
1519
|
+
for (const task of tasks) {
|
|
1520
|
+
const cycle = todoExpansionCycle(task)
|
|
1521
|
+
// A temporarily unloaded projection is not the end of its lifecycle.
|
|
1522
|
+
if (cycle === undefined || nextCycles[task.id] === cycle) continue
|
|
1523
|
+
nextCycles[task.id] = cycle
|
|
1524
|
+
cyclesChanged = true
|
|
1525
|
+
const key = `${task.id}:todo`
|
|
1526
|
+
if (!Object.hasOwn(nextExpanded, key)) continue
|
|
1527
|
+
delete nextExpanded[key]
|
|
1528
|
+
expandedChanged = true
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
cycles: cyclesChanged ? nextCycles : cycles,
|
|
1532
|
+
expanded: expandedChanged ? nextExpanded : value,
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function parseFinalMessageCache(raw) {
|
|
1537
|
+
if (typeof raw !== 'string' || raw === '') return {}
|
|
1538
|
+
try {
|
|
1539
|
+
const parsed = JSON.parse(raw)
|
|
1540
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
1541
|
+
const states = {}
|
|
1542
|
+
for (const [sessionId, entry] of Object.entries(parsed).slice(-100)) {
|
|
1543
|
+
if (entry === null || typeof entry !== 'object' || entry.status !== 'ready') continue
|
|
1544
|
+
if (!['string', 'number'].includes(typeof entry.updatedAt)) continue
|
|
1545
|
+
const value = entry.value
|
|
1546
|
+
if (value === null || typeof value !== 'object' || value.sessionId !== sessionId) continue
|
|
1547
|
+
if (value.final !== null && (typeof value.final !== 'object' || typeof value.final.text !== 'string' || [...value.final.text].length > 4000)) continue
|
|
1548
|
+
states[sessionId] = { status: 'ready', updatedAt: entry.updatedAt, value }
|
|
1549
|
+
}
|
|
1550
|
+
return states
|
|
1551
|
+
} catch {
|
|
1552
|
+
return {}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function serializeFinalMessageCache(states) {
|
|
1557
|
+
const ready = Object.entries(states).filter(([, entry]) => entry?.status === 'ready').slice(-100)
|
|
1558
|
+
return JSON.stringify(Object.fromEntries(ready))
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function loadFinalMessageCache() {
|
|
1562
|
+
try { return parseFinalMessageCache(localStorage.getItem(FINAL_CACHE_KEY) ?? '') } catch { return {} }
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function parseExpansionRecord(raw) {
|
|
1566
|
+
try {
|
|
1567
|
+
const parsed = JSON.parse(raw ?? '{}')
|
|
1568
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
1569
|
+
return Object.fromEntries(Object.entries(parsed).filter(([entryKey, expanded]) => entryKey.length > 0 && typeof expanded === 'boolean').slice(-300))
|
|
1570
|
+
} catch {
|
|
1571
|
+
return {}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Desktop's dsh-app renderer uses its isolated Electron user-data storage.
|
|
1576
|
+
// Host Config remains the owner of defaults; this is only per-client disclosure.
|
|
1577
|
+
// Accessing the localStorage property itself may throw, so guard the entire read.
|
|
1578
|
+
function loadStoredExpansion(key, legacyKey) {
|
|
1579
|
+
try {
|
|
1580
|
+
if (typeof localStorage === 'undefined') return {}
|
|
1581
|
+
const current = localStorage.getItem(key)
|
|
1582
|
+
if (current !== null) return parseExpansionRecord(current)
|
|
1583
|
+
if (legacyKey === undefined) return {}
|
|
1584
|
+
const legacy = parseExpansionRecord(localStorage.getItem(legacyKey))
|
|
1585
|
+
return Object.fromEntries(Object.entries(legacy).filter(([entryKey, expanded]) => entryKey.endsWith(':todo') && expanded === true))
|
|
1586
|
+
} catch {
|
|
1587
|
+
return {}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function useStoredExpansion(key, legacyKey) {
|
|
1592
|
+
const React = requireModule('react')
|
|
1593
|
+
const [value, setValue] = React.useState(() => loadStoredExpansion(key, legacyKey))
|
|
1594
|
+
React.useEffect(() => {
|
|
1595
|
+
writeStorage(key, JSON.stringify(value))
|
|
1596
|
+
}, [key, value])
|
|
1597
|
+
return [value, setValue]
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function useStoredRecord(key) {
|
|
1601
|
+
const React = requireModule('react')
|
|
1602
|
+
const [value, setValue] = React.useState(() => {
|
|
1603
|
+
try {
|
|
1604
|
+
if (typeof localStorage === 'undefined') return {}
|
|
1605
|
+
const parsed = JSON.parse(localStorage.getItem(key) ?? '{}')
|
|
1606
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
1607
|
+
// Keep at least as many cycle markers as bounded disclosure entries; a
|
|
1608
|
+
// missing marker must not reset a surviving manual choice on refresh.
|
|
1609
|
+
return Object.fromEntries(Object.entries(parsed).filter(([entryKey, cycle]) => entryKey.length > 0 && ['string', 'number'].includes(typeof cycle)).slice(-300))
|
|
1610
|
+
} catch {
|
|
1611
|
+
return {}
|
|
1612
|
+
}
|
|
1613
|
+
})
|
|
1614
|
+
React.useEffect(() => {
|
|
1615
|
+
writeStorage(key, JSON.stringify(value))
|
|
1616
|
+
}, [key, value])
|
|
1617
|
+
return [value, setValue]
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function useProjectAppearanceSnapshot() {
|
|
1621
|
+
const React = requireModule('react')
|
|
1622
|
+
const [state, setState] = React.useState({ status: 'loading', items: [], icons: [], colorKeys: [] })
|
|
1623
|
+
const [revision, setRevision] = React.useState(0)
|
|
1624
|
+
React.useEffect(() => {
|
|
1625
|
+
let alive = true
|
|
1626
|
+
loadProjectAppearances((url, options) => fetch(url, options)).then((next) => {
|
|
1627
|
+
if (alive) setState(next)
|
|
1628
|
+
})
|
|
1629
|
+
return () => { alive = false }
|
|
1630
|
+
}, [revision])
|
|
1631
|
+
React.useEffect(() => {
|
|
1632
|
+
if (typeof window === 'undefined') return
|
|
1633
|
+
const reload = () => setRevision((value) => value + 1)
|
|
1634
|
+
window.addEventListener('project-appearance-changed', reload)
|
|
1635
|
+
return () => window.removeEventListener('project-appearance-changed', reload)
|
|
1636
|
+
}, [])
|
|
1637
|
+
return state
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function renderCatalogIcon(icon, size = 18) {
|
|
1641
|
+
if (icon === undefined) return element(requireModule('@deepseek-ai/dsh-client-ui-primitives').IconFolderCloseRegular, { size })
|
|
1642
|
+
return element('svg', {
|
|
1643
|
+
viewBox: '0 0 24 24', width: size, height: size, fill: 'none', stroke: 'currentColor',
|
|
1644
|
+
strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true',
|
|
1645
|
+
}, ...icon.nodes.map(([tag, props], index) => element(tag, { ...props, key: `${tag}-${index}` })))
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/** Resolve verified 0.2 Regular exports lazily; never draw approximate replacement glyphs. */
|
|
1649
|
+
function nativeIcon(exportName, defaultSize = 14) {
|
|
1650
|
+
return function NativeIcon({ size = defaultSize, ...rest }) {
|
|
1651
|
+
const Icon = requireModule('@deepseek-ai/dsh-client-ui-primitives')[exportName]
|
|
1652
|
+
return element(Icon, { size, ...rest })
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const IconPanelLeftOutlineRegular = nativeIcon('IconPanelLeftOutlineRegular', 16)
|
|
1657
|
+
const IconChecklistOutlineRegular = nativeIcon('IconChecklistOutlineRegular')
|
|
1658
|
+
const IconChevronDownOutlineRegular = nativeIcon('IconChevronDownOutlineRegular')
|
|
1659
|
+
const IconChevronRightOutlineRegular = nativeIcon('IconChevronRightOutlineRegular')
|
|
1660
|
+
const IconGoalOutlineRegular = nativeIcon('IconGoalOutlineRegular')
|
|
1661
|
+
const IconSparkleRegular = nativeIcon('IconSparkleRegular')
|
|
1662
|
+
|
|
1663
|
+
function renderOfficialPinIcon(size = 14) {
|
|
1664
|
+
const { IconPinFillRegular } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1665
|
+
return element(IconPinFillRegular, { size })
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function AppearanceDialog({ workspace, appearance, appearanceState, onClose, t }) {
|
|
1669
|
+
const React = requireModule('react')
|
|
1670
|
+
const [query, setQuery] = React.useState('')
|
|
1671
|
+
const [iconKey, setIconKey] = React.useState(appearance?.iconKey ?? 'folder')
|
|
1672
|
+
const [colorKey, setColorKey] = React.useState(appearance?.colorKey ?? 'blue')
|
|
1673
|
+
const [busy, setBusy] = React.useState(false)
|
|
1674
|
+
const [error, setError] = React.useState('')
|
|
1675
|
+
const colors = appearanceState.colorKeys.length > 0 ? appearanceState.colorKeys : Object.keys(COLOR_HEX)
|
|
1676
|
+
const needle = query.trim().toLowerCase()
|
|
1677
|
+
const icons = appearanceState.icons.filter((icon) => needle === '' || String(icon.label ?? icon.key).toLowerCase().includes(needle)).slice(0, 160)
|
|
1678
|
+
|
|
1679
|
+
const save = async () => {
|
|
1680
|
+
setBusy(true)
|
|
1681
|
+
setError('')
|
|
1682
|
+
try {
|
|
1683
|
+
const response = await fetch('/api/project-appearance', {
|
|
1684
|
+
method: 'PUT',
|
|
1685
|
+
headers: { 'content-type': 'application/json' },
|
|
1686
|
+
body: JSON.stringify({ workspaceId: workspace.workspaceId, iconKey, colorKey }),
|
|
1687
|
+
})
|
|
1688
|
+
if (!response.ok) throw new Error(`save failed: ${response.status}`)
|
|
1689
|
+
if (typeof window !== 'undefined') window.dispatchEvent(new Event('project-appearance-changed'))
|
|
1690
|
+
onClose()
|
|
1691
|
+
} catch (cause) {
|
|
1692
|
+
setError(String(cause?.message ?? cause))
|
|
1693
|
+
setBusy(false)
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return element('div', { className: 'dbt-dialog-backdrop', role: 'presentation', onMouseDown: (event) => { if (event.target === event.currentTarget) onClose() } },
|
|
1698
|
+
element('section', { className: 'dbt-dialog', role: 'dialog', 'aria-modal': 'true', 'aria-label': t('tasks.appearanceTitle') },
|
|
1699
|
+
element('h2', null, `${t('tasks.appearanceTitle')} · ${workspace.title}`),
|
|
1700
|
+
element('input', {
|
|
1701
|
+
value: query,
|
|
1702
|
+
placeholder: t('tasks.iconSearch'),
|
|
1703
|
+
'aria-label': t('tasks.iconSearch'),
|
|
1704
|
+
onChange: (event) => setQuery(event.target.value),
|
|
1705
|
+
}),
|
|
1706
|
+
element('div', { className: 'dbt-icon-grid' }, ...icons.map((icon) => element('button', {
|
|
1707
|
+
key: icon.key,
|
|
1708
|
+
type: 'button',
|
|
1709
|
+
className: 'dbt-icon-choice',
|
|
1710
|
+
'data-selected': String(icon.key === iconKey),
|
|
1711
|
+
title: icon.label ?? icon.key,
|
|
1712
|
+
'aria-label': icon.label ?? icon.key,
|
|
1713
|
+
onClick: () => setIconKey(icon.key),
|
|
1714
|
+
}, renderCatalogIcon(icon, 21)))),
|
|
1715
|
+
element('div', { className: 'dbt-color-row', 'aria-label': 'Colors' }, ...colors.map((key) => element('button', {
|
|
1716
|
+
key,
|
|
1717
|
+
type: 'button',
|
|
1718
|
+
className: 'dbt-color-choice',
|
|
1719
|
+
'data-selected': String(key === colorKey),
|
|
1720
|
+
style: { background: COLOR_HEX[key] ?? '#64748b', color: COLOR_HEX[key] ?? '#64748b' },
|
|
1721
|
+
title: key,
|
|
1722
|
+
'aria-label': key,
|
|
1723
|
+
onClick: () => setColorKey(key),
|
|
1724
|
+
}))),
|
|
1725
|
+
error !== '' ? element('div', { className: 'dbt-task-warning', role: 'alert' }, error) : null,
|
|
1726
|
+
element('div', { className: 'dbt-dialog-actions' },
|
|
1727
|
+
element('button', { type: 'button', onClick: onClose, disabled: busy }, t('tasks.cancel')),
|
|
1728
|
+
element('button', { type: 'button', className: 'dbt-dialog-save', onClick: save, disabled: busy || icons.length === 0 }, t('tasks.save')))))
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function availabilityLabel(value, t) {
|
|
1732
|
+
if (value.availability === 'unknown') return t('tasks.unknown')
|
|
1733
|
+
if (value.availability === 'none') return t('tasks.none')
|
|
1734
|
+
return ''
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function compactRelativeTime(updatedAt, now, t, relativeTime) {
|
|
1738
|
+
const { unit, n } = relativeTime(updatedAt, now)
|
|
1739
|
+
return unit === 'now' ? t('time.now') : t(`time.${unit}`, { n })
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function resolveTaskContinueText(draft, defaultText) {
|
|
1743
|
+
return draft.trim() === '' ? defaultText : draft
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function taskContinuePlaceholder(draft, focused, defaultText) {
|
|
1747
|
+
return !focused && draft === '' ? defaultText : ''
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function TaskContinueInput({ sessionId, active, createTaskInput, onWarning, t }) {
|
|
1751
|
+
const React = requireModule('react')
|
|
1752
|
+
const [activated, setActivated] = React.useState(active)
|
|
1753
|
+
const [surface, setSurface] = React.useState(null)
|
|
1754
|
+
const [error, setError] = React.useState('')
|
|
1755
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
1756
|
+
const warning = React.useRef(onWarning)
|
|
1757
|
+
warning.current = onWarning
|
|
1758
|
+
// Hidden, never-opened cards must not open/retain every background Session.
|
|
1759
|
+
React.useEffect(() => { if (active) setActivated(true) }, [active])
|
|
1760
|
+
React.useEffect(() => {
|
|
1761
|
+
if (!activated) return undefined
|
|
1762
|
+
const controller = new AbortController()
|
|
1763
|
+
let owned
|
|
1764
|
+
setSurface(null)
|
|
1765
|
+
setError('')
|
|
1766
|
+
Promise.resolve().then(() => createTaskInput(sessionId, {
|
|
1767
|
+
signal: controller.signal,
|
|
1768
|
+
onWarning: (text) => warning.current?.(text),
|
|
1769
|
+
})).then((handle) => {
|
|
1770
|
+
owned = handle
|
|
1771
|
+
if (controller.signal.aborted) handle.dispose()
|
|
1772
|
+
else setSurface(handle)
|
|
1773
|
+
}).catch((cause) => {
|
|
1774
|
+
if (!controller.signal.aborted) setError(String(cause?.message ?? cause))
|
|
1775
|
+
})
|
|
1776
|
+
return () => { controller.abort(); owned?.dispose() }
|
|
1777
|
+
}, [activated, sessionId, createTaskInput, attempt])
|
|
1778
|
+
return element('div', { className: 'dbt-continue-host', 'data-no-card-drag': true, draggable: false,
|
|
1779
|
+
onDragStart: (event) => event.preventDefault(),
|
|
1780
|
+
onClick: (event) => {
|
|
1781
|
+
if (event.target?.closest?.('button,a,input,textarea,select,[role="dialog"],[role="listbox"],[role="option"],[data-input-popup],[contenteditable="true"]')) return
|
|
1782
|
+
if (active) surface?.input.focus()
|
|
1783
|
+
},
|
|
1784
|
+
}, surface === null ? (error === ''
|
|
1785
|
+
? activated ? element('div', { role: 'status' }, t('tasks.inputLoading')) : null
|
|
1786
|
+
: element('div', { className: 'dbt-task-warning', role: 'alert' }, error,
|
|
1787
|
+
element('button', { type: 'button', onClick: () => setAttempt(value => value + 1) }, t('tasks.retryInput'))))
|
|
1788
|
+
: element(surface.View, {
|
|
1789
|
+
className: 'dbt-continue-form', disabled: !active,
|
|
1790
|
+
placeholder: t('tasks.continueDefault'), defaultText: t('tasks.continueDefault'), submitLabel: t('tasks.send'),
|
|
1791
|
+
inputLabel: t('tasks.continue'), addLabel: t('tasks.add'), searchLabel: t('tasks.inputSearch'),
|
|
1792
|
+
emptyLabel: t('tasks.inputEmpty'), errorLabel: t('tasks.inputCatalogError'),
|
|
1793
|
+
}))
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function TaskQuestionCard({ target, createTaskQuestion, fontSize, revealQuestion, canRevealQuestion, t }) {
|
|
1797
|
+
const React = requireModule('react')
|
|
1798
|
+
const [handle, setHandle] = React.useState(null)
|
|
1799
|
+
const [error, setError] = React.useState('')
|
|
1800
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
1801
|
+
React.useEffect(() => {
|
|
1802
|
+
const controller = new AbortController()
|
|
1803
|
+
let owned
|
|
1804
|
+
setHandle(null); setError('')
|
|
1805
|
+
Promise.resolve().then(() => createTaskQuestion({
|
|
1806
|
+
sessionId: target.sessionId, requestKey: target.requestKey, callId: target.callId, signal: controller.signal,
|
|
1807
|
+
})).then(value => {
|
|
1808
|
+
owned = value
|
|
1809
|
+
if (controller.signal.aborted) value.dispose()
|
|
1810
|
+
else setHandle(value)
|
|
1811
|
+
}).catch(cause => { if (!controller.signal.aborted) setError(String(cause?.message ?? cause)) })
|
|
1812
|
+
return () => { controller.abort(); owned?.dispose() }
|
|
1813
|
+
}, [target.sessionId, target.requestKey, target.callId, createTaskQuestion, attempt])
|
|
1814
|
+
const fallback = () => typeof target.callId === 'string' && canRevealQuestion()
|
|
1815
|
+
? element('button', { type: 'button', className: 'dbt-task-answer', onClick: event => {
|
|
1816
|
+
event.stopPropagation()
|
|
1817
|
+
if (revealQuestion(target.sessionId, target.callId) === false) setError(t('tasks.answerFailed'))
|
|
1818
|
+
} }, t('tasks.answer')) : null
|
|
1819
|
+
if (handle !== null) return element(TaskQuestionView, { handle, fontSize, fallback, t, retry: () => setAttempt(value => value + 1) })
|
|
1820
|
+
if (error === '') return element('div', { role: 'status', className: 'dbt-question-notice' }, t('tasks.questionLoading'))
|
|
1821
|
+
return element('div', { role: 'alert', className: 'dbt-question-notice' }, error,
|
|
1822
|
+
element('button', { type: 'button', className: 'dbt-task-answer', onClick: () => setAttempt(value => value + 1) }, t('tasks.retryInput')), fallback())
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function TaskQuestionView({ handle, fontSize, fallback, t, retry }) {
|
|
1826
|
+
const React = requireModule('react')
|
|
1827
|
+
const state = React.useSyncExternalStore(listener => handle.state.subscribe(listener), () => handle.state.getSnapshot())
|
|
1828
|
+
if (state.phase === 'closed') return null
|
|
1829
|
+
if (state.phase === 'unsupported') return fallback()
|
|
1830
|
+
if (state.phase === 'error') return element('div', { className: 'dbt-question-notice', role: 'alert' }, state.message ?? t('tasks.answerFailed'),
|
|
1831
|
+
element('button', { type: 'button', className: 'dbt-task-answer', onClick: retry }, t('tasks.retryInput')), fallback())
|
|
1832
|
+
if (state.phase === 'waiting') return element('div', { className: 'dbt-question-notice', role: 'status' }, t('tasks.questionLoading'), fallback())
|
|
1833
|
+
return element(handle.View, { fontSize })
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function TaskActionDialog({ action, onClose, onSubmit, t }) {
|
|
1837
|
+
const React = requireModule('react')
|
|
1838
|
+
const { Modal, Button } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1839
|
+
const [draft, setDraft] = React.useState(action.title)
|
|
1840
|
+
const [busy, setBusy] = React.useState(false)
|
|
1841
|
+
const [error, setError] = React.useState('')
|
|
1842
|
+
const composing = React.useRef(false)
|
|
1843
|
+
const selectRenameInput = React.useCallback((node) => {
|
|
1844
|
+
if (node === null) return
|
|
1845
|
+
node.focus()
|
|
1846
|
+
node.select()
|
|
1847
|
+
}, [])
|
|
1848
|
+
const close = () => { if (!busy) onClose() }
|
|
1849
|
+
const submit = async () => {
|
|
1850
|
+
if (busy || draft.trim() === '') return
|
|
1851
|
+
setBusy(true)
|
|
1852
|
+
setError('')
|
|
1853
|
+
try { await onSubmit(action, draft); onClose() }
|
|
1854
|
+
catch (cause) { setError(String(cause?.message ?? cause)); setBusy(false) }
|
|
1855
|
+
}
|
|
1856
|
+
const inputProps = {
|
|
1857
|
+
className: 'dbt-action-input dbt-rename-input',
|
|
1858
|
+
value: draft,
|
|
1859
|
+
'aria-label': t('tasks.rename'),
|
|
1860
|
+
ref: selectRenameInput,
|
|
1861
|
+
disabled: busy,
|
|
1862
|
+
onChange: (event) => { setDraft(event.target.value); setError('') },
|
|
1863
|
+
onCompositionStart: () => { composing.current = true },
|
|
1864
|
+
onCompositionEnd: () => { composing.current = false },
|
|
1865
|
+
onKeyDown: (event) => {
|
|
1866
|
+
if (event.key === 'Enter' && !event.shiftKey && !composing.current && !event.nativeEvent?.isComposing) {
|
|
1867
|
+
event.preventDefault()
|
|
1868
|
+
submit()
|
|
1869
|
+
}
|
|
1870
|
+
},
|
|
1871
|
+
}
|
|
1872
|
+
return element(Modal, {
|
|
1873
|
+
open: true,
|
|
1874
|
+
onClose: close,
|
|
1875
|
+
closeLabel: t('tasks.cancel'),
|
|
1876
|
+
title: t('tasks.rename'),
|
|
1877
|
+
footer: element(React.Fragment, null,
|
|
1878
|
+
element(Button, { variant: 'outline', disabled: busy, onClick: close }, t('tasks.cancel')),
|
|
1879
|
+
element(Button, { variant: 'primary', disabled: busy || draft.trim() === '', onClick: submit }, t('tasks.renameAction'))),
|
|
1880
|
+
},
|
|
1881
|
+
element('input', inputProps),
|
|
1882
|
+
error === '' ? null : element('div', { className: 'dbt-task-warning', role: 'alert' }, error))
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function shouldToggleFinalInput(target) {
|
|
1886
|
+
return !target?.closest?.('a')
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function isTaskCardFinalShortcutTarget(target) {
|
|
1890
|
+
return !target?.closest?.('[data-no-card-drag],button,a,input,textarea,select,[contenteditable="true"],[role="button"],[role="switch"]')
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
/** A card gesture reads only the existing bounded, pinned-only Final endpoint. */
|
|
1894
|
+
function createCardFinalExpansionController({ getTask, getCached, isExpanded, readFinal, onReady, expand, collapse }) {
|
|
1895
|
+
const pending = new Map()
|
|
1896
|
+
let disposed = false
|
|
1897
|
+
const eligible = (task) => task?.session.status === 'idle'
|
|
1898
|
+
const validPayload = (id, value) => value?.sessionId === id &&
|
|
1899
|
+
(value.final === null || typeof value.final?.text === 'string')
|
|
1900
|
+
const hasFinal = (value) => typeof value.final?.text === 'string' && value.final.text.trim() !== ''
|
|
1901
|
+
const cancel = (id) => {
|
|
1902
|
+
const request = pending.get(id)
|
|
1903
|
+
if (request === undefined) return
|
|
1904
|
+
pending.delete(id)
|
|
1905
|
+
request.controller.abort()
|
|
1906
|
+
}
|
|
1907
|
+
const current = (request) => {
|
|
1908
|
+
const task = getTask(request.id)
|
|
1909
|
+
return !disposed && !request.controller.signal.aborted && pending.get(request.id) === request &&
|
|
1910
|
+
eligible(task) && task.updatedAt === request.updatedAt
|
|
1911
|
+
}
|
|
1912
|
+
return {
|
|
1913
|
+
request(id) {
|
|
1914
|
+
const task = getTask(id)
|
|
1915
|
+
if (disposed || !eligible(task)) return Promise.resolve(false)
|
|
1916
|
+
if (isExpanded(id)) {
|
|
1917
|
+
// Closing is immediate, even while a preview is loading or failed.
|
|
1918
|
+
// Invalidate an older read so its late response cannot reopen Final.
|
|
1919
|
+
cancel(id)
|
|
1920
|
+
collapse(id)
|
|
1921
|
+
return Promise.resolve(true)
|
|
1922
|
+
}
|
|
1923
|
+
const prior = pending.get(id)
|
|
1924
|
+
if (prior?.updatedAt === task.updatedAt) return prior.promise
|
|
1925
|
+
cancel(id)
|
|
1926
|
+
const cached = getCached(id)
|
|
1927
|
+
if (cached?.status === 'ready' && cached.updatedAt === task.updatedAt && validPayload(id, cached.value)) {
|
|
1928
|
+
if (!hasFinal(cached.value)) return Promise.resolve(false)
|
|
1929
|
+
expand(id)
|
|
1930
|
+
return Promise.resolve(true)
|
|
1931
|
+
}
|
|
1932
|
+
const request = { id, updatedAt: task.updatedAt, controller: new AbortController() }
|
|
1933
|
+
pending.set(id, request)
|
|
1934
|
+
request.promise = Promise.resolve().then(() => {
|
|
1935
|
+
if (!current(request)) return undefined
|
|
1936
|
+
return readFinal(request, request.controller.signal)
|
|
1937
|
+
}).then((value) => {
|
|
1938
|
+
if (!current(request) || !validPayload(id, value)) return false
|
|
1939
|
+
onReady(request, value)
|
|
1940
|
+
if (!hasFinal(value)) return false
|
|
1941
|
+
// This asynchronous path only opens; it never toggles a newer choice.
|
|
1942
|
+
expand(id)
|
|
1943
|
+
return true
|
|
1944
|
+
}).catch(() => false).finally(() => {
|
|
1945
|
+
if (pending.get(id) === request) pending.delete(id)
|
|
1946
|
+
})
|
|
1947
|
+
return request.promise
|
|
1948
|
+
},
|
|
1949
|
+
cancel,
|
|
1950
|
+
reconcile() {
|
|
1951
|
+
for (const [id, request] of pending) if (!current(request)) cancel(id)
|
|
1952
|
+
},
|
|
1953
|
+
dispose() {
|
|
1954
|
+
disposed = true
|
|
1955
|
+
for (const id of pending.keys()) cancel(id)
|
|
1956
|
+
},
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const FINAL_DOUBLE_CLICK_WAIT_MS = 300
|
|
1961
|
+
|
|
1962
|
+
function createFinalClickController(schedule = (callback, ms) => window.setTimeout(callback, ms), cancelTimer = (timer) => window.clearTimeout(timer), feedback = () => {}) {
|
|
1963
|
+
const pending = new Map()
|
|
1964
|
+
const cancel = (id) => {
|
|
1965
|
+
const timer = pending.get(id)
|
|
1966
|
+
if (timer === undefined) return
|
|
1967
|
+
cancelTimer(timer)
|
|
1968
|
+
pending.delete(id)
|
|
1969
|
+
feedback(id, false)
|
|
1970
|
+
}
|
|
1971
|
+
return {
|
|
1972
|
+
click(id, action) {
|
|
1973
|
+
const prior = pending.get(id)
|
|
1974
|
+
if (prior !== undefined) cancelTimer(prior)
|
|
1975
|
+
else feedback(id, true)
|
|
1976
|
+
const timer = schedule(() => {
|
|
1977
|
+
pending.delete(id)
|
|
1978
|
+
feedback(id, false)
|
|
1979
|
+
action()
|
|
1980
|
+
}, FINAL_DOUBLE_CLICK_WAIT_MS)
|
|
1981
|
+
pending.set(id, timer)
|
|
1982
|
+
},
|
|
1983
|
+
doubleClick(id, action) {
|
|
1984
|
+
if (!pending.has(id)) return false
|
|
1985
|
+
cancel(id)
|
|
1986
|
+
action()
|
|
1987
|
+
return true
|
|
1988
|
+
},
|
|
1989
|
+
cancel,
|
|
1990
|
+
dispose() {
|
|
1991
|
+
for (const timer of pending.values()) cancelTimer(timer)
|
|
1992
|
+
pending.clear()
|
|
1993
|
+
},
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function TaskSwimlane({ useSessions, useWorkspaces, useSessionStatus, useTaskConnection, usePanelInfo, useTaskPins, preferences, openSession, moveTaskBefore, unpinTask, revealQuestion, canRevealQuestion, findTaskQuestion, createTaskQuestion, twoColumns, t, submitTaskAction, createTaskInput }) {
|
|
1998
|
+
const React = requireModule('react')
|
|
1999
|
+
const {
|
|
2000
|
+
MarkdownText,
|
|
2001
|
+
StateDot,
|
|
2002
|
+
relativeTime,
|
|
2003
|
+
} = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
2004
|
+
const list = useSessions((snapshot) => snapshot)
|
|
2005
|
+
const panelActive = usePanelInfo((info) => info.activePanelId !== null)
|
|
2006
|
+
const workspaces = useWorkspaces((snapshot) => snapshot)
|
|
2007
|
+
// 0.2 carries the pending interaction on the Session status snapshot —
|
|
2008
|
+
// `Map<sessionId, { running, pendingInteraction, completionUnread }>`, reached
|
|
2009
|
+
// through the global standard prop every slot entry receives. The 0.1.5
|
|
2010
|
+
// pending-interaction hook was removed in 0.2, and reading it there yielded
|
|
2011
|
+
// `undefined` at render time: the TypeError retired this slot entry as an
|
|
2012
|
+
// abdicating crash and the whole sidebar column vanished.
|
|
2013
|
+
const sessionStatuses = useSessionStatus((snapshot) => snapshot)
|
|
2014
|
+
const connectionState = useTaskConnection((snapshot) => snapshot)
|
|
2015
|
+
const taskPins = useTaskPins((snapshot) => snapshot)
|
|
2016
|
+
const pendingInteractions = React.useMemo(() => {
|
|
2017
|
+
const next = new Map()
|
|
2018
|
+
// Metadata discovery is read-only; opening history is owned by an actual card.
|
|
2019
|
+
for (const id of taskPins.queue.sessionIds) {
|
|
2020
|
+
const target = findTaskQuestion?.(id)
|
|
2021
|
+
if (target !== undefined) next.set(id, { ...target, key: target.requestKey })
|
|
2022
|
+
}
|
|
2023
|
+
for (const [id, status] of sessionStatuses) {
|
|
2024
|
+
const pending = status?.pendingInteraction
|
|
2025
|
+
if (pending != null) next.set(id, pending)
|
|
2026
|
+
}
|
|
2027
|
+
return next
|
|
2028
|
+
}, [sessionStatuses, list, taskPins.queue.sessionIds, findTaskQuestion])
|
|
2029
|
+
const appearanceState = useProjectAppearanceSnapshot()
|
|
2030
|
+
const [editingWorkspaceId, setEditingWorkspaceId] = React.useState(null)
|
|
2031
|
+
const [taskAction, setTaskAction] = React.useState(null)
|
|
2032
|
+
const [storedExpanded, setExpanded] = useStoredExpansion('dsh-better-tasks.projection-expansion.v2', 'dsh-better-tasks.projection-expansion.v1')
|
|
2033
|
+
const [finalAutoCycles, setFinalAutoCycles] = useStoredRecord('dsh-better-tasks.final-auto-open-cycle.v1')
|
|
2034
|
+
const [goalAutoCycles, setGoalAutoCycles] = useStoredRecord('dsh-better-tasks.goal-auto-open-cycle.v1')
|
|
2035
|
+
const [todoAutoCycles, setTodoAutoCycles] = useStoredRecord('dsh-better-tasks.todo-expansion-cycle.v1')
|
|
2036
|
+
const [drag, setDrag] = React.useState(null)
|
|
2037
|
+
const dragClickGuard = React.useRef(null)
|
|
2038
|
+
const dragOriginGuard = React.useRef(null)
|
|
2039
|
+
const [pendingFinalIds, setPendingFinalIds] = React.useState(() => new Set())
|
|
2040
|
+
const finalClicks = React.useRef(null)
|
|
2041
|
+
if (finalClicks.current === null) finalClicks.current = createFinalClickController(undefined, undefined, (id, active) => {
|
|
2042
|
+
setPendingFinalIds((current) => {
|
|
2043
|
+
if (current.has(id) === active) return current
|
|
2044
|
+
const next = new Set(current)
|
|
2045
|
+
if (active) next.add(id)
|
|
2046
|
+
else next.delete(id)
|
|
2047
|
+
return next
|
|
2048
|
+
})
|
|
2049
|
+
})
|
|
2050
|
+
React.useEffect(() => () => finalClicks.current.dispose(), [])
|
|
2051
|
+
const [moveError, setMoveError] = React.useState('')
|
|
2052
|
+
const [actionWarning, setActionWarning] = React.useState('')
|
|
2053
|
+
|
|
2054
|
+
const tasks = derivePinnedTasks({
|
|
2055
|
+
list,
|
|
2056
|
+
panelActive,
|
|
2057
|
+
pinnedSessionIds: taskPins.queue.sessionIds,
|
|
2058
|
+
pendingInteractions,
|
|
2059
|
+
sessionStatuses,
|
|
2060
|
+
})
|
|
2061
|
+
const taskIds = tasks.map((task) => task.id)
|
|
2062
|
+
const goalTasks = tasks.filter((task) => task.goal.availability === 'ready' && typeof task.goal.id === 'string').map((task) => ({ id: task.id, goalId: task.goal.id }))
|
|
2063
|
+
const endedGoalTaskIds = tasks.filter((task) => task.goal.availability === 'none').map((task) => task.id)
|
|
2064
|
+
const goalCycleKey = `${goalTasks.map((task) => `${task.id}:${task.goalId}`).join('|')}::${endedGoalTaskIds.join('|')}`
|
|
2065
|
+
React.useEffect(() => {
|
|
2066
|
+
setGoalAutoCycles((cycles) => reconcileGoalExpansionCycles(cycles, goalTasks, endedGoalTaskIds, {}).cycles)
|
|
2067
|
+
setExpanded((value) => reconcileGoalExpansionCycles(goalAutoCycles, goalTasks, endedGoalTaskIds, value).expanded)
|
|
2068
|
+
}, [goalCycleKey])
|
|
2069
|
+
const todoCycleKey = JSON.stringify(tasks.map((task) => [task.id, todoExpansionCycle(task)]))
|
|
2070
|
+
React.useEffect(() => {
|
|
2071
|
+
setTodoAutoCycles((cycles) => reconcileTodoExpansionCycles(cycles, tasks, {}).cycles)
|
|
2072
|
+
// Functional updates preserve Goal/Final changes from this same commit.
|
|
2073
|
+
setExpanded((value) => reconcileTodoExpansionCycles(todoAutoCycles, tasks, value).expanded)
|
|
2074
|
+
}, [todoCycleKey])
|
|
2075
|
+
const idleTasks = tasks.filter((task) => task.session.status === 'idle').map((task) => ({ id: task.id, updatedAt: task.updatedAt, current: task.current }))
|
|
2076
|
+
const idleCycleKey = JSON.stringify(idleTasks)
|
|
2077
|
+
// Resolve BEFORE effects: display, lazy reads and manual gesture checks must
|
|
2078
|
+
// agree on current-cycle suppression, without a flash or an eager Final read.
|
|
2079
|
+
const expanded = expandFinalForIdleCycles(finalAutoCycles, idleTasks, storedExpanded).expanded
|
|
2080
|
+
React.useEffect(() => {
|
|
2081
|
+
const unseen = idleTasks.filter((task) => isNewFinalIdleCycle(finalAutoCycles, task))
|
|
2082
|
+
if (unseen.length === 0) return
|
|
2083
|
+
setFinalAutoCycles((cycles) => expandFinalForIdleCycles(cycles, unseen, {}).cycles)
|
|
2084
|
+
setExpanded((value) => expandFinalForIdleCycles(finalAutoCycles, unseen, value).expanded)
|
|
2085
|
+
}, [idleCycleKey])
|
|
2086
|
+
const [finalStates, setFinalStates] = React.useState(loadFinalMessageCache)
|
|
2087
|
+
React.useEffect(() => {
|
|
2088
|
+
writeStorage(FINAL_CACHE_KEY, serializeFinalMessageCache(finalStates))
|
|
2089
|
+
}, [finalStates])
|
|
2090
|
+
const finalRequests = tasks.filter((task) => {
|
|
2091
|
+
if (task.session.status !== 'idle' || !isProjectionExpanded(expanded, `${task.id}:final`, preferences.defaultFinalExpanded)) return false
|
|
2092
|
+
const cached = finalStates[task.id]
|
|
2093
|
+
return cached?.status !== 'ready' || cached.updatedAt !== task.updatedAt
|
|
2094
|
+
}).map((task) => ({ id: task.id, updatedAt: task.updatedAt }))
|
|
2095
|
+
const finalRequestKey = finalRequests.map((request) => `${request.id}:${request.updatedAt}`).join('|')
|
|
2096
|
+
const [finalRetry, setFinalRetry] = React.useState(0)
|
|
2097
|
+
React.useEffect(() => {
|
|
2098
|
+
if (finalRequests.length === 0) return undefined
|
|
2099
|
+
let disposed = false
|
|
2100
|
+
const controllers = finalRequests.map((request) => ({ request, controller: new AbortController() }))
|
|
2101
|
+
setFinalStates((current) => {
|
|
2102
|
+
const next = { ...current }
|
|
2103
|
+
for (const { request } of controllers) next[request.id] = { status: 'loading', updatedAt: request.updatedAt }
|
|
2104
|
+
return next
|
|
2105
|
+
})
|
|
2106
|
+
for (const { request, controller } of controllers) {
|
|
2107
|
+
fetch(`/api/better-tasks/final-message?sessionId=${encodeURIComponent(request.id)}`, { cache: 'no-store', signal: controller.signal })
|
|
2108
|
+
.then(async (response) => {
|
|
2109
|
+
const body = await response.json()
|
|
2110
|
+
if (!response.ok) throw new Error(body?.message ?? body?.code ?? `HTTP ${response.status}`)
|
|
2111
|
+
return body
|
|
2112
|
+
})
|
|
2113
|
+
.then((value) => {
|
|
2114
|
+
if (disposed) return
|
|
2115
|
+
setFinalStates((current) => current[request.id]?.updatedAt === request.updatedAt ? { ...current, [request.id]: { status: 'ready', updatedAt: request.updatedAt, value } } : current)
|
|
2116
|
+
})
|
|
2117
|
+
.catch((error) => {
|
|
2118
|
+
if (disposed || error?.name === 'AbortError') return
|
|
2119
|
+
setFinalStates((current) => current[request.id]?.updatedAt === request.updatedAt ? { ...current, [request.id]: { status: 'error', updatedAt: request.updatedAt, message: String(error?.message ?? error) } } : current)
|
|
2120
|
+
})
|
|
2121
|
+
}
|
|
2122
|
+
return () => {
|
|
2123
|
+
disposed = true
|
|
2124
|
+
for (const { controller } of controllers) controller.abort()
|
|
2125
|
+
}
|
|
2126
|
+
}, [finalRequestKey, finalRetry])
|
|
2127
|
+
const finalShortcutState = React.useRef(null)
|
|
2128
|
+
finalShortcutState.current = { tasks: new Map(tasks.map((task) => [task.id, task])), finalStates, expanded, preferences, finalAutoCycles }
|
|
2129
|
+
const cardFinalRequests = React.useRef(null)
|
|
2130
|
+
React.useEffect(() => {
|
|
2131
|
+
const changeFinalDisclosure = (id, visible) => {
|
|
2132
|
+
const snapshot = finalShortcutState.current
|
|
2133
|
+
const task = snapshot.tasks.get(id)
|
|
2134
|
+
if (task?.session.status !== 'idle') return
|
|
2135
|
+
finalClicks.current.cancel(id)
|
|
2136
|
+
const idleTask = { id, updatedAt: task.updatedAt, current: task.current }
|
|
2137
|
+
// Commit the same lifecycle and effective state the gesture observed;
|
|
2138
|
+
// otherwise a not-yet-persisted background cycle could erase manual close.
|
|
2139
|
+
setFinalAutoCycles((cycles) => expandFinalForIdleCycles(cycles, [idleTask], {}).cycles)
|
|
2140
|
+
setExpanded((current) => {
|
|
2141
|
+
const resolved = expandFinalForIdleCycles(snapshot.finalAutoCycles, [idleTask], current).expanded
|
|
2142
|
+
return resolved[`${id}:final`] === visible ? resolved : { ...resolved, [`${id}:final`]: visible }
|
|
2143
|
+
})
|
|
2144
|
+
}
|
|
2145
|
+
const operation = createCardFinalExpansionController({
|
|
2146
|
+
getTask: (id) => finalShortcutState.current.tasks.get(id),
|
|
2147
|
+
getCached: (id) => finalShortcutState.current.finalStates[id],
|
|
2148
|
+
isExpanded: (id) => isProjectionExpanded(finalShortcutState.current.expanded, `${id}:final`, finalShortcutState.current.preferences.defaultFinalExpanded),
|
|
2149
|
+
readFinal: async (request, signal) => {
|
|
2150
|
+
const response = await fetch(`/api/better-tasks/final-message?sessionId=${encodeURIComponent(request.id)}`, { cache: 'no-store', signal })
|
|
2151
|
+
const value = await response.json()
|
|
2152
|
+
if (!response.ok) throw new Error(value?.message ?? value?.code ?? `HTTP ${response.status}`)
|
|
2153
|
+
return value
|
|
2154
|
+
},
|
|
2155
|
+
onReady: (request, value) => setFinalStates((current) => {
|
|
2156
|
+
const task = finalShortcutState.current.tasks.get(request.id)
|
|
2157
|
+
if (task?.session.status !== 'idle' || task.updatedAt !== request.updatedAt) return current
|
|
2158
|
+
return { ...current, [request.id]: { status: 'ready', updatedAt: request.updatedAt, value } }
|
|
2159
|
+
}),
|
|
2160
|
+
expand: (id) => changeFinalDisclosure(id, true),
|
|
2161
|
+
collapse: (id) => changeFinalDisclosure(id, false),
|
|
2162
|
+
})
|
|
2163
|
+
cardFinalRequests.current = operation
|
|
2164
|
+
return () => {
|
|
2165
|
+
operation.dispose()
|
|
2166
|
+
if (cardFinalRequests.current === operation) cardFinalRequests.current = null
|
|
2167
|
+
}
|
|
2168
|
+
}, [])
|
|
2169
|
+
const finalShortcutKey = JSON.stringify(tasks.map((task) => [task.id, task.session.status, task.updatedAt]))
|
|
2170
|
+
React.useEffect(() => { cardFinalRequests.current?.reconcile() }, [finalShortcutKey])
|
|
2171
|
+
const workspaceBySession = indexWorkspacesBySession(workspaces.items)
|
|
2172
|
+
const appearanceByWorkspace = new Map(appearanceState.items.map((item) => [item.workspaceId, item]))
|
|
2173
|
+
const iconByKey = new Map(appearanceState.icons.map((icon) => [icon.key, icon]))
|
|
2174
|
+
const editingWorkspace = workspaces.items.find((workspace) => workspace.workspaceId === editingWorkspaceId)
|
|
2175
|
+
const pendingMoves = new Set(taskPins.pendingSessionIds)
|
|
2176
|
+
const now = Date.now()
|
|
2177
|
+
const toggleProjection = (sessionId, key, defaultExpanded) => {
|
|
2178
|
+
if (key === 'final') cardFinalRequests.current?.cancel(sessionId)
|
|
2179
|
+
const disclosureKey = `${sessionId}:${key}`
|
|
2180
|
+
setExpanded((current) => {
|
|
2181
|
+
const resolved = key === 'final' || key === 'input'
|
|
2182
|
+
? expandFinalForIdleCycles(finalAutoCycles, idleTasks, current).expanded
|
|
2183
|
+
: current
|
|
2184
|
+
return { ...resolved, [disclosureKey]: !isProjectionExpanded(resolved, disclosureKey, defaultExpanded) }
|
|
2185
|
+
})
|
|
2186
|
+
}
|
|
2187
|
+
const commitDrop = (overId, half) => {
|
|
2188
|
+
if (drag === null || taskPins.writable !== true) return
|
|
2189
|
+
const sessionId = drag.sessionId
|
|
2190
|
+
const beforeSessionId = beforeIdForTaskDrop(taskIds, overId, half)
|
|
2191
|
+
setDrag(null)
|
|
2192
|
+
if (beforeSessionId === sessionId) return
|
|
2193
|
+
setMoveError('')
|
|
2194
|
+
moveTaskBefore(sessionId, beforeSessionId).catch((error) => setMoveError(String(error?.message ?? error)))
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
return element('section', {
|
|
2198
|
+
className: 'dbt-task-view',
|
|
2199
|
+
'aria-label': t('view.tasks'),
|
|
2200
|
+
style: { '--dbt-projection-font-size': `${preferences.detailFontSize}px` },
|
|
2201
|
+
},
|
|
2202
|
+
appearanceState.status === 'unavailable' ? element('div', { className: 'dbt-task-warning', role: 'status' }, t('tasks.appearanceUnavailable')) : null,
|
|
2203
|
+
taskPins.migration === 'initializing' ? element('div', { className: 'dbt-task-warning', role: 'status' }, t('tasks.orderRestoring')) : null,
|
|
2204
|
+
taskPins.migration === 'running' ? element('div', { className: 'dbt-task-warning', role: 'status' }, t('tasks.pinMigration')) : null,
|
|
2205
|
+
taskPins.error !== null || moveError !== '' ? element('div', { className: 'dbt-task-warning', role: 'status' }, moveError || taskPins.error) : null,
|
|
2206
|
+
actionWarning === '' ? null : element('div', { className: 'dbt-task-warning', role: 'alert' }, actionWarning),
|
|
2207
|
+
tasks.length === 0 ? element('div', { className: 'dbt-task-empty' }, t('tasks.empty')) :
|
|
2208
|
+
element('div', {
|
|
2209
|
+
className: 'dbt-task-list',
|
|
2210
|
+
'data-columns': twoColumns ? '2' : '1',
|
|
2211
|
+
'data-title-emphasis': preferences.titleEmphasis,
|
|
2212
|
+
onDragOver: (event) => {
|
|
2213
|
+
if (drag === null || event.target !== event.currentTarget) return
|
|
2214
|
+
event.preventDefault()
|
|
2215
|
+
const lastId = taskIds.at(-1)
|
|
2216
|
+
if (lastId !== undefined && (drag.overId !== lastId || drag.half !== 'after')) setDrag({ ...drag, overId: lastId, half: 'after' })
|
|
2217
|
+
},
|
|
2218
|
+
onDrop: (event) => {
|
|
2219
|
+
if (drag === null || event.target !== event.currentTarget) return
|
|
2220
|
+
event.preventDefault()
|
|
2221
|
+
const lastId = taskIds.at(-1)
|
|
2222
|
+
if (lastId !== undefined) commitDrop(lastId, 'after')
|
|
2223
|
+
},
|
|
2224
|
+
}, ...((cards) => twoColumns ? [
|
|
2225
|
+
element('div', { key: 'column-even', className: 'dbt-task-column', 'data-column': 'even' }, ...cards.filter((_, index) => index % 2 === 0)),
|
|
2226
|
+
element('div', { key: 'column-odd', className: 'dbt-task-column', 'data-column': 'odd' }, ...cards.filter((_, index) => index % 2 === 1)),
|
|
2227
|
+
] : cards)(tasks.map((task) => {
|
|
2228
|
+
const workspace = workspaceBySession.get(task.id)
|
|
2229
|
+
const projectName = workspace?.title ?? task.cwd ?? t('tasks.unknown')
|
|
2230
|
+
const appearance = workspace === undefined ? undefined : appearanceByWorkspace.get(workspace.workspaceId)
|
|
2231
|
+
const icon = appearance === undefined ? undefined : iconByKey.get(appearance.iconKey)
|
|
2232
|
+
const iconColor = COLOR_HEX[appearance?.colorKey] ?? 'var(--dsw-alias-label-secondary)'
|
|
2233
|
+
const goalAvailable = task.goal.availability === 'ready'
|
|
2234
|
+
const todoAvailable = task.todos.availability === 'ready'
|
|
2235
|
+
const todoVisible = !todoAvailable || task.todos.total > 0
|
|
2236
|
+
const goalExpanded = goalAvailable && isProjectionExpanded(expanded, `${task.id}:goal`, preferences.defaultGoalExpanded)
|
|
2237
|
+
const todoExpanded = todoAvailable && task.todos.total > 0 && isProjectionExpanded(expanded, `${task.id}:todo`, preferences.defaultTodoExpanded)
|
|
2238
|
+
const finalExpanded = task.session.status === 'idle' && isProjectionExpanded(expanded, `${task.id}:final`, preferences.defaultFinalExpanded)
|
|
2239
|
+
const inputExpanded = finalExpanded && isProjectionExpanded(expanded, `${task.id}:input`, preferences.defaultInputExpanded)
|
|
2240
|
+
const finalState = finalStates[task.id]
|
|
2241
|
+
const pendingInteraction = pendingInteractions.get(task.id)
|
|
2242
|
+
const questionTarget = pendingInteraction?.kind === 'question' || pendingInteraction?.kind === 'plan-review'
|
|
2243
|
+
? { sessionId: task.id, kind: pendingInteraction.kind,
|
|
2244
|
+
requestKey: pendingInteraction.key ?? pendingInteraction.requestKey, callId: pendingInteraction.callId }
|
|
2245
|
+
: undefined
|
|
2246
|
+
const answerCallId = questionTarget?.callId
|
|
2247
|
+
const inlineQuestion = questionTarget?.kind === 'question' &&
|
|
2248
|
+
(typeof questionTarget.requestKey === 'string' || typeof answerCallId === 'string')
|
|
2249
|
+
const answerable = inlineQuestion || typeof answerCallId === 'string' && canRevealQuestion()
|
|
2250
|
+
const goalLabel = task.goal.availability === 'ready' ? `${task.goal.phase}: ${task.goal.objective}` : availabilityLabel(task.goal, t)
|
|
2251
|
+
const todoLabel = task.todos.availability === 'ready' ? `${task.todos.completed}/${task.todos.total}` : availabilityLabel(task.todos, t)
|
|
2252
|
+
const executionWarning = taskExecutionWarning({
|
|
2253
|
+
...list.byId[task.id],
|
|
2254
|
+
running: task.session.status === 'running',
|
|
2255
|
+
}, pendingInteraction, connectionState, list.phase === undefined || list.phase === 'ready')
|
|
2256
|
+
const warningLabel = executionWarning === null ? '' : t(`tasks.execution.${executionWarning.kind}`)
|
|
2257
|
+
const stateLabel = executionWarning === null
|
|
2258
|
+
? (task.session.status === 'block' ? t('tasks.blockAria') : task.session.status === 'running' ? t('tasks.runningAria') : t('tasks.idleAria'))
|
|
2259
|
+
: [warningLabel, executionWarning.detail].filter(Boolean).join('\n')
|
|
2260
|
+
const dropHalf = drag?.overId === task.id ? drag.half : undefined
|
|
2261
|
+
const detailRows = []
|
|
2262
|
+
if (goalExpanded) detailRows.push(element('div', {
|
|
2263
|
+
key: 'goal',
|
|
2264
|
+
className: 'dbt-projection-detail',
|
|
2265
|
+
'data-tone': task.goal.phase ?? task.goal.availability,
|
|
2266
|
+
title: goalLabel,
|
|
2267
|
+
'aria-label': `${t('tasks.goal')}: ${goalLabel}`,
|
|
2268
|
+
},
|
|
2269
|
+
element(IconGoalOutlineRegular, { size: 14 }),
|
|
2270
|
+
element('div', { className: 'dbt-projection-copy' }, task.goal.availability === 'ready' ?
|
|
2271
|
+
element(React.Fragment, null,
|
|
2272
|
+
element('div', null, task.goal.objective),
|
|
2273
|
+
task.goal.blockedReason === undefined ? null : element('div', { title: task.goal.blockedReason.code }, task.goal.blockedReason.message)) :
|
|
2274
|
+
availabilityLabel(task.goal, t))))
|
|
2275
|
+
if (todoExpanded) detailRows.push(element('div', {
|
|
2276
|
+
key: 'todo',
|
|
2277
|
+
className: 'dbt-projection-detail',
|
|
2278
|
+
title: todoLabel,
|
|
2279
|
+
'aria-label': `${t('tasks.todo')}: ${todoLabel}`,
|
|
2280
|
+
},
|
|
2281
|
+
element(IconChecklistOutlineRegular, { size: 14 }),
|
|
2282
|
+
element('div', { className: 'dbt-projection-copy' }, task.todos.availability === 'ready' ?
|
|
2283
|
+
element('ul', { className: 'dbt-todo-list' }, ...task.todos.items.map((todo, index) => element('li', {
|
|
2284
|
+
key: `${index}:${todo.content}`,
|
|
2285
|
+
className: 'dbt-todo-item',
|
|
2286
|
+
title: todo.status,
|
|
2287
|
+
},
|
|
2288
|
+
element('span', { className: 'dbt-todo-glyph', role: 'img', 'aria-label': t(todo.status === 'completed' ? 'tasks.todoCompleted' : todo.status === 'in_progress' ? 'tasks.todoInProgress' : 'tasks.todoPending') },
|
|
2289
|
+
element(StateDot, { state: todo.status === 'completed' ? 'done' : todo.status === 'in_progress' ? 'ongoing' : 'idle' })),
|
|
2290
|
+
element('span', null, todo.content)))) : availabilityLabel(task.todos, t))))
|
|
2291
|
+
if (finalExpanded) {
|
|
2292
|
+
let finalContent
|
|
2293
|
+
if (finalState === undefined || finalState.status === 'loading') {
|
|
2294
|
+
finalContent = t('tasks.finalLoading')
|
|
2295
|
+
} else if (finalState.status === 'error') {
|
|
2296
|
+
finalContent = t('tasks.finalError')
|
|
2297
|
+
} else if (finalState.value.final === null) {
|
|
2298
|
+
finalContent = t('tasks.finalNone')
|
|
2299
|
+
} else {
|
|
2300
|
+
finalContent = element(React.Fragment, null,
|
|
2301
|
+
element('div', { className: 'dbt-final-copy' }, element(MarkdownText, {
|
|
2302
|
+
text: finalState.value.final.text,
|
|
2303
|
+
labels: { code: { copyLabel: t('tasks.copyCode'), copiedLabel: t('tasks.copiedCode') }, footnotes: t('tasks.footnotes') },
|
|
2304
|
+
})),
|
|
2305
|
+
finalState.value.truncated ? element('div', { className: 'dbt-final-meta' }, `${t('tasks.finalTruncated')} · ${finalState.value.totalCodePoints}`) : null)
|
|
2306
|
+
}
|
|
2307
|
+
detailRows.push(element('div', {
|
|
2308
|
+
key: 'final',
|
|
2309
|
+
className: 'dbt-projection-detail dbt-final-detail',
|
|
2310
|
+
'data-feedback': pendingFinalIds.has(task.id) ? 'pending' : undefined,
|
|
2311
|
+
'data-tone': finalState?.status ?? 'loading',
|
|
2312
|
+
'aria-label': t('tasks.final'),
|
|
2313
|
+
}, element('div', {
|
|
2314
|
+
className: 'dbt-final-input-toggle', role: 'button', tabIndex: 0,
|
|
2315
|
+
'data-no-card-drag': true, draggable: false,
|
|
2316
|
+
'aria-expanded': inputExpanded,
|
|
2317
|
+
'aria-label': t(inputExpanded ? 'tasks.collapseInput' : 'tasks.expandInput'),
|
|
2318
|
+
onClickCapture: (event) => {
|
|
2319
|
+
if (!shouldToggleFinalInput(event.target)) return
|
|
2320
|
+
if (event.detail === 2 && finalClicks.current.doubleClick(task.id, () => toggleProjection(task.id, 'final', preferences.defaultFinalExpanded))) {
|
|
2321
|
+
event.preventDefault()
|
|
2322
|
+
return
|
|
2323
|
+
}
|
|
2324
|
+
if (event.detail > 2) return
|
|
2325
|
+
finalClicks.current.click(task.id, () => toggleProjection(task.id, 'input', preferences.defaultInputExpanded))
|
|
2326
|
+
},
|
|
2327
|
+
onKeyDown: (event) => {
|
|
2328
|
+
if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return
|
|
2329
|
+
event.preventDefault()
|
|
2330
|
+
event.stopPropagation()
|
|
2331
|
+
finalClicks.current.cancel(task.id)
|
|
2332
|
+
toggleProjection(task.id, 'input', preferences.defaultInputExpanded)
|
|
2333
|
+
},
|
|
2334
|
+
}, element('div', { className: 'dbt-projection-copy' }, finalContent)),
|
|
2335
|
+
finalState?.status === 'error' ? element('button', {
|
|
2336
|
+
type: 'button', className: 'dbt-final-retry', 'data-no-card-drag': true, draggable: false,
|
|
2337
|
+
title: finalState.message,
|
|
2338
|
+
onClick: () => setFinalRetry((value) => value + 1),
|
|
2339
|
+
}, t('tasks.finalRetry')) : null))
|
|
2340
|
+
}
|
|
2341
|
+
return element('article', {
|
|
2342
|
+
key: task.id,
|
|
2343
|
+
className: 'dbt-task-card',
|
|
2344
|
+
'data-session-id': task.id,
|
|
2345
|
+
'data-current': String(task.current),
|
|
2346
|
+
'data-state': task.session.status,
|
|
2347
|
+
'data-execution-warning': executionWarning?.kind,
|
|
2348
|
+
'data-drop': dropHalf,
|
|
2349
|
+
'aria-label': `${projectName}: ${stateLabel}`,
|
|
2350
|
+
title: stateLabel,
|
|
2351
|
+
tabIndex: 0,
|
|
2352
|
+
draggable: taskPins.writable === true && !pendingMoves.has(task.id),
|
|
2353
|
+
onPointerDownCapture: (event) => {
|
|
2354
|
+
dragOriginGuard.current = event.target?.closest?.('[data-no-card-drag]') ? task.id : null
|
|
2355
|
+
},
|
|
2356
|
+
onKeyDown: (event) => {
|
|
2357
|
+
if (event.target !== event.currentTarget || !['Enter', ' '].includes(event.key)) return
|
|
2358
|
+
event.preventDefault()
|
|
2359
|
+
openSession(task.id)
|
|
2360
|
+
},
|
|
2361
|
+
onClick: (event) => {
|
|
2362
|
+
const fromControl = event.detail !== 0 && dragOriginGuard.current === task.id
|
|
2363
|
+
dragOriginGuard.current = null
|
|
2364
|
+
if (fromControl || event.target?.closest?.('[data-no-card-drag]') || dragClickGuard.current === task.id) return
|
|
2365
|
+
openSession(task.id)
|
|
2366
|
+
// The browser's second click is the double-click gesture. Keep the
|
|
2367
|
+
// first click immediate; do not re-open Final after its own body
|
|
2368
|
+
// double-click unmounts that control before a later dblclick event.
|
|
2369
|
+
if (event.detail === 2 && isTaskCardFinalShortcutTarget(event.target)) {
|
|
2370
|
+
void cardFinalRequests.current?.request(task.id)
|
|
2371
|
+
}
|
|
2372
|
+
},
|
|
2373
|
+
onDragStart: (event) => {
|
|
2374
|
+
if (taskPins.writable !== true) { event.preventDefault(); return }
|
|
2375
|
+
if (dragOriginGuard.current === task.id || event.target?.closest?.('[data-no-card-drag]')) {
|
|
2376
|
+
dragOriginGuard.current = null
|
|
2377
|
+
event.preventDefault()
|
|
2378
|
+
return
|
|
2379
|
+
}
|
|
2380
|
+
dragClickGuard.current = task.id
|
|
2381
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
2382
|
+
event.dataTransfer.setData('text/plain', task.id)
|
|
2383
|
+
setDrag({ sessionId: task.id, overId: task.id, half: 'before' })
|
|
2384
|
+
},
|
|
2385
|
+
onDragEnd: () => {
|
|
2386
|
+
setDrag(null)
|
|
2387
|
+
window.setTimeout(() => { if (dragClickGuard.current === task.id) dragClickGuard.current = null }, 0)
|
|
2388
|
+
},
|
|
2389
|
+
onDragOver: (event) => {
|
|
2390
|
+
if (drag === null) return
|
|
2391
|
+
event.preventDefault()
|
|
2392
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
2393
|
+
const half = twoColumns ? (event.clientX < rect.left + rect.width / 2 ? 'before' : 'after') : (event.clientY < rect.top + rect.height / 2 ? 'before' : 'after')
|
|
2394
|
+
if (drag.overId !== task.id || drag.half !== half) setDrag({ ...drag, overId: task.id, half })
|
|
2395
|
+
},
|
|
2396
|
+
onDrop: (event) => {
|
|
2397
|
+
event.preventDefault()
|
|
2398
|
+
if (drag !== null) commitDrop(task.id, drag.overId === task.id ? drag.half : 'before')
|
|
2399
|
+
},
|
|
2400
|
+
},
|
|
2401
|
+
element('div', { className: 'dbt-task-title-row' },
|
|
2402
|
+
element('button', {
|
|
2403
|
+
type: 'button',
|
|
2404
|
+
className: 'dbt-project-icon',
|
|
2405
|
+
style: { color: iconColor },
|
|
2406
|
+
'data-no-card-drag': true,
|
|
2407
|
+
draggable: false,
|
|
2408
|
+
disabled: workspace === undefined || appearanceState.status !== 'ready',
|
|
2409
|
+
title: t('tasks.editAppearance'),
|
|
2410
|
+
'aria-label': t('tasks.editAppearance'),
|
|
2411
|
+
onClick: () => setEditingWorkspaceId(workspace?.workspaceId ?? null),
|
|
2412
|
+
}, renderCatalogIcon(icon, 18)),
|
|
2413
|
+
element('div', { className: 'dbt-task-open' },
|
|
2414
|
+
element('span', { className: 'dbt-task-project' }, projectName),
|
|
2415
|
+
element('span', { className: 'dbt-task-session-line' },
|
|
2416
|
+
executionWarning !== null ? element('svg', {
|
|
2417
|
+
className: 'dbt-task-error-icon', width: 14, height: 14, viewBox: '0 0 24 24',
|
|
2418
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: 2, 'aria-hidden': true,
|
|
2419
|
+
}, element('path', { d: 'M12 3 2 21h20L12 3Z', strokeLinejoin: 'round' }),
|
|
2420
|
+
element('path', { d: 'M12 9v5m0 3v1' })) :
|
|
2421
|
+
task.session.status === 'running' ? element(StateDot, { state: 'ongoing' }) : null,
|
|
2422
|
+
element('button', { type: 'button', className: 'dbt-task-rename', 'data-no-card-drag': true, draggable: false,
|
|
2423
|
+
title: t('tasks.rename'), 'aria-label': `${t('tasks.rename')}: ${task.title}`,
|
|
2424
|
+
onClick: () => setTaskAction({ kind: 'rename', id: task.id, title: task.title }),
|
|
2425
|
+
}, task.title),
|
|
2426
|
+
element('span', { className: 'dbt-task-time' }, compactRelativeTime(task.updatedAt, now, t, relativeTime)))),
|
|
2427
|
+
element('button', {
|
|
2428
|
+
type: 'button',
|
|
2429
|
+
className: 'dbt-task-unpin',
|
|
2430
|
+
'data-no-card-drag': true,
|
|
2431
|
+
draggable: false,
|
|
2432
|
+
disabled: pendingMoves.has(task.id),
|
|
2433
|
+
title: t('tasks.unpin'),
|
|
2434
|
+
'aria-label': t('tasks.unpin'),
|
|
2435
|
+
'aria-pressed': true,
|
|
2436
|
+
onDragStart: (event) => event.preventDefault(),
|
|
2437
|
+
onClick: () => {
|
|
2438
|
+
setMoveError('')
|
|
2439
|
+
unpinTask(task.id).catch((error) => setMoveError(String(error?.message ?? error)))
|
|
2440
|
+
},
|
|
2441
|
+
}, renderOfficialPinIcon(14))),
|
|
2442
|
+
element('div', { className: 'dbt-task-projection-bar' },
|
|
2443
|
+
element('button', {
|
|
2444
|
+
type: 'button',
|
|
2445
|
+
className: 'dbt-projection-toggle',
|
|
2446
|
+
'data-no-card-drag': true,
|
|
2447
|
+
draggable: false,
|
|
2448
|
+
disabled: !goalAvailable,
|
|
2449
|
+
'aria-expanded': goalExpanded,
|
|
2450
|
+
'aria-label': t(goalExpanded ? 'tasks.collapseGoal' : 'tasks.expandGoal'),
|
|
2451
|
+
title: goalLabel,
|
|
2452
|
+
onClick: () => { if (goalAvailable) toggleProjection(task.id, 'goal', preferences.defaultGoalExpanded) },
|
|
2453
|
+
}, element(IconGoalOutlineRegular, { size: 14 }), element(goalExpanded ? IconChevronDownOutlineRegular : IconChevronRightOutlineRegular, { size: 14 })),
|
|
2454
|
+
todoVisible ? element('button', {
|
|
2455
|
+
type: 'button',
|
|
2456
|
+
className: 'dbt-projection-toggle',
|
|
2457
|
+
'data-no-card-drag': true,
|
|
2458
|
+
draggable: false,
|
|
2459
|
+
disabled: !todoAvailable,
|
|
2460
|
+
'aria-expanded': todoExpanded,
|
|
2461
|
+
'aria-label': t(todoExpanded ? 'tasks.collapseTodo' : 'tasks.expandTodo'),
|
|
2462
|
+
title: todoLabel,
|
|
2463
|
+
onClick: () => { if (todoAvailable) toggleProjection(task.id, 'todo', preferences.defaultTodoExpanded) },
|
|
2464
|
+
}, element(IconChecklistOutlineRegular, { size: 14 }), todoAvailable ? element('span', { className: 'dbt-todo-count' }, todoLabel) : null, element(todoExpanded ? IconChevronDownOutlineRegular : IconChevronRightOutlineRegular, { size: 14 })) : null,
|
|
2465
|
+
task.session.status !== 'idle' ? null : element('button', {
|
|
2466
|
+
type: 'button',
|
|
2467
|
+
className: 'dbt-projection-toggle',
|
|
2468
|
+
'data-no-card-drag': true,
|
|
2469
|
+
draggable: false,
|
|
2470
|
+
'aria-expanded': finalExpanded,
|
|
2471
|
+
'aria-label': t(finalExpanded ? 'tasks.collapseFinal' : 'tasks.expandFinal'),
|
|
2472
|
+
title: t('tasks.final'),
|
|
2473
|
+
onClick: () => {
|
|
2474
|
+
finalClicks.current.cancel(task.id)
|
|
2475
|
+
toggleProjection(task.id, 'final', preferences.defaultFinalExpanded)
|
|
2476
|
+
},
|
|
2477
|
+
}, element(IconSparkleRegular, { size: 14 }), element(finalExpanded ? IconChevronDownOutlineRegular : IconChevronRightOutlineRegular, { size: 14 }))),
|
|
2478
|
+
detailRows.length === 0 ? null : element('div', { className: 'dbt-projection-details' }, ...detailRows),
|
|
2479
|
+
task.session.status !== 'idle' ? null : element('div', {
|
|
2480
|
+
className: 'dbt-continue-shell', hidden: !inputExpanded,
|
|
2481
|
+
'data-no-card-drag': true, draggable: false,
|
|
2482
|
+
onDragStart: (event) => event.preventDefault(),
|
|
2483
|
+
}, element(TaskContinueInput, {
|
|
2484
|
+
sessionId: task.id,
|
|
2485
|
+
active: inputExpanded,
|
|
2486
|
+
createTaskInput,
|
|
2487
|
+
t,
|
|
2488
|
+
onWarning: (warning) => setActionWarning(`${t('tasks.goalResumeWarning')}: ${warning}`),
|
|
2489
|
+
})),
|
|
2490
|
+
answerable ? element('div', {
|
|
2491
|
+
className: 'dbt-question-surface',
|
|
2492
|
+
'data-no-card-drag': true,
|
|
2493
|
+
draggable: false,
|
|
2494
|
+
onDragStart: (event) => event.preventDefault(),
|
|
2495
|
+
},
|
|
2496
|
+
inlineQuestion ? element(TaskQuestionCard, {
|
|
2497
|
+
key: `${task.id}:${questionTarget.requestKey ?? questionTarget.callId}`,
|
|
2498
|
+
target: questionTarget, createTaskQuestion, fontSize: preferences.detailFontSize,
|
|
2499
|
+
revealQuestion, canRevealQuestion, t,
|
|
2500
|
+
}) : element('button', {
|
|
2501
|
+
type: 'button',
|
|
2502
|
+
className: 'dbt-task-answer',
|
|
2503
|
+
title: t('tasks.answerHint'),
|
|
2504
|
+
onClick: (event) => {
|
|
2505
|
+
event.stopPropagation()
|
|
2506
|
+
const revealed = revealQuestion(task.id, answerCallId)
|
|
2507
|
+
// `null` is "this composition has no question service" (already
|
|
2508
|
+
// filtered out above); `false` is "the panel exists but would not
|
|
2509
|
+
// open". Only the latter is worth telling the user about.
|
|
2510
|
+
if (revealed === false) setActionWarning(t('tasks.answerFailed'))
|
|
2511
|
+
},
|
|
2512
|
+
}, t('tasks.answer'))) : null)
|
|
2513
|
+
}))),
|
|
2514
|
+
editingWorkspace !== undefined ? element(AppearanceDialog, {
|
|
2515
|
+
workspace: editingWorkspace,
|
|
2516
|
+
appearance: appearanceByWorkspace.get(editingWorkspace.workspaceId),
|
|
2517
|
+
appearanceState,
|
|
2518
|
+
onClose: () => setEditingWorkspaceId(null),
|
|
2519
|
+
t,
|
|
2520
|
+
}) : null,
|
|
2521
|
+
taskAction === null ? null : element(TaskActionDialog, {
|
|
2522
|
+
key: `${taskAction.kind}:${taskAction.id}`,
|
|
2523
|
+
action: taskAction,
|
|
2524
|
+
onClose: () => setTaskAction(null),
|
|
2525
|
+
onSubmit: (action, text) => {
|
|
2526
|
+
const current = tasks.find((item) => item.id === action.id)
|
|
2527
|
+
if (current === undefined || action.kind === 'continue' && current.session.status !== 'idle') throw new Error(t('tasks.actionUnavailable'))
|
|
2528
|
+
return submitTaskAction(action, text)
|
|
2529
|
+
},
|
|
2530
|
+
t,
|
|
2531
|
+
}))
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
function SidebarRegion({ wide, collapsed, view, setView, twoColumns, preferences, toggleSidebar, renderSlot, useSessions, useWorkspaces, useSessionStatus, useTaskConnection, usePanelInfo, useTaskPins, openSession, moveTaskBefore, unpinTask, revealQuestion, canRevealQuestion, findTaskQuestion, createTaskQuestion, submitTaskAction, createTaskInput, t }) {
|
|
2535
|
+
if (!wide) {
|
|
2536
|
+
return renderSlot('sidebar.workspaces', { wide: false, expandSidebar: () => { if (collapsed) toggleSidebar() } })
|
|
2537
|
+
}
|
|
2538
|
+
return element('div', { className: 'dbt-region-shell' },
|
|
2539
|
+
element('div', { className: 'dbt-view-tabs', role: 'tablist', 'aria-label': 'Sidebar view' },
|
|
2540
|
+
element('button', { type: 'button', role: 'tab', className: 'dbt-view-tab', 'aria-selected': view === 'native', onClick: () => setView('native') }, t('view.native')),
|
|
2541
|
+
element('button', { type: 'button', role: 'tab', className: 'dbt-view-tab', 'aria-selected': view === 'tasks', onClick: () => setView('tasks') }, t('view.tasks'))),
|
|
2542
|
+
element('div', { className: 'dbt-region-panel', role: 'tabpanel' },
|
|
2543
|
+
view === 'native' ? renderSlot('sidebar.workspaces', { wide: true, expandSidebar: () => {} }) : element(TaskSwimlane, {
|
|
2544
|
+
useSessions,
|
|
2545
|
+
useWorkspaces,
|
|
2546
|
+
useSessionStatus, useTaskConnection,
|
|
2547
|
+
usePanelInfo,
|
|
2548
|
+
useTaskPins,
|
|
2549
|
+
openSession,
|
|
2550
|
+
moveTaskBefore,
|
|
2551
|
+
unpinTask,
|
|
2552
|
+
revealQuestion,
|
|
2553
|
+
canRevealQuestion,
|
|
2554
|
+
findTaskQuestion,
|
|
2555
|
+
createTaskQuestion,
|
|
2556
|
+
submitTaskAction,
|
|
2557
|
+
createTaskInput,
|
|
2558
|
+
twoColumns,
|
|
2559
|
+
preferences,
|
|
2560
|
+
t,
|
|
2561
|
+
})))
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function PanelRow({ panel, usePanelInfo, selectPanel, renderSlot, renderFactorySlot, canUnpin, unpinLabel, setPanelPinned }) {
|
|
2565
|
+
const { Tooltip, IconPinFillRegular, focusWithoutRing } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
2566
|
+
const active = usePanelInfo((info) => info.activePanelId === panel.id)
|
|
2567
|
+
return element('div', { className: 'dbt-panel-entry' },
|
|
2568
|
+
element(Tooltip, { label: panel.label, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, side: 'bottom', portal: true,
|
|
2569
|
+
children: element('button', {
|
|
2570
|
+
type: 'button', className: 'dbt-panel-row',
|
|
2571
|
+
'aria-label': panel.label, 'aria-current': active ? 'page' : undefined,
|
|
2572
|
+
onClick: () => selectPanel(panel.id),
|
|
2573
|
+
}, element('span', { className: 'dbt-panel-glyph', 'aria-hidden': 'true' },
|
|
2574
|
+
typeof renderFactorySlot === 'function'
|
|
2575
|
+
? renderFactorySlot('sidebar.panel.icon', { id: panel.id, size: SIDEBAR_LAYOUT.iconSize, active })
|
|
2576
|
+
: renderSlot('sidebar.panellist', { size: SIDEBAR_LAYOUT.iconSize, active }, { only: panel.id }))) }),
|
|
2577
|
+
// Native More lists only UNPINNED entries. Keep a separate unpin affordance,
|
|
2578
|
+
// or the menu would have no way to acquire its first hidden launcher.
|
|
2579
|
+
canUnpin ? element(Tooltip, { label: unpinLabel, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, side: 'bottom', portal: true,
|
|
2580
|
+
children: element('button', { type: 'button', className: 'dbt-panel-unpin', 'aria-label': unpinLabel,
|
|
2581
|
+
onClick: (event) => {
|
|
2582
|
+
const more = event.currentTarget.closest('nav')?.querySelector('button[aria-haspopup="menu"]')
|
|
2583
|
+
if (more) { if (typeof focusWithoutRing === 'function') focusWithoutRing(more); else more.focus() }
|
|
2584
|
+
setPanelPinned(panel.id, false)
|
|
2585
|
+
},
|
|
2586
|
+
}, element(IconPinFillRegular, { size: 12, 'aria-hidden': true })) }) : null)
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
function PanelMoreMenu({ entries, label, onOpen, onPinnedChange, renderSlot }) {
|
|
2590
|
+
const React = requireModule('react')
|
|
2591
|
+
const { Menu, Tooltip, IconAppsAddOutlineRegular } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
2592
|
+
const [open, setOpen] = React.useState(false)
|
|
2593
|
+
const onDismiss = () => setOpen(false)
|
|
2594
|
+
return element(Menu, {
|
|
2595
|
+
open, onClose: onDismiss, portal: true, autoFocus: true,
|
|
2596
|
+
className: 'dbt-panel-more-anchor', listClassName: 'dbt-panel-more-menu',
|
|
2597
|
+
anchor: element(Tooltip, { label, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, side: 'bottom', portal: true,
|
|
2598
|
+
children: element('button', { type: 'button', className: 'dbt-panel-row',
|
|
2599
|
+
'aria-label': label, 'aria-haspopup': 'menu', 'aria-expanded': open,
|
|
2600
|
+
onClick: () => setOpen(value => !value),
|
|
2601
|
+
}, element(IconAppsAddOutlineRegular, { size: SIDEBAR_LAYOUT.iconSize, 'aria-hidden': true })) }),
|
|
2602
|
+
}, renderSlot('sidebar.panellist.actions', { entries, onOpen, onPinnedChange, onDismiss }))
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/**
|
|
2606
|
+
* Guarded root: the component actually handed to `ctx.slots.register`.
|
|
2607
|
+
*
|
|
2608
|
+
* Two narrow jobs:
|
|
2609
|
+
* 1. Forensics — record whether a hook can legally run in this frame. That one
|
|
2610
|
+
* fact separates "React called us" from "something called the component as a
|
|
2611
|
+
* plain function", which is the difference between a props bug and an
|
|
2612
|
+
* unsupported call site.
|
|
2613
|
+
* 2. Containment — a bare call (no arguments) from anywhere outside React gets a
|
|
2614
|
+
* visible diagnostic block instead of throwing. A throw during render is what
|
|
2615
|
+
* makes the slot retire the occupant and blank the whole column.
|
|
2616
|
+
*
|
|
2617
|
+
* It deliberately owns no hook except the probe, so the short circuit below can
|
|
2618
|
+
* never change the real component's hook order.
|
|
2619
|
+
*/
|
|
2620
|
+
function SidebarRootGuard(props) {
|
|
2621
|
+
const React = requireModule('react')
|
|
2622
|
+
const startedAt = Date.now()
|
|
2623
|
+
// Probe FIRST: a guard that returned before this would hide the very bare call
|
|
2624
|
+
// the probe exists to observe, and B would be misread as "did not crash".
|
|
2625
|
+
let hookOk = true
|
|
2626
|
+
try { React.useState(0) } catch { hookOk = false }
|
|
2627
|
+
const zeroArg = arguments.length === 0
|
|
2628
|
+
// Captured here, while this frame is still the top of the stack: inside the
|
|
2629
|
+
// caller, not after createElement. Empty for a React render, which needs none.
|
|
2630
|
+
const stack = zeroArg && !hookOk ? captureCallStack() : ''
|
|
2631
|
+
if (RENDER_PROBE) {
|
|
2632
|
+
writeRenderProbe({
|
|
2633
|
+
zeroArg,
|
|
2634
|
+
isElement: React.isValidElement(props),
|
|
2635
|
+
propKeys: props !== null && typeof props === 'object' ? Object.keys(props).slice(0, 8) : [],
|
|
2636
|
+
durationMs: Date.now() - startedAt,
|
|
2637
|
+
hookOk,
|
|
2638
|
+
stack,
|
|
2639
|
+
})
|
|
2640
|
+
if (renderProbeCount === 1) {
|
|
2641
|
+
writeStorage(RENDER_ENV_KEY, JSON.stringify({
|
|
2642
|
+
revision: BUILD_REVISION,
|
|
2643
|
+
reactVersion: React.version,
|
|
2644
|
+
propsNull: props === null || props === undefined,
|
|
2645
|
+
argCount: arguments.length,
|
|
2646
|
+
}))
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
// Dry-run guard, after the probe.
|
|
2650
|
+
if (zeroArg && !hookOk) {
|
|
2651
|
+
return React.createElement('div', {
|
|
2652
|
+
'data-better-tasks-diagnostic': 'bare-call',
|
|
2653
|
+
style: { padding: '10px', font: '12px/1.5 ui-monospace, monospace', color: '#ff8f8f' },
|
|
2654
|
+
}, `Better Tasks: sidebar root invoked outside React (revision ${BUILD_REVISION})`)
|
|
2655
|
+
}
|
|
2656
|
+
// Safe defaults keep the real component's hook sequence stable.
|
|
2657
|
+
const safe = props === null || props === undefined ? {} : props
|
|
2658
|
+
// Rendered as an element so the real component keeps its own fiber. The
|
|
2659
|
+
// occupant's throw came from an undefined element type (React #130) among the
|
|
2660
|
+
// descendant icons, which is fixed at the source; no boundary is carried here.
|
|
2661
|
+
return React.createElement(BetterSidebarRoot, safe)
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
/** AppFrame mounts this root-scoped chrome seat only while the macOS rail is hidden. */
|
|
2665
|
+
function SidebarReopenControls({ toggleSidebar, startSession, useRemoteAccess, t }) {
|
|
2666
|
+
const { Tooltip, IconPanelLeftOutlineRegular: PanelIcon, IconNewChatOutlineRegular: NewIcon } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
2667
|
+
return element('div', { className: 'dbt-shell-leading' },
|
|
2668
|
+
element(Tooltip, {
|
|
2669
|
+
label: t('toggle.open'), delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, portal: true,
|
|
2670
|
+
children: element('button', {
|
|
2671
|
+
type: 'button', className: 'dbt-icon-button',
|
|
2672
|
+
'aria-label': t('toggle.open'), 'aria-expanded': false,
|
|
2673
|
+
onClick: toggleSidebar,
|
|
2674
|
+
}, element(PanelIcon, { size: 16 })),
|
|
2675
|
+
}),
|
|
2676
|
+
(typeof useRemoteAccess !== 'function' || useRemoteAccess('session/create')) ? element(Tooltip, {
|
|
2677
|
+
label: t('session.new.label'), delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, portal: true,
|
|
2678
|
+
children: element('button', {
|
|
2679
|
+
type: 'button', className: 'dbt-icon-button',
|
|
2680
|
+
'aria-label': t('session.new.label'),
|
|
2681
|
+
onClick: () => startSession(),
|
|
2682
|
+
}, element(NewIcon, { size: 16 })),
|
|
2683
|
+
}) : null)
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
function BetterSidebarRoot({
|
|
2687
|
+
collapsed,
|
|
2688
|
+
width,
|
|
2689
|
+
startSession,
|
|
2690
|
+
toggleSidebar,
|
|
2691
|
+
selectPanel,
|
|
2692
|
+
usePanels,
|
|
2693
|
+
usePanelInfo,
|
|
2694
|
+
useEntryPins,
|
|
2695
|
+
useEntryActions,
|
|
2696
|
+
useShortcuts,
|
|
2697
|
+
useRemoteAccess,
|
|
2698
|
+
setPanelPinned,
|
|
2699
|
+
nativePanelIcons = false,
|
|
2700
|
+
useSessions,
|
|
2701
|
+
useWorkspaces,
|
|
2702
|
+
useSessionStatus, useTaskConnection,
|
|
2703
|
+
useTaskPins,
|
|
2704
|
+
useTaskPreferences,
|
|
2705
|
+
setTaskPreference,
|
|
2706
|
+
openSession,
|
|
2707
|
+
moveTaskBefore,
|
|
2708
|
+
unpinTask,
|
|
2709
|
+
revealQuestion,
|
|
2710
|
+
canRevealQuestion,
|
|
2711
|
+
findTaskQuestion,
|
|
2712
|
+
createTaskQuestion,
|
|
2713
|
+
submitTaskAction,
|
|
2714
|
+
createTaskInput,
|
|
2715
|
+
t,
|
|
2716
|
+
renderSlot,
|
|
2717
|
+
renderFactorySlot,
|
|
2718
|
+
}) {
|
|
2719
|
+
const React = requireModule('react')
|
|
2720
|
+
const { Tooltip, IconNewChatOutlineMedium, IconNewChatOutlineRegular, ShortcutKeys } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
2721
|
+
const panels = usePanels((value) => value)
|
|
2722
|
+
const unpinned = useEntryPins?.((value) => value) ?? []
|
|
2723
|
+
const hasEntryActions = useEntryActions?.((value) => value) ?? false
|
|
2724
|
+
const activePanelId = usePanelInfo?.((info) => info.activePanelId)
|
|
2725
|
+
const newShortcut = useShortcuts?.((rows) => rows.find(row => row.id === 'session.new'))
|
|
2726
|
+
const canStartSession = typeof useRemoteAccess !== 'function' || useRemoteAccess('session/create')
|
|
2727
|
+
const preferences = useTaskPreferences((snapshot) => snapshot.value)
|
|
2728
|
+
// D1: the view is a Config preference rather than plugin-local localStorage, so
|
|
2729
|
+
// the Host persists it and 0.2 and 0.1.5 reach it through the same path. The old
|
|
2730
|
+
// localStorage value is folded in once by migrateLegacySidebarView().
|
|
2731
|
+
const view = preferences.sidebarView === 'tasks' ? 'tasks' : 'native'
|
|
2732
|
+
const setView = (next) => {
|
|
2733
|
+
void Promise.resolve(setTaskPreference('sidebarView', next === 'tasks' ? 'tasks' : 'native')).catch(() => {})
|
|
2734
|
+
}
|
|
2735
|
+
const windowsTitlebar = typeof document !== 'undefined' && document.documentElement.hasAttribute('data-windows-titlebar')
|
|
2736
|
+
const [settled, setSettled] = React.useState(collapsed)
|
|
2737
|
+
React.useEffect(() => {
|
|
2738
|
+
if (!collapsed) {
|
|
2739
|
+
setSettled(false)
|
|
2740
|
+
return undefined
|
|
2741
|
+
}
|
|
2742
|
+
const timer = window.setTimeout(() => setSettled(true), 150)
|
|
2743
|
+
return () => window.clearTimeout(timer)
|
|
2744
|
+
}, [collapsed])
|
|
2745
|
+
const wide = windowsTitlebar ? !collapsed : !collapsed || !settled
|
|
2746
|
+
const twoColumns = !collapsed && (preferences.columnLayout === 'double' || (preferences.columnLayout === 'auto' && width > TASK_GRID_TWO_COLUMN_THRESHOLD))
|
|
2747
|
+
const lastWideWidth = React.useRef(width)
|
|
2748
|
+
if (!collapsed) lastWideWidth.current = width
|
|
2749
|
+
const toggleLabel = collapsed ? t('toggle.open') : t('toggle.collapse')
|
|
2750
|
+
const newLabel = t('session.new.label')
|
|
2751
|
+
const startNewSession = () => startSession()
|
|
2752
|
+
|
|
2753
|
+
return element('aside', {
|
|
2754
|
+
className: 'dbt-sidebar',
|
|
2755
|
+
'data-wide': String(wide),
|
|
2756
|
+
'data-collapsing': String(collapsed && wide),
|
|
2757
|
+
style: wide ? { width: collapsed ? lastWideWidth.current : width } : undefined,
|
|
2758
|
+
'aria-label': 'DSH sidebar',
|
|
2759
|
+
},
|
|
2760
|
+
element('div', { className: 'dbt-brand-row' },
|
|
2761
|
+
wide ? element(canStartSession ? 'button' : 'span', {
|
|
2762
|
+
type: canStartSession ? 'button' : undefined,
|
|
2763
|
+
className: 'dbt-brand',
|
|
2764
|
+
'aria-label': canStartSession ? newLabel : undefined,
|
|
2765
|
+
'aria-keyshortcuts': canStartSession ? newShortcut?.aria : undefined,
|
|
2766
|
+
onClick: canStartSession ? startNewSession : undefined,
|
|
2767
|
+
},
|
|
2768
|
+
element('span', { className: 'dbt-brand-mark', 'aria-hidden': 'true' },
|
|
2769
|
+
renderSlot('sidebar.brand.mark', { size: 24 }, { fallback: element('span', null, '◆') })),
|
|
2770
|
+
element('span', { className: 'dbt-brand-name' },
|
|
2771
|
+
renderSlot('sidebar.brand.name', {}, { fallback: element('span', null, t('brand.fallback')) }))) : null,
|
|
2772
|
+
element(Tooltip, {
|
|
2773
|
+
label: toggleLabel,
|
|
2774
|
+
delayMs: SIDEBAR_LAYOUT.tooltipDelayMs,
|
|
2775
|
+
side: windowsTitlebar ? 'bottom' : 'right',
|
|
2776
|
+
portal: windowsTitlebar,
|
|
2777
|
+
children: element('button', {
|
|
2778
|
+
type: 'button',
|
|
2779
|
+
className: 'dbt-icon-button dbt-toggle',
|
|
2780
|
+
'aria-label': toggleLabel,
|
|
2781
|
+
'aria-expanded': !collapsed,
|
|
2782
|
+
onClick: toggleSidebar,
|
|
2783
|
+
},
|
|
2784
|
+
!wide && !windowsTitlebar ? element('span', { className: 'dbt-rail-mark', 'aria-hidden': 'true' },
|
|
2785
|
+
renderSlot('sidebar.brand.mark', { size: 24 }, { fallback: element('span', null, '◆') })) : null,
|
|
2786
|
+
element(IconPanelLeftOutlineRegular, { className: 'dbt-panel-icon', size: wide || windowsTitlebar ? 16 : 18 }))
|
|
2787
|
+
})),
|
|
2788
|
+
// rc.3 HOC: owner content is an option, not an owner prop or a fallback.
|
|
2789
|
+
// The renderer keeps this button when no optional action is registered.
|
|
2790
|
+
canStartSession ? element('div', { className: 'dbt-new-session-row' },
|
|
2791
|
+
renderSlot('sidebar.session.action', { wide }, { children: element(Tooltip, {
|
|
2792
|
+
label: newLabel, shortcutKeys: newShortcut?.keys, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs,
|
|
2793
|
+
side: windowsTitlebar ? 'bottom' : 'right', disabled: wide,
|
|
2794
|
+
children: element('button', {
|
|
2795
|
+
type: 'button', className: 'dbt-new-session',
|
|
2796
|
+
'aria-label': newLabel, 'aria-keyshortcuts': newShortcut?.aria, onClick: startNewSession,
|
|
2797
|
+
}, element('span', { className: 'dbt-new-label-mask' },
|
|
2798
|
+
element('span', { className: 'dbt-new-content' },
|
|
2799
|
+
element(wide ? IconNewChatOutlineMedium ?? IconNewChatOutlineRegular : IconNewChatOutlineRegular,
|
|
2800
|
+
{ size: wide ? 14 : windowsTitlebar ? 16 : 18, 'aria-hidden': true }),
|
|
2801
|
+
wide ? element('span', { className: 'dbt-new-label' }, t('session.new')) : null)),
|
|
2802
|
+
wide && newShortcut?.keys?.length > 0 ? element('span', { className: 'dbt-new-shortcut', 'aria-hidden': true },
|
|
2803
|
+
element(ShortcutKeys, { keys: newShortcut.keys })) : null),
|
|
2804
|
+
}) })) : null,
|
|
2805
|
+
panels.length > 0 ? element('nav', { className: 'dbt-panel-list', 'aria-label': t('panels.label') },
|
|
2806
|
+
...panels.filter(panel => panel.id === 'plugins' || !hasEntryActions || !unpinned.includes(panel.id)).map((panel) => element(PanelRow, {
|
|
2807
|
+
key: panel.id, panel, usePanelInfo, selectPanel, renderSlot, setPanelPinned,
|
|
2808
|
+
canUnpin: hasEntryActions && panel.id !== 'plugins', unpinLabel: t('panels.unpin', { name: panel.label }),
|
|
2809
|
+
renderFactorySlot: nativePanelIcons ? renderFactorySlot : undefined,
|
|
2810
|
+
})),
|
|
2811
|
+
hasEntryActions ? element(PanelMoreMenu, {
|
|
2812
|
+
label: t('panels.more'),
|
|
2813
|
+
entries: panels.filter(panel => panel.id !== 'plugins').map(panel => ({
|
|
2814
|
+
id: panel.id, label: panel.label, pinned: !unpinned.includes(panel.id), active: panel.id === activePanelId,
|
|
2815
|
+
})),
|
|
2816
|
+
onOpen: selectPanel, onPinnedChange: setPanelPinned, renderSlot,
|
|
2817
|
+
}) : null) : null,
|
|
2818
|
+
element('div', { className: 'dbt-workspaces' },
|
|
2819
|
+
element(SidebarRegion, {
|
|
2820
|
+
wide,
|
|
2821
|
+
collapsed,
|
|
2822
|
+
view,
|
|
2823
|
+
setView,
|
|
2824
|
+
twoColumns,
|
|
2825
|
+
preferences,
|
|
2826
|
+
toggleSidebar,
|
|
2827
|
+
renderSlot,
|
|
2828
|
+
useSessions,
|
|
2829
|
+
useWorkspaces,
|
|
2830
|
+
useSessionStatus, useTaskConnection,
|
|
2831
|
+
usePanelInfo,
|
|
2832
|
+
useTaskPins,
|
|
2833
|
+
openSession,
|
|
2834
|
+
moveTaskBefore,
|
|
2835
|
+
unpinTask,
|
|
2836
|
+
revealQuestion,
|
|
2837
|
+
canRevealQuestion,
|
|
2838
|
+
findTaskQuestion,
|
|
2839
|
+
createTaskQuestion,
|
|
2840
|
+
submitTaskAction,
|
|
2841
|
+
createTaskInput,
|
|
2842
|
+
t,
|
|
2843
|
+
})),
|
|
2844
|
+
element('div', { className: 'dbt-foot' },
|
|
2845
|
+
element('div', { className: 'dbt-footer-actions' }, renderSlot('sidebar.footer.action', { wide })),
|
|
2846
|
+
element('div', { className: 'dbt-settings' }, renderSlot('sidebar.settings', { wide }))))
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
function TaskSettingsSection({ useTaskPreferences, setTaskPreference, t }) {
|
|
2850
|
+
const React = requireModule('react')
|
|
2851
|
+
const element = React.createElement
|
|
2852
|
+
const snapshot = useTaskPreferences((value) => value)
|
|
2853
|
+
const [draft, setDraft] = React.useState(snapshot.value)
|
|
2854
|
+
const [pending, setPending] = React.useState(false)
|
|
2855
|
+
const [failed, setFailed] = React.useState(false)
|
|
2856
|
+
React.useEffect(() => setDraft(snapshot.value), [snapshot.value])
|
|
2857
|
+
const writable = snapshot.status === 'ready' && snapshot.writable && !pending
|
|
2858
|
+
const update = (field, value) => {
|
|
2859
|
+
if (!writable) return
|
|
2860
|
+
setDraft((current) => ({ ...current, [field]: value }))
|
|
2861
|
+
setFailed(false)
|
|
2862
|
+
setPending(true)
|
|
2863
|
+
Promise.resolve(setTaskPreference(field, value)).catch(() => {
|
|
2864
|
+
setDraft(snapshot.value)
|
|
2865
|
+
setFailed(true)
|
|
2866
|
+
}).finally(() => setPending(false))
|
|
2867
|
+
}
|
|
2868
|
+
const switchRow = (field, label) => element('div', { className: 'dbt-settings-switch-row', key: field },
|
|
2869
|
+
element('span', null, label),
|
|
2870
|
+
element('button', {
|
|
2871
|
+
type: 'button',
|
|
2872
|
+
role: 'switch',
|
|
2873
|
+
className: 'dbt-settings-switch',
|
|
2874
|
+
'aria-label': label,
|
|
2875
|
+
'aria-checked': String(draft[field]),
|
|
2876
|
+
disabled: !writable,
|
|
2877
|
+
onClick: () => update(field, !draft[field]),
|
|
2878
|
+
}))
|
|
2879
|
+
const status = failed
|
|
2880
|
+
? element('div', { className: 'dbt-settings-status', 'data-error': true }, t('settings.writeFailed'))
|
|
2881
|
+
: snapshot.status === 'loading'
|
|
2882
|
+
? element('div', { className: 'dbt-settings-status' }, t('settings.loading'))
|
|
2883
|
+
: snapshot.status === 'unavailable'
|
|
2884
|
+
? element('div', { className: 'dbt-settings-status' }, t('settings.unavailable'))
|
|
2885
|
+
: snapshot.status === 'ready' && !snapshot.writable
|
|
2886
|
+
? element('div', { className: 'dbt-settings-status' }, t('settings.readOnly'))
|
|
2887
|
+
: null
|
|
2888
|
+
return element('section', { className: 'dbt-settings-section', 'aria-labelledby': 'dbt-settings-title' },
|
|
2889
|
+
element('div', { className: 'dbt-settings-heading' },
|
|
2890
|
+
element('h2', { id: 'dbt-settings-title' }, t('settings.title')),
|
|
2891
|
+
element('p', null, t('settings.description'))),
|
|
2892
|
+
status,
|
|
2893
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2894
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.fontSize')),
|
|
2895
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2896
|
+
element('div', { className: 'dbt-settings-font-options' },
|
|
2897
|
+
...[11, 12, 13, 14, 15, 16].map((size) => element('button', {
|
|
2898
|
+
key: size,
|
|
2899
|
+
type: 'button',
|
|
2900
|
+
className: 'dbt-settings-font-option',
|
|
2901
|
+
'aria-label': `${size}px`,
|
|
2902
|
+
'aria-pressed': String(draft.detailFontSize === size),
|
|
2903
|
+
disabled: !writable,
|
|
2904
|
+
onClick: () => update('detailFontSize', size),
|
|
2905
|
+
}, `${size}px`))),
|
|
2906
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.fontSizeHint')))),
|
|
2907
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2908
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.titleEmphasis')),
|
|
2909
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2910
|
+
element('div', { className: 'dbt-settings-segments', 'data-options': '2' },
|
|
2911
|
+
...['project', 'task'].map((emphasis) => element('button', {
|
|
2912
|
+
key: emphasis,
|
|
2913
|
+
type: 'button',
|
|
2914
|
+
className: 'dbt-settings-segment',
|
|
2915
|
+
'aria-pressed': String(draft.titleEmphasis === emphasis),
|
|
2916
|
+
disabled: !writable,
|
|
2917
|
+
onClick: () => update('titleEmphasis', emphasis),
|
|
2918
|
+
}, t(`settings.titleEmphasis.${emphasis}`)))),
|
|
2919
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.titleEmphasisHint')))),
|
|
2920
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2921
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.defaultExpansion')),
|
|
2922
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2923
|
+
switchRow('defaultTodoExpanded', t('settings.todoExpanded')),
|
|
2924
|
+
switchRow('defaultGoalExpanded', t('settings.goalExpanded')),
|
|
2925
|
+
switchRow('defaultFinalExpanded', t('settings.finalExpanded')),
|
|
2926
|
+
switchRow('defaultInputExpanded', t('settings.inputExpanded')),
|
|
2927
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.expansionHint')))),
|
|
2928
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2929
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.columns')),
|
|
2930
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2931
|
+
element('div', { className: 'dbt-settings-segments' },
|
|
2932
|
+
...['auto', 'single', 'double'].map((layout) => element('button', {
|
|
2933
|
+
key: layout,
|
|
2934
|
+
type: 'button',
|
|
2935
|
+
className: 'dbt-settings-segment',
|
|
2936
|
+
'aria-pressed': String(draft.columnLayout === layout),
|
|
2937
|
+
disabled: !writable,
|
|
2938
|
+
onClick: () => update('columnLayout', layout),
|
|
2939
|
+
}, t(`settings.columns.${layout}`)))),
|
|
2940
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.columnsHint')))))
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
function mountTaskSettingsSection(ctx, taskPreferences) {
|
|
2944
|
+
const injected = () => ({
|
|
2945
|
+
hooks: { taskPreferences: taskPreferences.source },
|
|
2946
|
+
setTaskPreference: (field, value) => taskPreferences.set(field, value),
|
|
2947
|
+
})
|
|
2948
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
2949
|
+
name: 'settings.section',
|
|
2950
|
+
id: 'better-tasks',
|
|
2951
|
+
order: 45,
|
|
2952
|
+
label: () => ctx.locale.bind(NS)('settings.nav'),
|
|
2953
|
+
locale: NS,
|
|
2954
|
+
inject: injected,
|
|
2955
|
+
}, TaskSettingsSection))
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
function mountSidebar(ctx, taskPins, taskPreferences) {
|
|
2959
|
+
// One confirmed native membership source for the official list and task cards.
|
|
2960
|
+
// uiWorkspace adds the official sidebar pin-fronting policy without navigation.
|
|
2961
|
+
ctx.effect(() => taskPins.connectNative({
|
|
2962
|
+
source: ctx.get?.('workspaces')?.list,
|
|
2963
|
+
sessions: ctx.get?.('sessions')?.list,
|
|
2964
|
+
pinSession: (id) => ctx.uiWorkspace.pinSession(id),
|
|
2965
|
+
unpinSession: (id) => ctx.uiWorkspace.unpinSession(id),
|
|
2966
|
+
}), 'dsh-better-tasks: native Pin projection')
|
|
2967
|
+
ctx.effect(() => ctx.uiWorkspace.onSessionCreated((sessionId) => taskPins.pinCreated(sessionId)), 'dsh-better-tasks: auto-pin created sessions')
|
|
2968
|
+
|
|
2969
|
+
// 0.2's catalog is metadata-only until projections are explicitly requested.
|
|
2970
|
+
// Read the Host-owned snapshots for pinned sessions, including unopened ones;
|
|
2971
|
+
// sessions.refreshProjections deduplicates per connection and needs no retain.
|
|
2972
|
+
ctx.effect(() => {
|
|
2973
|
+
const refresh = () => {
|
|
2974
|
+
const sessions = ctx.get?.('sessions')
|
|
2975
|
+
if (typeof sessions?.refreshProjections !== 'function') return
|
|
2976
|
+
for (const id of taskPins.source.getSnapshot().queue.sessionIds) {
|
|
2977
|
+
void sessions.refreshProjections(id).catch(() => {})
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const stop = taskPins.source.subscribe(refresh)
|
|
2981
|
+
refresh()
|
|
2982
|
+
return stop
|
|
2983
|
+
}, 'dsh-better-tasks: pinned session projections')
|
|
2984
|
+
|
|
2985
|
+
const panels = createSnapshotStore([])
|
|
2986
|
+
const panelPins = createPanelLauncherPins()
|
|
2987
|
+
const nativePanelIcons = typeof ctx.slots.registerFactory === 'function'
|
|
2988
|
+
const entryActions = {
|
|
2989
|
+
getSnapshot: () => nativePanelIcons && ctx.slots.entriesOfSlot('sidebar.panellist.actions').length > 0,
|
|
2990
|
+
subscribe: (listener) => ctx.slots.subscribe('sidebar.panellist.actions', listener),
|
|
2991
|
+
}
|
|
2992
|
+
const emptyShortcuts = createSnapshotStore([])
|
|
2993
|
+
const syncPanels = () => {
|
|
2994
|
+
const next = ctx.slots.entriesOfSlot('sidebar.panellist').map(({ options }, index) => ({
|
|
2995
|
+
id: options.id,
|
|
2996
|
+
order: options.order ?? 0,
|
|
2997
|
+
registrationIndex: index,
|
|
2998
|
+
label: resolveLabel(options.label, options.id),
|
|
2999
|
+
})).filter((panel) => typeof panel.id === 'string' && panel.id !== '')
|
|
3000
|
+
.sort((left, right) => left.order - right.order || left.registrationIndex - right.registrationIndex)
|
|
3001
|
+
panels.set(next)
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
ctx.effect(() => ctx.slots.subscribe('sidebar.panellist', syncPanels), 'dsh-better-tasks: panel entries')
|
|
3005
|
+
ctx.effect(() => ctx.locale.subscribe(syncPanels), 'dsh-better-tasks: panel labels')
|
|
3006
|
+
|
|
3007
|
+
// Optional factory lookup keeps startup independent of the input providers.
|
|
3008
|
+
// The surface owns its retained generation; metadata reads are not editor leases.
|
|
3009
|
+
const taskInputs = createTaskInputManager({
|
|
3010
|
+
getConversation: () => ctx.get('conversation'),
|
|
3011
|
+
getSessions: () => ctx.get('sessions'),
|
|
3012
|
+
getStatus: (id) => ctx.get('uiSession')?.sessionStatus.getSnapshot().get(id),
|
|
3013
|
+
getGoals: () => ctx.get('remote.goals') ?? ctx.get('remote')?.goals,
|
|
3014
|
+
isPinned: (id) => taskPins.source.getSnapshot().queue.sessionIds.includes(id),
|
|
3015
|
+
})
|
|
3016
|
+
const createTaskInput = (sessionId, options) => taskInputs.create(sessionId, options)
|
|
3017
|
+
const findTaskQuestion = (sessionId) => {
|
|
3018
|
+
try { return ctx.get('questionSurface')?.find?.(sessionId) }
|
|
3019
|
+
catch { return undefined } // Optional discovery cannot retire the sidebar root.
|
|
3020
|
+
}
|
|
3021
|
+
const createTaskQuestion = (options) => {
|
|
3022
|
+
const provider = ctx.get('questionSurface')
|
|
3023
|
+
if (typeof provider?.createCard !== 'function') throw new Error('Inline question surface is unavailable')
|
|
3024
|
+
return provider.createCard(options)
|
|
3025
|
+
}
|
|
3026
|
+
ctx.effect(() => () => taskInputs.dispose(), 'dsh-better-tasks: input surfaces')
|
|
3027
|
+
|
|
3028
|
+
const taskConnection = ctx.get?.('connection')?.state ?? createSnapshotStore(undefined)
|
|
3029
|
+
const injectProps = () => ({
|
|
3030
|
+
setTaskPreference: (field, value) => taskPreferences.set(field, value),
|
|
3031
|
+
startSession: (workspaceId, beforeOpen) => ctx.uiWorkspace.startSession(workspaceId, beforeOpen),
|
|
3032
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
3033
|
+
selectPanel: (panelId) => {
|
|
3034
|
+
if (panels.getSnapshot().some(panel => panel.id === panelId)) ctx.layout.selectPanel(panelId)
|
|
3035
|
+
},
|
|
3036
|
+
setPanelPinned: (panelId, pinned) => {
|
|
3037
|
+
if (panels.getSnapshot().some(panel => panel.id === panelId)) panelPins.setPinned(panelId, pinned)
|
|
3038
|
+
},
|
|
3039
|
+
nativePanelIcons,
|
|
3040
|
+
openSession: (sessionId) => ctx.uiWorkspace.openSession(sessionId),
|
|
3041
|
+
moveTaskBefore: (sessionId, beforeSessionId) => taskPins.moveBefore(sessionId, beforeSessionId),
|
|
3042
|
+
unpinTask: (sessionId) => taskPins.setPinned(sessionId, false),
|
|
3043
|
+
createTaskInput,
|
|
3044
|
+
findTaskQuestion,
|
|
3045
|
+
createTaskQuestion,
|
|
3046
|
+
submitTaskAction: (action, text) => {
|
|
3047
|
+
if (action.kind !== 'rename') throw new Error('Messages must use the native input surface')
|
|
3048
|
+
return taskInputs.rename(action.id, text)
|
|
3049
|
+
},
|
|
3050
|
+
// Optional by design: a composition without `ui-user-questions` (or without
|
|
3051
|
+
// this Session's call identity) hides the action instead of rendering a dead
|
|
3052
|
+
// button. Probed per render, not memoized: the service may load later.
|
|
3053
|
+
canRevealQuestion: () => ctx.get('userQuestionPanels') !== undefined,
|
|
3054
|
+
revealQuestion: (sessionId, callId) => ctx.get('userQuestionPanels')?.reveal(sessionId, callId) ?? null,
|
|
3055
|
+
hooks: { panels, entryPins: panelPins.source, entryActions,
|
|
3056
|
+
shortcuts: ctx.get?.('shortcuts')?.catalog ?? ctx.shortcuts?.catalog ?? emptyShortcuts,
|
|
3057
|
+
taskPins: taskPins.source, taskPreferences: taskPreferences.source, taskConnection },
|
|
3058
|
+
})
|
|
3059
|
+
|
|
3060
|
+
ctx.slots.inject('sidebar', () => {
|
|
3061
|
+
// The factory is the sole owner of panellist on rc.2; keep the direct legacy
|
|
3062
|
+
// declaration only on renderers without factories. No duplicate slot owners.
|
|
3063
|
+
const stopIcon = nativePanelIcons ? ctx.slots.registerFactory({
|
|
3064
|
+
name: 'sidebar.panel.icon', scope: 'root',
|
|
3065
|
+
children: { 'sidebar.panellist': { kind: 'list', scope: 'root' } },
|
|
3066
|
+
}, SidebarPanelIcon) : undefined
|
|
3067
|
+
const stopSidebar = ctx.slots.register({
|
|
3068
|
+
name: 'sidebar', locale: NS,
|
|
3069
|
+
children: createSidebarChildDeclarations({ iconsInFactory: nativePanelIcons }),
|
|
3070
|
+
inject: injectProps,
|
|
3071
|
+
}, SidebarRootGuard)
|
|
3072
|
+
return () => { stopSidebar(); stopIcon?.() }
|
|
3073
|
+
})
|
|
3074
|
+
// Disabling native ui-sidebar removes more than its column root. Restore its
|
|
3075
|
+
// outside-the-column reopen seat without taking over the native frame.
|
|
3076
|
+
ctx.slots.inject('shell.leading', () => ctx.slots.register({
|
|
3077
|
+
name: 'shell.leading',
|
|
3078
|
+
locale: NS,
|
|
3079
|
+
inject: () => ({
|
|
3080
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
3081
|
+
startSession: () => ctx.uiWorkspace.startSession(),
|
|
3082
|
+
}),
|
|
3083
|
+
}, SidebarReopenControls))
|
|
3084
|
+
|
|
3085
|
+
syncPanels()
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
function apply(ctx) {
|
|
3089
|
+
ctx.effect(() => installStyles(), 'dsh-better-tasks: styles')
|
|
3090
|
+
ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-better-tasks: dictionaries')
|
|
3091
|
+
const taskPins = createTaskPinsClient()
|
|
3092
|
+
const taskPreferences = createTaskPreferencesStore()
|
|
3093
|
+
ctx.provide('betterTaskPins', taskPins)
|
|
3094
|
+
ctx.effect(() => taskPins.start(), 'dsh-better-tasks: pin projection')
|
|
3095
|
+
// Settings, two generations, neither name in the module-level `inject`:
|
|
3096
|
+
// 0.1.5 -> `settingsScope.bind({ namespace })` returns the read/write scope.
|
|
3097
|
+
// 0.2 -> that service is gone; `configForms.get(entryId)` returns a
|
|
3098
|
+
// ConfigFormController whose getSnapshot/subscribe/set are the same
|
|
3099
|
+
// shape. The namespace is the plugin's own entry id, which is why
|
|
3100
|
+
// TASK_SETTINGS_NAMESPACE is both the 0.1.5 namespace and (by
|
|
3101
|
+
// install convention, as every in-box plugin does) the 0.2 row id.
|
|
3102
|
+
// A generation that lacks one name still activates: inject is non-blocking, and
|
|
3103
|
+
// only the matching probe fires.
|
|
3104
|
+
let settingsBound = false
|
|
3105
|
+
/**
|
|
3106
|
+
* Fold the pre-D1 localStorage view preference into the Config once.
|
|
3107
|
+
*
|
|
3108
|
+
* The Config is the single source of truth from now on, so the legacy key is
|
|
3109
|
+
* removed only after the Host write lands; a failed or unwritable Host leaves it
|
|
3110
|
+
* in place for the next start. Runs behind the settings inject, which is why it
|
|
3111
|
+
* cannot be a render-time effect.
|
|
3112
|
+
* @param store - the preferences store.
|
|
3113
|
+
*/
|
|
3114
|
+
let migrationPending = false
|
|
3115
|
+
const migrateLegacySidebarView = (store) => {
|
|
3116
|
+
if (migrationPending) return
|
|
3117
|
+
let legacy = null
|
|
3118
|
+
try {
|
|
3119
|
+
if (localStorage.getItem(VIEW_MIGRATED_KEY) !== null) return
|
|
3120
|
+
legacy = localStorage.getItem(LEGACY_VIEW_KEY)
|
|
3121
|
+
} catch {
|
|
3122
|
+
return
|
|
3123
|
+
}
|
|
3124
|
+
if (legacy !== 'tasks' && legacy !== 'native') {
|
|
3125
|
+
writeStorage(VIEW_MIGRATED_KEY, '1')
|
|
3126
|
+
return
|
|
3127
|
+
}
|
|
3128
|
+
if (store.source.getSnapshot().writable !== true) return
|
|
3129
|
+
migrationPending = true
|
|
3130
|
+
void Promise.resolve().then(() => store.set('sidebarView', legacy)).then(
|
|
3131
|
+
() => {
|
|
3132
|
+
try { localStorage.removeItem(LEGACY_VIEW_KEY) } catch {}
|
|
3133
|
+
writeStorage(VIEW_MIGRATED_KEY, '1')
|
|
3134
|
+
},
|
|
3135
|
+
() => {},
|
|
3136
|
+
).finally(() => { migrationPending = false })
|
|
3137
|
+
}
|
|
3138
|
+
const bindSettings = (scope) => {
|
|
3139
|
+
if (settingsBound) return
|
|
3140
|
+
const settings = typeof scope.configForms?.get === 'function'
|
|
3141
|
+
? scope.configForms.get(TASK_SETTINGS_ENTRY_ID)
|
|
3142
|
+
: typeof scope.settingsScope?.bind === 'function'
|
|
3143
|
+
? scope.settingsScope.bind({ namespace: TASK_SETTINGS_NAMESPACE })
|
|
3144
|
+
: undefined
|
|
3145
|
+
if (settings === undefined) return
|
|
3146
|
+
settingsBound = true
|
|
3147
|
+
scope.effect(() => taskPreferences.connect(settings), 'dsh-better-tasks: settings projection')
|
|
3148
|
+
// Config forms load asynchronously. Run migration once a writable snapshot
|
|
3149
|
+
// arrives, not just while the controller still reports `loading`.
|
|
3150
|
+
scope.effect(() => taskPreferences.source.subscribe(() => migrateLegacySidebarView(taskPreferences)), 'dsh-better-tasks: legacy view migration')
|
|
3151
|
+
migrateLegacySidebarView(taskPreferences)
|
|
3152
|
+
mountTaskSettingsSection(scope, taskPreferences)
|
|
3153
|
+
}
|
|
3154
|
+
ctx.inject(['configForms'], bindSettings)
|
|
3155
|
+
ctx.inject(['settingsScope'], bindSettings)
|
|
3156
|
+
ctx.inject(['uiWorkspace'], (scope) => mountSidebar(scope, taskPins, taskPreferences))
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
const contract = Object.freeze({
|
|
3160
|
+
activityRanges: ACTIVITY_RANGE_DAYS,
|
|
3161
|
+
childSlots: SIDEBAR_CHILD_SLOTS,
|
|
3162
|
+
beforeIdForTaskDrop,
|
|
3163
|
+
createSidebarChildDeclarations,
|
|
3164
|
+
derivePinnedTasks,
|
|
3165
|
+
deriveRecentTasks,
|
|
3166
|
+
indexWorkspacesBySession,
|
|
3167
|
+
loadProjectAppearances,
|
|
3168
|
+
summarizeGoal,
|
|
3169
|
+
summarizeSession,
|
|
3170
|
+
summarizeTodos,
|
|
3171
|
+
taskExecutionWarning,
|
|
3172
|
+
})
|
|
3173
|
+
|
|
3174
|
+
return { name, inject, apply, contract }
|
|
3175
|
+
},
|
|
3176
|
+
})
|