@veewo/dsh-better-tasks 0.0.0-stage → 0.4.4
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 +65 -0
- package/docs/rollback.md +78 -0
- package/docs/sidebar-layout.md +72 -0
- package/docs/task-session-activity.md +13 -0
- package/lib/client.js +3184 -0
- package/package.json +78 -4
- package/src/client.js +2355 -0
- package/src/final-message.mjs +75 -0
- package/src/index.js +222 -0
- package/src/native-pin-host.mjs +309 -0
- package/src/pin-client.mjs +204 -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/src/client.js
ADDED
|
@@ -0,0 +1,2355 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTIVITY_RANGE_DAYS,
|
|
3
|
+
SIDEBAR_CHILD_SLOTS,
|
|
4
|
+
beforeIdForTaskDrop,
|
|
5
|
+
createSidebarChildDeclarations,
|
|
6
|
+
derivePinnedTasks,
|
|
7
|
+
deriveRecentTasks,
|
|
8
|
+
indexWorkspacesBySession,
|
|
9
|
+
loadProjectAppearances,
|
|
10
|
+
summarizeGoal,
|
|
11
|
+
summarizeSession,
|
|
12
|
+
summarizeTodos,
|
|
13
|
+
taskExecutionWarning,
|
|
14
|
+
} from './task-model.mjs'
|
|
15
|
+
import { createTaskPinsClient } from './pin-client.mjs'
|
|
16
|
+
import { createTaskInputManager } from './task-input-surface.mjs'
|
|
17
|
+
import { TASK_SETTINGS_NAMESPACE, TASK_SETTINGS_ENTRY_ID, createTaskPreferencesStore } from './task-preferences.mjs'
|
|
18
|
+
|
|
19
|
+
export const name = 'dsh-better-tasks'
|
|
20
|
+
// The shared question surface is optional: older/native question packages keep
|
|
21
|
+
// their composer behavior without delaying sidebar, Session, or Workspace startup.
|
|
22
|
+
export const inject = ['slots', 'layout', 'locale']
|
|
23
|
+
|
|
24
|
+
const NS = 'better-tasks-sidebar'
|
|
25
|
+
const STYLE_ID = 'dsh-better-tasks/sidebar'
|
|
26
|
+
const TASK_GRID_TWO_COLUMN_THRESHOLD = 528
|
|
27
|
+
const FINAL_CACHE_KEY = 'dsh-better-tasks.final-message-cache.v1'
|
|
28
|
+
/**
|
|
29
|
+
* Build revision, injected by scripts/build-client.mjs as the sha256 of this
|
|
30
|
+
* plugin's client-side sources. Every render-probe payload carries it: the
|
|
31
|
+
* browser's localStorage is an append log, so records from earlier builds stay
|
|
32
|
+
* forever and only payloads whose revision matches the delivered build count.
|
|
33
|
+
*/
|
|
34
|
+
const BUILD_REVISION = '__BUILD_REVISION__'
|
|
35
|
+
/** Render probe: on by default, so a delivered build can prove the column rendered. */
|
|
36
|
+
const RENDER_PROBE = true
|
|
37
|
+
/** Per-render payload; rewritten on every invocation. */
|
|
38
|
+
const RENDER_PROBE_KEY = 'dbt-render-probe'
|
|
39
|
+
/** One-shot environment facts for the same build. */
|
|
40
|
+
const RENDER_ENV_KEY = 'dbt-render-env'
|
|
41
|
+
/** Cumulative call-mix ledger; survives the per-frame payload being overwritten. */
|
|
42
|
+
const RENDER_LEDGER_KEY = 'dbt-render-ledger'
|
|
43
|
+
/** The inner component's own throw, captured in the same fiber before it escapes. */
|
|
44
|
+
const RENDER_ERROR_KEY = 'dbt-render-error'
|
|
45
|
+
/** The view this plugin kept in localStorage before D1 moved it to the Config. */
|
|
46
|
+
const LEGACY_VIEW_KEY = 'dsh-better-tasks.view'
|
|
47
|
+
/** Set once the legacy localStorage view has been folded into the preference. */
|
|
48
|
+
const VIEW_MIGRATED_KEY = 'dsh-better-tasks.view-migrated'
|
|
49
|
+
|
|
50
|
+
let renderProbeCount = 0
|
|
51
|
+
let renderBareCount = 0
|
|
52
|
+
let renderReactCount = 0
|
|
53
|
+
let firstBareStack = null
|
|
54
|
+
|
|
55
|
+
/** Diagnostics must never break a render. */
|
|
56
|
+
function writeStorage(key, value) {
|
|
57
|
+
try { localStorage.setItem(key, value) } catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Capture the call site of a bare invocation.
|
|
62
|
+
*
|
|
63
|
+
* Taken while this frame is still the top of the stack, so the frames above it are
|
|
64
|
+
* the caller's. This is the only way to name the path that invokes the registered
|
|
65
|
+
* component outside React.
|
|
66
|
+
* @returns the first 25 frames, truncated to 1500 characters.
|
|
67
|
+
*/
|
|
68
|
+
function captureCallStack() {
|
|
69
|
+
try {
|
|
70
|
+
return String(new Error().stack ?? '').split('\n').slice(0, 25).join('\n').slice(0, 1500)
|
|
71
|
+
} catch {
|
|
72
|
+
return ''
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Write one render-probe payload. Called at the very top of the guarded root,
|
|
78
|
+
* BEFORE any dry-run short circuit: a guard that returns first would hide the
|
|
79
|
+
* bare call this probe exists to observe, and the A/B question would stay
|
|
80
|
+
* unanswered forever while looking like "did not crash".
|
|
81
|
+
* @param input - the frame's observations.
|
|
82
|
+
*/
|
|
83
|
+
function writeRenderProbe(input) {
|
|
84
|
+
renderProbeCount += 1
|
|
85
|
+
if (input.zeroArg) renderBareCount += 1
|
|
86
|
+
if (input.hookOk && !input.zeroArg) renderReactCount += 1
|
|
87
|
+
if (input.zeroArg && firstBareStack === null) firstBareStack = input.stack ?? ''
|
|
88
|
+
const via = input.zeroArg && !input.hookOk ? 'bare' : (input.hookOk ? 'react' : 'unknown')
|
|
89
|
+
writeStorage(RENDER_PROBE_KEY, JSON.stringify({
|
|
90
|
+
revision: BUILD_REVISION,
|
|
91
|
+
rendered: renderProbeCount,
|
|
92
|
+
zeroArg: input.zeroArg,
|
|
93
|
+
isElement: input.isElement,
|
|
94
|
+
propKeys: input.propKeys,
|
|
95
|
+
durationMs: input.durationMs,
|
|
96
|
+
// The A/B criterion itself: a hook is legal here only on a React render
|
|
97
|
+
// stack, so a failed probe means this call site is not React rendering.
|
|
98
|
+
hookOk: input.hookOk,
|
|
99
|
+
via,
|
|
100
|
+
zeroArgCount: renderBareCount,
|
|
101
|
+
// Only on a bare call, and only from the true call site.
|
|
102
|
+
...(input.zeroArg ? { stack: input.stack ?? '' } : {}),
|
|
103
|
+
}))
|
|
104
|
+
// One overwritten key cannot show a MIX of callers: the last frame wins, so
|
|
105
|
+
// "all six were bare" and "five bare then one React render" look identical.
|
|
106
|
+
// The ledger keeps the totals, making `react: 0` a provable fact.
|
|
107
|
+
writeRenderLedger()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Rewrite the cumulative ledger. Shared by the per-frame probe and the inner
|
|
112
|
+
* component's throw capture, so the error count is visible in the same record.
|
|
113
|
+
*/
|
|
114
|
+
function writeRenderLedger() {
|
|
115
|
+
writeStorage(RENDER_LEDGER_KEY, JSON.stringify({
|
|
116
|
+
revision: BUILD_REVISION,
|
|
117
|
+
total: renderProbeCount,
|
|
118
|
+
react: renderReactCount,
|
|
119
|
+
bare: renderBareCount,
|
|
120
|
+
zeroArgCount: renderBareCount,
|
|
121
|
+
firstBareStack,
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
const dictionaries = {
|
|
125
|
+
zh: {
|
|
126
|
+
'brand.fallback': 'DSH',
|
|
127
|
+
'session.new': '新会话',
|
|
128
|
+
'session.new.label': '新建会话',
|
|
129
|
+
'toggle.open': '打开侧边栏',
|
|
130
|
+
'toggle.collapse': '收起侧边栏',
|
|
131
|
+
'panels.label': '全局面板',
|
|
132
|
+
'panels.more': '更多',
|
|
133
|
+
'panels.unpin': '取消固定 {name}',
|
|
134
|
+
'view.native': '会话',
|
|
135
|
+
'view.tasks': '任务',
|
|
136
|
+
'tasks.goal': 'Goal',
|
|
137
|
+
'tasks.todo': 'Todo',
|
|
138
|
+
'tasks.todoCompleted': '已完成',
|
|
139
|
+
'tasks.todoInProgress': '进行中',
|
|
140
|
+
'tasks.todoPending': '待处理',
|
|
141
|
+
'tasks.empty': '还没有置顶会话',
|
|
142
|
+
'tasks.pinMigration': '正在合并已有置顶任务…',
|
|
143
|
+
'tasks.expandGoal': '展开 Goal',
|
|
144
|
+
'tasks.collapseGoal': '收起 Goal',
|
|
145
|
+
'tasks.expandTodo': '展开 Todo',
|
|
146
|
+
'tasks.collapseTodo': '收起 Todo',
|
|
147
|
+
'tasks.final': 'Final',
|
|
148
|
+
'tasks.expandFinal': '展开 Final message',
|
|
149
|
+
'tasks.collapseFinal': '收起 Final message',
|
|
150
|
+
'tasks.finalLoading': '正在读取 Final message…',
|
|
151
|
+
'tasks.finalNone': '没有 Final message',
|
|
152
|
+
'tasks.finalError': 'Final message 读取失败',
|
|
153
|
+
'tasks.finalRetry': '重试',
|
|
154
|
+
'tasks.finalTruncated': '内容已截断',
|
|
155
|
+
'tasks.copyCode': '复制代码',
|
|
156
|
+
'tasks.copiedCode': '已复制',
|
|
157
|
+
'tasks.footnotes': '脚注',
|
|
158
|
+
'tasks.unpin': '从任务中移除',
|
|
159
|
+
'tasks.continue': '继续任务',
|
|
160
|
+
'tasks.continueDefault': '继续',
|
|
161
|
+
'tasks.inputLoading': '正在打开输入框…',
|
|
162
|
+
'tasks.inputSearch': '搜索技能或指令…',
|
|
163
|
+
'tasks.inputEmpty': '未找到匹配的技能或指令',
|
|
164
|
+
'tasks.inputCatalogError': '部分项目加载失败,请收起后重试',
|
|
165
|
+
'tasks.retryInput': '重试',
|
|
166
|
+
'tasks.expandInput': '展开续行输入栏',
|
|
167
|
+
'tasks.collapseInput': '收起续行输入栏',
|
|
168
|
+
'tasks.goalResumeWarning': '消息已发送,但 Goal 恢复失败',
|
|
169
|
+
'tasks.send': '发送',
|
|
170
|
+
'tasks.add': '添加',
|
|
171
|
+
'tasks.skillSearch': '搜索技能',
|
|
172
|
+
'tasks.skillLoading': '正在加载…',
|
|
173
|
+
'tasks.skillEmpty': '没有匹配的技能',
|
|
174
|
+
'tasks.skillUnavailable': '技能列表暂不可用',
|
|
175
|
+
'tasks.rename': '重命名会话',
|
|
176
|
+
'tasks.renameAction': '重命名',
|
|
177
|
+
'tasks.actionUnavailable': '当前会话状态已变化,请重试',
|
|
178
|
+
'tasks.runningAria': '运行中',
|
|
179
|
+
'tasks.blockAria': '等待交互',
|
|
180
|
+
'tasks.answer': '去回答',
|
|
181
|
+
'tasks.questionLoading': '正在加载问题选项…',
|
|
182
|
+
'tasks.answerHint': '打开该会话的问题面板',
|
|
183
|
+
'tasks.answerFailed': '没能打开问题面板,请打开会话作答',
|
|
184
|
+
'tasks.idleAria': '空闲',
|
|
185
|
+
'tasks.execution.disconnected': '连接已断开 · 任务状态未知',
|
|
186
|
+
'tasks.execution.error': '执行失败',
|
|
187
|
+
'tasks.execution.aborted': '执行已停止',
|
|
188
|
+
'tasks.execution.interrupted': '执行已中断',
|
|
189
|
+
'tasks.execution.max-tokens': '执行已中断 · 达到输出上限',
|
|
190
|
+
'tasks.appearanceUnavailable': '项目外观暂不可用',
|
|
191
|
+
'tasks.unknown': '未知',
|
|
192
|
+
'tasks.none': '无',
|
|
193
|
+
'tasks.editAppearance': '设置项目图标和颜色',
|
|
194
|
+
'tasks.appearanceTitle': '项目外观',
|
|
195
|
+
'tasks.iconSearch': '搜索图标',
|
|
196
|
+
'tasks.cancel': '取消',
|
|
197
|
+
'tasks.save': '保存',
|
|
198
|
+
'settings.nav': '任务视图',
|
|
199
|
+
'settings.title': '任务视图',
|
|
200
|
+
'settings.description': '自定义任务卡投影的显示方式。修改会立即生效。',
|
|
201
|
+
'settings.fontSize': '详情正文字号',
|
|
202
|
+
'settings.fontSizeHint': '影响 Todo、Goal、Final 的展开正文及任务卡问题区。',
|
|
203
|
+
'settings.titleEmphasis': '任务卡名称强调',
|
|
204
|
+
'settings.titleEmphasis.project': '强调项目名',
|
|
205
|
+
'settings.titleEmphasis.task': '强调任务名',
|
|
206
|
+
'settings.titleEmphasisHint': '仅调整项目名与任务名的字号、字重和主次色。',
|
|
207
|
+
'settings.defaultExpansion': '默认展开',
|
|
208
|
+
'settings.todoExpanded': 'Todo',
|
|
209
|
+
'settings.goalExpanded': 'Goal',
|
|
210
|
+
'settings.finalExpanded': 'Final',
|
|
211
|
+
'settings.inputExpanded': '快速消息栏',
|
|
212
|
+
'settings.expansionHint': '默认值适用于未手动调整的内容。Goal 换目标、Todo 换目标或清单内容、Final 和快速消息栏进入新空闲周期时恢复默认;仅进度变化不重置 Todo。当前正在浏览的任务进入新空闲周期时不自动展开 Final;手动展开仍有效。快速消息栏仅在空闲卡片的 Final 展开时显示。',
|
|
213
|
+
'settings.columns': '任务列布局',
|
|
214
|
+
'settings.columns.auto': '自动',
|
|
215
|
+
'settings.columns.single': '单列',
|
|
216
|
+
'settings.columns.double': '双列',
|
|
217
|
+
'settings.columnsHint': '自动模式在侧边栏宽度大于 528px 时使用双列。',
|
|
218
|
+
'settings.loading': '正在读取设置…',
|
|
219
|
+
'settings.unavailable': '设置暂不可用,当前使用默认值。',
|
|
220
|
+
'settings.readOnly': '当前连接只读。',
|
|
221
|
+
'settings.writeFailed': '保存失败,已恢复 Host 设置。',
|
|
222
|
+
'time.now': '刚刚',
|
|
223
|
+
'time.minutes': '{n}分钟',
|
|
224
|
+
'time.hours': '{n}小时',
|
|
225
|
+
'time.days': '{n}天',
|
|
226
|
+
'time.months': '{n}个月',
|
|
227
|
+
'time.years': '{n}年',
|
|
228
|
+
},
|
|
229
|
+
en: {
|
|
230
|
+
'brand.fallback': 'DSH',
|
|
231
|
+
'session.new': 'New Session',
|
|
232
|
+
'session.new.label': 'New session',
|
|
233
|
+
'toggle.open': 'Open sidebar',
|
|
234
|
+
'toggle.collapse': 'Collapse sidebar',
|
|
235
|
+
'panels.label': 'Global panels',
|
|
236
|
+
'panels.more': 'More',
|
|
237
|
+
'panels.unpin': 'Unpin {name}',
|
|
238
|
+
'view.native': 'Sessions',
|
|
239
|
+
'view.tasks': 'Tasks',
|
|
240
|
+
'tasks.goal': 'Goal',
|
|
241
|
+
'tasks.todo': 'Todo',
|
|
242
|
+
'tasks.todoCompleted': 'Completed',
|
|
243
|
+
'tasks.todoInProgress': 'In progress',
|
|
244
|
+
'tasks.todoPending': 'Pending',
|
|
245
|
+
'tasks.empty': 'No pinned sessions yet',
|
|
246
|
+
'tasks.pinMigration': 'Importing existing pinned tasks…',
|
|
247
|
+
'tasks.expandGoal': 'Expand Goal',
|
|
248
|
+
'tasks.collapseGoal': 'Collapse Goal',
|
|
249
|
+
'tasks.expandTodo': 'Expand Todo',
|
|
250
|
+
'tasks.collapseTodo': 'Collapse Todo',
|
|
251
|
+
'tasks.final': 'Final',
|
|
252
|
+
'tasks.expandFinal': 'Expand final message',
|
|
253
|
+
'tasks.collapseFinal': 'Collapse final message',
|
|
254
|
+
'tasks.finalLoading': 'Loading final message…',
|
|
255
|
+
'tasks.finalNone': 'No final message',
|
|
256
|
+
'tasks.finalError': 'Failed to load final message',
|
|
257
|
+
'tasks.finalRetry': 'Retry',
|
|
258
|
+
'tasks.finalTruncated': 'Content truncated',
|
|
259
|
+
'tasks.copyCode': 'Copy code',
|
|
260
|
+
'tasks.copiedCode': 'Copied',
|
|
261
|
+
'tasks.footnotes': 'Footnotes',
|
|
262
|
+
'tasks.unpin': 'Unpin task',
|
|
263
|
+
'tasks.continue': 'Continue task',
|
|
264
|
+
'tasks.continueDefault': 'Continue',
|
|
265
|
+
'tasks.inputLoading': 'Opening input…',
|
|
266
|
+
'tasks.inputSearch': 'Search skills or commands…',
|
|
267
|
+
'tasks.inputEmpty': 'No matching skills or commands',
|
|
268
|
+
'tasks.inputCatalogError': 'Some items could not load. Close and reopen to retry.',
|
|
269
|
+
'tasks.retryInput': 'Retry',
|
|
270
|
+
'tasks.expandInput': 'Expand follow-up input',
|
|
271
|
+
'tasks.collapseInput': 'Collapse follow-up input',
|
|
272
|
+
'tasks.goalResumeWarning': 'Message sent, but Goal resume failed',
|
|
273
|
+
'tasks.send': 'Send',
|
|
274
|
+
'tasks.add': 'Add',
|
|
275
|
+
'tasks.skillSearch': 'Search skills',
|
|
276
|
+
'tasks.skillLoading': 'Loading…',
|
|
277
|
+
'tasks.skillEmpty': 'No matching skills',
|
|
278
|
+
'tasks.skillUnavailable': 'Skills are unavailable',
|
|
279
|
+
'tasks.rename': 'Rename session',
|
|
280
|
+
'tasks.renameAction': 'Rename',
|
|
281
|
+
'tasks.actionUnavailable': 'Session state changed; please try again',
|
|
282
|
+
'tasks.runningAria': 'Running',
|
|
283
|
+
'tasks.blockAria': 'Waiting for interaction',
|
|
284
|
+
'tasks.answer': 'Go answer',
|
|
285
|
+
'tasks.questionLoading': 'Loading question options…',
|
|
286
|
+
'tasks.answerHint': 'Open this Session’s question panel',
|
|
287
|
+
'tasks.answerFailed': 'Could not open the question panel — open the Session to answer',
|
|
288
|
+
'tasks.idleAria': 'Idle',
|
|
289
|
+
'tasks.execution.disconnected': 'Connection lost · task status unknown',
|
|
290
|
+
'tasks.execution.error': 'Execution failed',
|
|
291
|
+
'tasks.execution.aborted': 'Execution stopped',
|
|
292
|
+
'tasks.execution.interrupted': 'Execution interrupted',
|
|
293
|
+
'tasks.execution.max-tokens': 'Execution stopped · token limit reached',
|
|
294
|
+
'tasks.appearanceUnavailable': 'Project appearance is unavailable',
|
|
295
|
+
'tasks.unknown': 'Unknown',
|
|
296
|
+
'tasks.none': 'None',
|
|
297
|
+
'tasks.editAppearance': 'Set project icon and color',
|
|
298
|
+
'tasks.appearanceTitle': 'Project appearance',
|
|
299
|
+
'tasks.iconSearch': 'Search icons',
|
|
300
|
+
'tasks.cancel': 'Cancel',
|
|
301
|
+
'tasks.save': 'Save',
|
|
302
|
+
'settings.nav': 'Task View',
|
|
303
|
+
'settings.title': 'Task View',
|
|
304
|
+
'settings.description': 'Customize how task-card projections are displayed. Changes apply immediately.',
|
|
305
|
+
'settings.fontSize': 'Detail text size',
|
|
306
|
+
'settings.fontSizeHint': 'Affects expanded Todo, Goal, and Final text, plus task-card questions.',
|
|
307
|
+
'settings.titleEmphasis': 'Task card name emphasis',
|
|
308
|
+
'settings.titleEmphasis.project': 'Emphasize project',
|
|
309
|
+
'settings.titleEmphasis.task': 'Emphasize task',
|
|
310
|
+
'settings.titleEmphasisHint': 'Changes only the project and task name size, weight, and color.',
|
|
311
|
+
'settings.defaultExpansion': 'Default expansion',
|
|
312
|
+
'settings.todoExpanded': 'Todo',
|
|
313
|
+
'settings.goalExpanded': 'Goal',
|
|
314
|
+
'settings.finalExpanded': 'Final',
|
|
315
|
+
'settings.inputExpanded': 'Quick message bar',
|
|
316
|
+
'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.',
|
|
317
|
+
'settings.columns': 'Task columns',
|
|
318
|
+
'settings.columns.auto': 'Auto',
|
|
319
|
+
'settings.columns.single': 'Single',
|
|
320
|
+
'settings.columns.double': 'Double',
|
|
321
|
+
'settings.columnsHint': 'Auto uses two columns when the sidebar is wider than 528px.',
|
|
322
|
+
'settings.loading': 'Loading settings…',
|
|
323
|
+
'settings.unavailable': 'Settings are unavailable; defaults are active.',
|
|
324
|
+
'settings.readOnly': 'This connection is read-only.',
|
|
325
|
+
'settings.writeFailed': 'Save failed; Host settings were restored.',
|
|
326
|
+
'time.now': 'now',
|
|
327
|
+
'time.minutes': '{n}min',
|
|
328
|
+
'time.hours': '{n}h',
|
|
329
|
+
'time.days': '{n}d',
|
|
330
|
+
'time.months': '{n}mo',
|
|
331
|
+
'time.years': '{n}y',
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Single numeric source for sidebar presentation; upstream semantic tokens stay native.
|
|
336
|
+
export const SIDEBAR_LAYOUT = Object.freeze({
|
|
337
|
+
newSessionHeight: 38, newSessionFontSize: 14, newSessionLineHeight: 22,
|
|
338
|
+
iconTarget: 34, railTarget: 36, iconSize: 18, unpinTarget: 18,
|
|
339
|
+
navigationGap: 4, maxNavigationRows: 2, tooltipDelayMs: 500,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const css = `
|
|
343
|
+
.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}
|
|
344
|
+
.dbt-sidebar[data-wide="false"]{--dbt-pad:8px;align-items:center;padding-inline:8px}
|
|
345
|
+
.dbt-brand-row{width:100%;height:32px;display:flex;align-items:center;gap:6px;flex:none}
|
|
346
|
+
.dbt-brand,.dbt-icon-button,.dbt-new-session,.dbt-panel-row{border:0;font:inherit;color:inherit;cursor:pointer}
|
|
347
|
+
.dbt-brand{min-width:0;display:flex;align-items:center;gap:8px;flex:1;background:transparent;padding:0 4px;text-align:left}
|
|
348
|
+
.dbt-brand-mark,.dbt-panel-glyph,.dbt-rail-mark{display:inline-flex;align-items:center;justify-content:center;flex:none}
|
|
349
|
+
.dbt-brand-name,.dbt-panel-label,.dbt-new-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
350
|
+
.dbt-brand-name{font-weight:650}
|
|
351
|
+
.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}
|
|
352
|
+
.dbt-icon-button:hover,.dbt-panel-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
353
|
+
.dbt-sidebar[data-wide="false"] .dbt-brand-row{height:36px}
|
|
354
|
+
.dbt-sidebar[data-wide="false"] .dbt-icon-button{width:36px;height:36px;color:var(--dsw-alias-label-primary)}
|
|
355
|
+
.dbt-sidebar[data-wide="false"] .dbt-toggle .dbt-panel-icon{display:none}
|
|
356
|
+
.dbt-sidebar[data-wide="false"] .dbt-toggle:hover .dbt-panel-icon{display:inline}
|
|
357
|
+
.dbt-sidebar[data-wide="false"] .dbt-toggle:hover .dbt-rail-mark{display:none}
|
|
358
|
+
/* Desktop collapses this column to zero, not to the browser's icon rail.
|
|
359
|
+
Match the native sidebar's Windows caption affordances instead of changing
|
|
360
|
+
the layout owner or forcing a phantom rail width. */
|
|
361
|
+
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}
|
|
362
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-brand-row{height:0}
|
|
363
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"]{padding:0}
|
|
364
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-toggle .dbt-panel-icon{display:inline}
|
|
365
|
+
html[data-windows-titlebar] .dbt-sidebar .dbt-toggle .dbt-rail-mark{display:none}
|
|
366
|
+
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}
|
|
367
|
+
html[data-windows-titlebar] .dbt-sidebar[data-wide="false"] .dbt-new-session:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
368
|
+
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}
|
|
369
|
+
html[data-windows-titlebar]:has(.dbt-sidebar[data-wide="false"]){--dsh-windows-menu-start:84px}
|
|
370
|
+
.dbt-shell-leading{display:flex;align-items:center;gap:8px;-webkit-app-region:no-drag}
|
|
371
|
+
/* Native New Session tokens; the rc.3 HOC anchor is display:contents.
|
|
372
|
+
The row owns spacing, so optional actions sit beside (not inside) the button. */
|
|
373
|
+
.dbt-new-session-row{width:calc(100% - 8px);min-width:0;display:flex;align-items:center;gap:6px;flex:none;margin:0 4px}
|
|
374
|
+
.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}
|
|
375
|
+
.dbt-new-session:hover{background:var(--dsw-alias-button-floating-hover)}
|
|
376
|
+
.dbt-new-label-mask{flex:1;min-width:0}
|
|
377
|
+
.dbt-new-content{display:flex;width:100cqw;align-items:center;justify-content:center;gap:6px}
|
|
378
|
+
.dbt-new-content>svg{flex:none}
|
|
379
|
+
.dbt-new-label{max-width:200px;overflow:hidden;white-space:nowrap}
|
|
380
|
+
.dbt-new-shortcut{opacity:0;display:inline-flex;flex:none;pointer-events:none;font-weight:400}
|
|
381
|
+
.dbt-new-session:is(:hover,:focus-visible) .dbt-new-shortcut{opacity:1}
|
|
382
|
+
.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)}
|
|
383
|
+
html[data-platform="darwin"] .dbt-new-session{background:var(--dsw-specific-sidebar-glass-fill)}
|
|
384
|
+
html[data-platform="darwin"] .dbt-new-session:hover{background:var(--dsw-specific-sidebar-glass-hover)}
|
|
385
|
+
.dbt-sidebar[data-wide="false"] .dbt-new-session-row{width:36px;flex-direction:column;align-items:center;gap:4px;margin:0}
|
|
386
|
+
.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}
|
|
387
|
+
.dbt-sidebar[data-wide="false"] .dbt-new-session:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
388
|
+
.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}
|
|
389
|
+
.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}
|
|
390
|
+
.dbt-panel-row:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
391
|
+
.dbt-panel-row[aria-current="page"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-brand-primary)}
|
|
392
|
+
.dbt-panel-glyph{display:inline-flex;align-items:center;justify-content:center}
|
|
393
|
+
.dbt-panel-entry{position:relative;display:inline-flex;flex:none}
|
|
394
|
+
.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}
|
|
395
|
+
.dbt-panel-entry:is(:hover,:focus-within) .dbt-panel-unpin{opacity:1;pointer-events:auto}
|
|
396
|
+
.dbt-panel-unpin:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
397
|
+
.dbt-panel-more-anchor{display:inline-flex;flex:none}
|
|
398
|
+
.dbt-panel-more-menu{min-width:220px}
|
|
399
|
+
.dbt-sidebar[data-wide="false"] .dbt-panel-list{flex-direction:column;flex-wrap:nowrap;max-height:30%}
|
|
400
|
+
.dbt-sidebar[data-wide="false"] .dbt-panel-row{width:var(--dbt-rail-target);height:var(--dbt-rail-target)}
|
|
401
|
+
.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}
|
|
402
|
+
.dbt-workspaces{width:100%;min-height:0;display:flex;flex:1;overflow:hidden}
|
|
403
|
+
.dbt-foot{width:100%;display:flex;flex-direction:column;gap:4px;flex:none}
|
|
404
|
+
.dbt-footer-actions,.dbt-settings{width:100%}
|
|
405
|
+
.dbt-sidebar[data-wide="false"] .dbt-footer-actions,.dbt-sidebar[data-wide="false"] .dbt-settings{display:flex;justify-content:center}
|
|
406
|
+
.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}
|
|
407
|
+
.dbt-region-shell{width:100%;min-height:0;display:flex;flex:1;flex-direction:column;overflow:hidden}
|
|
408
|
+
.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}
|
|
409
|
+
.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}
|
|
410
|
+
.dbt-view-tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
411
|
+
.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}
|
|
412
|
+
.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}
|
|
413
|
+
.dbt-task-view{min-height:0;display:flex;flex:1;flex-direction:column;gap:7px;overflow:hidden}
|
|
414
|
+
.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}
|
|
415
|
+
.dbt-task-list[data-columns="2"]{display:flex;align-items:flex-start}
|
|
416
|
+
.dbt-task-column{min-width:0;flex:1;display:flex;flex-direction:column;gap:7px}
|
|
417
|
+
.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}
|
|
418
|
+
.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))}
|
|
419
|
+
.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))}
|
|
420
|
+
.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))}
|
|
421
|
+
.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))}
|
|
422
|
+
.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))}
|
|
423
|
+
.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))}
|
|
424
|
+
.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))}
|
|
425
|
+
.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))}
|
|
426
|
+
.dbt-task-error-icon{flex:none;color:var(--dsw-alias-state-error-primary)}
|
|
427
|
+
.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}
|
|
428
|
+
.dbt-task-card[data-drop="before"]:before{top:1px}.dbt-task-card[data-drop="after"]:after{bottom:1px}
|
|
429
|
+
.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}
|
|
430
|
+
.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}
|
|
431
|
+
.dbt-task-title-row{display:flex;align-items:center;gap:7px;min-width:0}
|
|
432
|
+
.dbt-task-open{min-width:0;flex:1;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;padding:0}
|
|
433
|
+
.dbt-task-project{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:680;line-height:19px}
|
|
434
|
+
.dbt-task-session-line{display:flex;align-items:center;gap:5px;min-width:0;margin-top:1px}
|
|
435
|
+
.dbt-task-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
436
|
+
.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}
|
|
437
|
+
.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)}
|
|
438
|
+
.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)}
|
|
439
|
+
.dbt-task-rename:hover{text-decoration:underline;color:var(--dsw-alias-label-primary)}
|
|
440
|
+
.dbt-task-card[data-current="true"] .dbt-task-rename{color:var(--dsw-alias-brand-primary)}
|
|
441
|
+
.dbt-task-list[data-title-emphasis="task"] .dbt-task-card[data-current="true"] .dbt-task-rename{color:var(--dsw-alias-brand-primary)}
|
|
442
|
+
.dbt-continue-shell,.dbt-continue-form{cursor:text}
|
|
443
|
+
.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)}
|
|
444
|
+
.dbt-continue-form:focus-within{border-color:var(--dsw-alias-brand-primary)}
|
|
445
|
+
.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}
|
|
446
|
+
.dbt-continue-actions{box-sizing:border-box;display:flex;justify-content:space-between;align-items:center;min-height:36px;padding:1px 6px 6px}
|
|
447
|
+
.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}
|
|
448
|
+
.dbt-continue-add:hover,.dbt-continue-add[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
449
|
+
.dbt-continue-add:disabled{opacity:.4;cursor:default}
|
|
450
|
+
.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}
|
|
451
|
+
.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}
|
|
452
|
+
.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}
|
|
453
|
+
.dbt-skill-results{max-height:180px;overflow-y:auto;margin-top:5px;display:flex;flex-direction:column;gap:2px}
|
|
454
|
+
.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}
|
|
455
|
+
.dbt-skill-picker button:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
456
|
+
.dbt-skill-picker small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;color:var(--dsw-alias-label-tertiary);font-size:11px}
|
|
457
|
+
.dbt-skill-status{padding:6px;color:var(--dsw-alias-label-secondary);font-size:11px}
|
|
458
|
+
.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}
|
|
459
|
+
.dbt-continue-send:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}
|
|
460
|
+
.dbt-continue-send:disabled{opacity:.4;cursor:default}
|
|
461
|
+
.dbt-continue-send:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
462
|
+
.dbt-continue-error{margin-top:-4px}
|
|
463
|
+
.dbt-task-time{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary)}
|
|
464
|
+
.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}
|
|
465
|
+
.dbt-project-icon:disabled{cursor:default;opacity:.72}
|
|
466
|
+
.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}
|
|
467
|
+
.dbt-task-unpin svg{width:14px;height:14px;flex:none}
|
|
468
|
+
.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}
|
|
469
|
+
.dbt-task-unpin:hover{border-color:color-mix(in srgb,var(--dbt-unpin-color) 40%,transparent)}
|
|
470
|
+
.dbt-task-unpin:disabled{cursor:default;opacity:.45}
|
|
471
|
+
.dbt-task-projection-bar{display:flex;align-items:center;gap:5px}
|
|
472
|
+
.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}
|
|
473
|
+
.dbt-projection-toggle:hover,.dbt-projection-toggle[aria-expanded="true"]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
474
|
+
.dbt-projection-toggle:disabled{cursor:default;opacity:.38;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}
|
|
475
|
+
.dbt-todo-count{font-size:10px;font-variant-numeric:tabular-nums}
|
|
476
|
+
.dbt-projection-details{display:flex;flex-direction:column;gap:6px;padding-top:1px}
|
|
477
|
+
.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}
|
|
478
|
+
.dbt-projection-detail.dbt-final-detail{display:block;background:var(--dsw-alias-bg-layer-1);padding:10px;transition:background-color .34s ease}
|
|
479
|
+
.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}
|
|
480
|
+
@media (prefers-reduced-motion:reduce){.dbt-projection-detail.dbt-final-detail{transition:none}}
|
|
481
|
+
.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}
|
|
482
|
+
.dbt-final-input-toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
483
|
+
.dbt-projection-detail[data-tone="blocked"]{color:var(--dsw-alias-error,#d14343)}
|
|
484
|
+
.dbt-projection-copy{min-width:0;overflow-wrap:anywhere}
|
|
485
|
+
.dbt-final-copy{min-width:0;overflow-wrap:anywhere}
|
|
486
|
+
/* Rebind the consumed Markdown shorthands locally: BODY-derived theme tokens do
|
|
487
|
+
not recompute merely because a descendant changes the content-size input. */
|
|
488
|
+
.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)}
|
|
489
|
+
.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)}
|
|
490
|
+
.dbt-final-copy [data-code-block-banner] > :first-child,.dbt-question-surface [data-code-block-banner] > :first-child{font-size:inherit;line-height:inherit}
|
|
491
|
+
.dbt-final-copy pre,.dbt-final-copy table{max-width:100%;overflow-x:auto}
|
|
492
|
+
.dbt-final-copy img{max-width:100%;height:auto}
|
|
493
|
+
.dbt-final-meta{margin-top:5px;color:var(--dsw-alias-label-tertiary);font-size:10px}
|
|
494
|
+
.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}
|
|
495
|
+
.dbt-question-surface{min-width:0;max-width:100%;overflow:hidden;font-size:var(--dbt-projection-font-size,13px);line-height:1.5}
|
|
496
|
+
.dbt-question-notice{padding:6px 8px;color:var(--dsw-alias-warning,#a26900);font:inherit}
|
|
497
|
+
.dbt-question-surface *{min-width:0}
|
|
498
|
+
.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)}
|
|
499
|
+
.dbt-task-answer:hover{background:color-mix(in srgb,var(--dbt-block-yellow,#d2aa00) 28%,transparent)}
|
|
500
|
+
.dbt-task-answer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
501
|
+
.dbt-todo-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:5px}
|
|
502
|
+
.dbt-todo-item{display:grid;grid-template-columns:16px minmax(0,1fr);gap:5px;align-items:start}
|
|
503
|
+
.dbt-todo-glyph{display:grid;place-items:center;flex:none;width:16px;height:16px}
|
|
504
|
+
.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)}
|
|
505
|
+
.dbt-task-warning{padding:6px 8px;color:var(--dsw-alias-warning,#a26900)}
|
|
506
|
+
.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}
|
|
507
|
+
.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}
|
|
508
|
+
.dbt-dialog h2{font-size:16px;margin:0}
|
|
509
|
+
.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}
|
|
510
|
+
.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}
|
|
511
|
+
.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}
|
|
512
|
+
.dbt-icon-grid{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:6px;max-height:360px;overflow:auto}
|
|
513
|
+
.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}
|
|
514
|
+
.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)}
|
|
515
|
+
.dbt-color-row{display:flex;flex-wrap:wrap;gap:7px}
|
|
516
|
+
.dbt-color-choice{width:25px;height:25px;border-radius:50%;border:2px solid transparent;cursor:pointer}
|
|
517
|
+
.dbt-color-choice[data-selected="true"]{outline:2px solid currentColor;outline-offset:2px}
|
|
518
|
+
.dbt-dialog-actions{display:flex;justify-content:flex-end;gap:8px}
|
|
519
|
+
.dbt-dialog-actions button{height:32px;border:0;border-radius:8px;padding:0 12px;cursor:pointer}
|
|
520
|
+
.dbt-dialog-save{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-static-white,#fff)}
|
|
521
|
+
.dbt-settings-section{width:min(680px,100%);display:flex;flex-direction:column;gap:18px;color:var(--dsw-alias-label-primary)}
|
|
522
|
+
.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}
|
|
523
|
+
.dbt-settings-group{display:flex;flex-direction:column;gap:10px}.dbt-settings-group-title{font-size:14px;font-weight:650}
|
|
524
|
+
.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}
|
|
525
|
+
.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}
|
|
526
|
+
.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}
|
|
527
|
+
.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}
|
|
528
|
+
.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)}
|
|
529
|
+
@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}}
|
|
530
|
+
`
|
|
531
|
+
|
|
532
|
+
function createSnapshotStore(initial) {
|
|
533
|
+
let snapshot = initial
|
|
534
|
+
const listeners = new Set()
|
|
535
|
+
return {
|
|
536
|
+
getSnapshot: () => snapshot,
|
|
537
|
+
subscribe(listener) {
|
|
538
|
+
listeners.add(listener)
|
|
539
|
+
return () => listeners.delete(listener)
|
|
540
|
+
},
|
|
541
|
+
set(next) {
|
|
542
|
+
snapshot = next
|
|
543
|
+
for (const listener of listeners) listener()
|
|
544
|
+
},
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Same transient owner/lifetime as native ui-sidebar: panel launchers are not Session Pins.
|
|
549
|
+
export function createPanelLauncherPins() {
|
|
550
|
+
const source = createSnapshotStore(Object.freeze([]))
|
|
551
|
+
return {
|
|
552
|
+
source,
|
|
553
|
+
setPinned(id, pinned) {
|
|
554
|
+
if (typeof id !== 'string' || id === '' || typeof pinned !== 'boolean' || id === 'plugins') return
|
|
555
|
+
const unpinned = source.getSnapshot()
|
|
556
|
+
if (pinned !== unpinned.includes(id)) return
|
|
557
|
+
source.set(Object.freeze(pinned ? unpinned.filter(value => value !== id) : [...unpinned, id]))
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function SidebarPanelIcon({ id, size, active, renderSlot }) {
|
|
563
|
+
return renderSlot('sidebar.panellist', { size, active }, { only: id })
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function resolveLabel(label, fallback) {
|
|
567
|
+
if (typeof label === 'function') return String(label())
|
|
568
|
+
return typeof label === 'string' && label !== '' ? label : fallback
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function installStyles() {
|
|
572
|
+
if (typeof document === 'undefined') return () => {}
|
|
573
|
+
const previous = document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`)
|
|
574
|
+
if (previous !== null) return () => {}
|
|
575
|
+
const tag = document.createElement('style')
|
|
576
|
+
tag.dataset.plugin = name
|
|
577
|
+
tag.dataset.pluginCss = STYLE_ID
|
|
578
|
+
tag.textContent = css
|
|
579
|
+
document.head.appendChild(tag)
|
|
580
|
+
return () => tag.remove()
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function element(type, props, ...children) {
|
|
584
|
+
return requireModule('react').createElement(type, props, ...children)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const COLOR_HEX = Object.freeze({
|
|
588
|
+
blue: '#3b82f6', cyan: '#06b6d4', green: '#22c55e', orange: '#f97316',
|
|
589
|
+
pink: '#ec4899', purple: '#8b5cf6', red: '#ef4444', yellow: '#eab308',
|
|
590
|
+
teal: '#14b8a6', indigo: '#6366f1', amber: '#f59e0b', slate: '#64748b', black: '#111827',
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
function useStoredPreference(key, fallback, allowed) {
|
|
594
|
+
const React = requireModule('react')
|
|
595
|
+
const [value, setValue] = React.useState(() => {
|
|
596
|
+
try {
|
|
597
|
+
const stored = localStorage.getItem(key)
|
|
598
|
+
return stored !== null && allowed.includes(stored) ? stored : fallback
|
|
599
|
+
} catch {
|
|
600
|
+
return fallback
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
React.useEffect(() => {
|
|
604
|
+
writeStorage(key, value)
|
|
605
|
+
}, [key, value])
|
|
606
|
+
return [value, setValue]
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function isProjectionExpanded(value, key, defaultExpanded) {
|
|
610
|
+
return typeof value[key] === 'boolean' ? value[key] : defaultExpanded
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Native activity is lastPromptAt; cold cache misses may fall back to createdAt. */
|
|
614
|
+
export function isNewFinalIdleCycle(cycles, task) {
|
|
615
|
+
if (!Number.isFinite(task.updatedAt) || task.updatedAt < 0) return false
|
|
616
|
+
const consumed = cycles[task.id]
|
|
617
|
+
// A cold/partial/older snapshot is not evidence of another completed cycle.
|
|
618
|
+
// Never lower the persisted watermark and then reset again on hydration.
|
|
619
|
+
return !Number.isFinite(consumed) || task.updatedAt > consumed
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function expandFinalForIdleCycles(cycles, idleTasks, value) {
|
|
623
|
+
const nextCycles = { ...cycles }
|
|
624
|
+
const nextExpanded = { ...value }
|
|
625
|
+
let cyclesChanged = false
|
|
626
|
+
let expandedChanged = false
|
|
627
|
+
for (const task of idleTasks) {
|
|
628
|
+
if (!isNewFinalIdleCycle(nextCycles, task)) continue
|
|
629
|
+
nextCycles[task.id] = task.updatedAt
|
|
630
|
+
cyclesChanged = true
|
|
631
|
+
// Consume every new cycle, including the currently browsed Session. Its
|
|
632
|
+
// suppression is remembered so navigation/refresh cannot open it later.
|
|
633
|
+
const finalKey = `${task.id}:final`
|
|
634
|
+
if (task.current === true) {
|
|
635
|
+
// Explicit manual expansion wins; suppress only the automatic default.
|
|
636
|
+
if (nextExpanded[finalKey] !== true && nextExpanded[finalKey] !== false) {
|
|
637
|
+
nextExpanded[finalKey] = false
|
|
638
|
+
expandedChanged = true
|
|
639
|
+
}
|
|
640
|
+
} else if (Object.hasOwn(nextExpanded, finalKey)) {
|
|
641
|
+
delete nextExpanded[finalKey]
|
|
642
|
+
expandedChanged = true
|
|
643
|
+
}
|
|
644
|
+
// Input remains subordinate to Final and starts a new disclosure cycle;
|
|
645
|
+
// neither this reset nor suppression touches drafts, Todo, or Goal.
|
|
646
|
+
const inputKey = `${task.id}:input`
|
|
647
|
+
if (Object.hasOwn(nextExpanded, inputKey)) {
|
|
648
|
+
delete nextExpanded[inputKey]
|
|
649
|
+
expandedChanged = true
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
cycles: cyclesChanged ? nextCycles : cycles,
|
|
654
|
+
expanded: expandedChanged ? nextExpanded : value,
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function reconcileGoalExpansionCycles(cycles, goals, endedSessionIds, value) {
|
|
659
|
+
const nextCycles = { ...cycles }
|
|
660
|
+
const nextExpanded = { ...value }
|
|
661
|
+
let cyclesChanged = false
|
|
662
|
+
let expandedChanged = false
|
|
663
|
+
for (const sessionId of endedSessionIds) {
|
|
664
|
+
if (Object.hasOwn(nextCycles, sessionId)) {
|
|
665
|
+
delete nextCycles[sessionId]
|
|
666
|
+
cyclesChanged = true
|
|
667
|
+
}
|
|
668
|
+
const key = `${sessionId}:goal`
|
|
669
|
+
if (Object.hasOwn(nextExpanded, key)) {
|
|
670
|
+
delete nextExpanded[key]
|
|
671
|
+
expandedChanged = true
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (const goal of goals) {
|
|
675
|
+
if (nextCycles[goal.id] === goal.goalId) continue
|
|
676
|
+
nextCycles[goal.id] = goal.goalId
|
|
677
|
+
cyclesChanged = true
|
|
678
|
+
const key = `${goal.id}:goal`
|
|
679
|
+
if (!Object.hasOwn(nextExpanded, key)) continue
|
|
680
|
+
delete nextExpanded[key]
|
|
681
|
+
expandedChanged = true
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
cycles: cyclesChanged ? nextCycles : cycles,
|
|
685
|
+
expanded: expandedChanged ? nextExpanded : value,
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Progress-only changes do not start a new Todo disclosure lifecycle. */
|
|
690
|
+
export function todoExpansionCycle(task) {
|
|
691
|
+
if (task.todos.availability === 'unknown' || task.goal.availability === 'unknown') return undefined
|
|
692
|
+
return JSON.stringify([
|
|
693
|
+
task.goal.availability === 'ready' ? task.goal.id : null,
|
|
694
|
+
task.todos.availability === 'ready' ? task.todos.items.map((todo) => todo.content) : null,
|
|
695
|
+
])
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function reconcileTodoExpansionCycles(cycles, tasks, value) {
|
|
699
|
+
const nextCycles = { ...cycles }
|
|
700
|
+
const nextExpanded = { ...value }
|
|
701
|
+
let cyclesChanged = false
|
|
702
|
+
let expandedChanged = false
|
|
703
|
+
for (const task of tasks) {
|
|
704
|
+
const cycle = todoExpansionCycle(task)
|
|
705
|
+
// A temporarily unloaded projection is not the end of its lifecycle.
|
|
706
|
+
if (cycle === undefined || nextCycles[task.id] === cycle) continue
|
|
707
|
+
nextCycles[task.id] = cycle
|
|
708
|
+
cyclesChanged = true
|
|
709
|
+
const key = `${task.id}:todo`
|
|
710
|
+
if (!Object.hasOwn(nextExpanded, key)) continue
|
|
711
|
+
delete nextExpanded[key]
|
|
712
|
+
expandedChanged = true
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
cycles: cyclesChanged ? nextCycles : cycles,
|
|
716
|
+
expanded: expandedChanged ? nextExpanded : value,
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function parseFinalMessageCache(raw) {
|
|
721
|
+
if (typeof raw !== 'string' || raw === '') return {}
|
|
722
|
+
try {
|
|
723
|
+
const parsed = JSON.parse(raw)
|
|
724
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
725
|
+
const states = {}
|
|
726
|
+
for (const [sessionId, entry] of Object.entries(parsed).slice(-100)) {
|
|
727
|
+
if (entry === null || typeof entry !== 'object' || entry.status !== 'ready') continue
|
|
728
|
+
if (!['string', 'number'].includes(typeof entry.updatedAt)) continue
|
|
729
|
+
const value = entry.value
|
|
730
|
+
if (value === null || typeof value !== 'object' || value.sessionId !== sessionId) continue
|
|
731
|
+
if (value.final !== null && (typeof value.final !== 'object' || typeof value.final.text !== 'string' || [...value.final.text].length > 4000)) continue
|
|
732
|
+
states[sessionId] = { status: 'ready', updatedAt: entry.updatedAt, value }
|
|
733
|
+
}
|
|
734
|
+
return states
|
|
735
|
+
} catch {
|
|
736
|
+
return {}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function serializeFinalMessageCache(states) {
|
|
741
|
+
const ready = Object.entries(states).filter(([, entry]) => entry?.status === 'ready').slice(-100)
|
|
742
|
+
return JSON.stringify(Object.fromEntries(ready))
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function loadFinalMessageCache() {
|
|
746
|
+
try { return parseFinalMessageCache(localStorage.getItem(FINAL_CACHE_KEY) ?? '') } catch { return {} }
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function parseExpansionRecord(raw) {
|
|
750
|
+
try {
|
|
751
|
+
const parsed = JSON.parse(raw ?? '{}')
|
|
752
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
753
|
+
return Object.fromEntries(Object.entries(parsed).filter(([entryKey, expanded]) => entryKey.length > 0 && typeof expanded === 'boolean').slice(-300))
|
|
754
|
+
} catch {
|
|
755
|
+
return {}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Desktop's dsh-app renderer uses its isolated Electron user-data storage.
|
|
760
|
+
// Host Config remains the owner of defaults; this is only per-client disclosure.
|
|
761
|
+
// Accessing the localStorage property itself may throw, so guard the entire read.
|
|
762
|
+
export function loadStoredExpansion(key, legacyKey) {
|
|
763
|
+
try {
|
|
764
|
+
if (typeof localStorage === 'undefined') return {}
|
|
765
|
+
const current = localStorage.getItem(key)
|
|
766
|
+
if (current !== null) return parseExpansionRecord(current)
|
|
767
|
+
if (legacyKey === undefined) return {}
|
|
768
|
+
const legacy = parseExpansionRecord(localStorage.getItem(legacyKey))
|
|
769
|
+
return Object.fromEntries(Object.entries(legacy).filter(([entryKey, expanded]) => entryKey.endsWith(':todo') && expanded === true))
|
|
770
|
+
} catch {
|
|
771
|
+
return {}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function useStoredExpansion(key, legacyKey) {
|
|
776
|
+
const React = requireModule('react')
|
|
777
|
+
const [value, setValue] = React.useState(() => loadStoredExpansion(key, legacyKey))
|
|
778
|
+
React.useEffect(() => {
|
|
779
|
+
writeStorage(key, JSON.stringify(value))
|
|
780
|
+
}, [key, value])
|
|
781
|
+
return [value, setValue]
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function useStoredRecord(key) {
|
|
785
|
+
const React = requireModule('react')
|
|
786
|
+
const [value, setValue] = React.useState(() => {
|
|
787
|
+
try {
|
|
788
|
+
if (typeof localStorage === 'undefined') return {}
|
|
789
|
+
const parsed = JSON.parse(localStorage.getItem(key) ?? '{}')
|
|
790
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
791
|
+
// Keep at least as many cycle markers as bounded disclosure entries; a
|
|
792
|
+
// missing marker must not reset a surviving manual choice on refresh.
|
|
793
|
+
return Object.fromEntries(Object.entries(parsed).filter(([entryKey, cycle]) => entryKey.length > 0 && ['string', 'number'].includes(typeof cycle)).slice(-300))
|
|
794
|
+
} catch {
|
|
795
|
+
return {}
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
React.useEffect(() => {
|
|
799
|
+
writeStorage(key, JSON.stringify(value))
|
|
800
|
+
}, [key, value])
|
|
801
|
+
return [value, setValue]
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function useProjectAppearanceSnapshot() {
|
|
805
|
+
const React = requireModule('react')
|
|
806
|
+
const [state, setState] = React.useState({ status: 'loading', items: [], icons: [], colorKeys: [] })
|
|
807
|
+
const [revision, setRevision] = React.useState(0)
|
|
808
|
+
React.useEffect(() => {
|
|
809
|
+
let alive = true
|
|
810
|
+
loadProjectAppearances((url, options) => fetch(url, options)).then((next) => {
|
|
811
|
+
if (alive) setState(next)
|
|
812
|
+
})
|
|
813
|
+
return () => { alive = false }
|
|
814
|
+
}, [revision])
|
|
815
|
+
React.useEffect(() => {
|
|
816
|
+
if (typeof window === 'undefined') return
|
|
817
|
+
const reload = () => setRevision((value) => value + 1)
|
|
818
|
+
window.addEventListener('project-appearance-changed', reload)
|
|
819
|
+
return () => window.removeEventListener('project-appearance-changed', reload)
|
|
820
|
+
}, [])
|
|
821
|
+
return state
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function renderCatalogIcon(icon, size = 18) {
|
|
825
|
+
if (icon === undefined) return element(requireModule('@deepseek-ai/dsh-client-ui-primitives').IconFolderCloseRegular, { size })
|
|
826
|
+
return element('svg', {
|
|
827
|
+
viewBox: '0 0 24 24', width: size, height: size, fill: 'none', stroke: 'currentColor',
|
|
828
|
+
strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true',
|
|
829
|
+
}, ...icon.nodes.map(([tag, props], index) => element(tag, { ...props, key: `${tag}-${index}` })))
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Resolve verified 0.2 Regular exports lazily; never draw approximate replacement glyphs. */
|
|
833
|
+
function nativeIcon(exportName, defaultSize = 14) {
|
|
834
|
+
return function NativeIcon({ size = defaultSize, ...rest }) {
|
|
835
|
+
const Icon = requireModule('@deepseek-ai/dsh-client-ui-primitives')[exportName]
|
|
836
|
+
return element(Icon, { size, ...rest })
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const IconPanelLeftOutlineRegular = nativeIcon('IconPanelLeftOutlineRegular', 16)
|
|
841
|
+
const IconChecklistOutlineRegular = nativeIcon('IconChecklistOutlineRegular')
|
|
842
|
+
const IconChevronDownOutlineRegular = nativeIcon('IconChevronDownOutlineRegular')
|
|
843
|
+
const IconChevronRightOutlineRegular = nativeIcon('IconChevronRightOutlineRegular')
|
|
844
|
+
const IconGoalOutlineRegular = nativeIcon('IconGoalOutlineRegular')
|
|
845
|
+
const IconSparkleRegular = nativeIcon('IconSparkleRegular')
|
|
846
|
+
|
|
847
|
+
function renderOfficialPinIcon(size = 14) {
|
|
848
|
+
const { IconPinFillRegular } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
849
|
+
return element(IconPinFillRegular, { size })
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function AppearanceDialog({ workspace, appearance, appearanceState, onClose, t }) {
|
|
853
|
+
const React = requireModule('react')
|
|
854
|
+
const [query, setQuery] = React.useState('')
|
|
855
|
+
const [iconKey, setIconKey] = React.useState(appearance?.iconKey ?? 'folder')
|
|
856
|
+
const [colorKey, setColorKey] = React.useState(appearance?.colorKey ?? 'blue')
|
|
857
|
+
const [busy, setBusy] = React.useState(false)
|
|
858
|
+
const [error, setError] = React.useState('')
|
|
859
|
+
const colors = appearanceState.colorKeys.length > 0 ? appearanceState.colorKeys : Object.keys(COLOR_HEX)
|
|
860
|
+
const needle = query.trim().toLowerCase()
|
|
861
|
+
const icons = appearanceState.icons.filter((icon) => needle === '' || String(icon.label ?? icon.key).toLowerCase().includes(needle)).slice(0, 160)
|
|
862
|
+
|
|
863
|
+
const save = async () => {
|
|
864
|
+
setBusy(true)
|
|
865
|
+
setError('')
|
|
866
|
+
try {
|
|
867
|
+
const response = await fetch('/api/project-appearance', {
|
|
868
|
+
method: 'PUT',
|
|
869
|
+
headers: { 'content-type': 'application/json' },
|
|
870
|
+
body: JSON.stringify({ workspaceId: workspace.workspaceId, iconKey, colorKey }),
|
|
871
|
+
})
|
|
872
|
+
if (!response.ok) throw new Error(`save failed: ${response.status}`)
|
|
873
|
+
if (typeof window !== 'undefined') window.dispatchEvent(new Event('project-appearance-changed'))
|
|
874
|
+
onClose()
|
|
875
|
+
} catch (cause) {
|
|
876
|
+
setError(String(cause?.message ?? cause))
|
|
877
|
+
setBusy(false)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return element('div', { className: 'dbt-dialog-backdrop', role: 'presentation', onMouseDown: (event) => { if (event.target === event.currentTarget) onClose() } },
|
|
882
|
+
element('section', { className: 'dbt-dialog', role: 'dialog', 'aria-modal': 'true', 'aria-label': t('tasks.appearanceTitle') },
|
|
883
|
+
element('h2', null, `${t('tasks.appearanceTitle')} · ${workspace.title}`),
|
|
884
|
+
element('input', {
|
|
885
|
+
value: query,
|
|
886
|
+
placeholder: t('tasks.iconSearch'),
|
|
887
|
+
'aria-label': t('tasks.iconSearch'),
|
|
888
|
+
onChange: (event) => setQuery(event.target.value),
|
|
889
|
+
}),
|
|
890
|
+
element('div', { className: 'dbt-icon-grid' }, ...icons.map((icon) => element('button', {
|
|
891
|
+
key: icon.key,
|
|
892
|
+
type: 'button',
|
|
893
|
+
className: 'dbt-icon-choice',
|
|
894
|
+
'data-selected': String(icon.key === iconKey),
|
|
895
|
+
title: icon.label ?? icon.key,
|
|
896
|
+
'aria-label': icon.label ?? icon.key,
|
|
897
|
+
onClick: () => setIconKey(icon.key),
|
|
898
|
+
}, renderCatalogIcon(icon, 21)))),
|
|
899
|
+
element('div', { className: 'dbt-color-row', 'aria-label': 'Colors' }, ...colors.map((key) => element('button', {
|
|
900
|
+
key,
|
|
901
|
+
type: 'button',
|
|
902
|
+
className: 'dbt-color-choice',
|
|
903
|
+
'data-selected': String(key === colorKey),
|
|
904
|
+
style: { background: COLOR_HEX[key] ?? '#64748b', color: COLOR_HEX[key] ?? '#64748b' },
|
|
905
|
+
title: key,
|
|
906
|
+
'aria-label': key,
|
|
907
|
+
onClick: () => setColorKey(key),
|
|
908
|
+
}))),
|
|
909
|
+
error !== '' ? element('div', { className: 'dbt-task-warning', role: 'alert' }, error) : null,
|
|
910
|
+
element('div', { className: 'dbt-dialog-actions' },
|
|
911
|
+
element('button', { type: 'button', onClick: onClose, disabled: busy }, t('tasks.cancel')),
|
|
912
|
+
element('button', { type: 'button', className: 'dbt-dialog-save', onClick: save, disabled: busy || icons.length === 0 }, t('tasks.save')))))
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function availabilityLabel(value, t) {
|
|
916
|
+
if (value.availability === 'unknown') return t('tasks.unknown')
|
|
917
|
+
if (value.availability === 'none') return t('tasks.none')
|
|
918
|
+
return ''
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function compactRelativeTime(updatedAt, now, t, relativeTime) {
|
|
922
|
+
const { unit, n } = relativeTime(updatedAt, now)
|
|
923
|
+
return unit === 'now' ? t('time.now') : t(`time.${unit}`, { n })
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function resolveTaskContinueText(draft, defaultText) {
|
|
927
|
+
return draft.trim() === '' ? defaultText : draft
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export function taskContinuePlaceholder(draft, focused, defaultText) {
|
|
931
|
+
return !focused && draft === '' ? defaultText : ''
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function TaskContinueInput({ sessionId, active, createTaskInput, onWarning, t }) {
|
|
935
|
+
const React = requireModule('react')
|
|
936
|
+
const [activated, setActivated] = React.useState(active)
|
|
937
|
+
const [surface, setSurface] = React.useState(null)
|
|
938
|
+
const [error, setError] = React.useState('')
|
|
939
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
940
|
+
const warning = React.useRef(onWarning)
|
|
941
|
+
warning.current = onWarning
|
|
942
|
+
// Hidden, never-opened cards must not open/retain every background Session.
|
|
943
|
+
React.useEffect(() => { if (active) setActivated(true) }, [active])
|
|
944
|
+
React.useEffect(() => {
|
|
945
|
+
if (!activated) return undefined
|
|
946
|
+
const controller = new AbortController()
|
|
947
|
+
let owned
|
|
948
|
+
setSurface(null)
|
|
949
|
+
setError('')
|
|
950
|
+
Promise.resolve().then(() => createTaskInput(sessionId, {
|
|
951
|
+
signal: controller.signal,
|
|
952
|
+
onWarning: (text) => warning.current?.(text),
|
|
953
|
+
})).then((handle) => {
|
|
954
|
+
owned = handle
|
|
955
|
+
if (controller.signal.aborted) handle.dispose()
|
|
956
|
+
else setSurface(handle)
|
|
957
|
+
}).catch((cause) => {
|
|
958
|
+
if (!controller.signal.aborted) setError(String(cause?.message ?? cause))
|
|
959
|
+
})
|
|
960
|
+
return () => { controller.abort(); owned?.dispose() }
|
|
961
|
+
}, [activated, sessionId, createTaskInput, attempt])
|
|
962
|
+
return element('div', { className: 'dbt-continue-host', 'data-no-card-drag': true, draggable: false,
|
|
963
|
+
onDragStart: (event) => event.preventDefault(),
|
|
964
|
+
onClick: (event) => {
|
|
965
|
+
if (event.target?.closest?.('button,a,input,textarea,select,[role="dialog"],[role="listbox"],[role="option"],[data-input-popup],[contenteditable="true"]')) return
|
|
966
|
+
if (active) surface?.input.focus()
|
|
967
|
+
},
|
|
968
|
+
}, surface === null ? (error === ''
|
|
969
|
+
? activated ? element('div', { role: 'status' }, t('tasks.inputLoading')) : null
|
|
970
|
+
: element('div', { className: 'dbt-task-warning', role: 'alert' }, error,
|
|
971
|
+
element('button', { type: 'button', onClick: () => setAttempt(value => value + 1) }, t('tasks.retryInput'))))
|
|
972
|
+
: element(surface.View, {
|
|
973
|
+
className: 'dbt-continue-form', disabled: !active,
|
|
974
|
+
placeholder: t('tasks.continueDefault'), defaultText: t('tasks.continueDefault'), submitLabel: t('tasks.send'),
|
|
975
|
+
inputLabel: t('tasks.continue'), addLabel: t('tasks.add'), searchLabel: t('tasks.inputSearch'),
|
|
976
|
+
emptyLabel: t('tasks.inputEmpty'), errorLabel: t('tasks.inputCatalogError'),
|
|
977
|
+
}))
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function TaskQuestionCard({ target, createTaskQuestion, fontSize, revealQuestion, canRevealQuestion, t }) {
|
|
981
|
+
const React = requireModule('react')
|
|
982
|
+
const [handle, setHandle] = React.useState(null)
|
|
983
|
+
const [error, setError] = React.useState('')
|
|
984
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
985
|
+
React.useEffect(() => {
|
|
986
|
+
const controller = new AbortController()
|
|
987
|
+
let owned
|
|
988
|
+
setHandle(null); setError('')
|
|
989
|
+
Promise.resolve().then(() => createTaskQuestion({
|
|
990
|
+
sessionId: target.sessionId, requestKey: target.requestKey, callId: target.callId, signal: controller.signal,
|
|
991
|
+
})).then(value => {
|
|
992
|
+
owned = value
|
|
993
|
+
if (controller.signal.aborted) value.dispose()
|
|
994
|
+
else setHandle(value)
|
|
995
|
+
}).catch(cause => { if (!controller.signal.aborted) setError(String(cause?.message ?? cause)) })
|
|
996
|
+
return () => { controller.abort(); owned?.dispose() }
|
|
997
|
+
}, [target.sessionId, target.requestKey, target.callId, createTaskQuestion, attempt])
|
|
998
|
+
const fallback = () => typeof target.callId === 'string' && canRevealQuestion()
|
|
999
|
+
? element('button', { type: 'button', className: 'dbt-task-answer', onClick: event => {
|
|
1000
|
+
event.stopPropagation()
|
|
1001
|
+
if (revealQuestion(target.sessionId, target.callId) === false) setError(t('tasks.answerFailed'))
|
|
1002
|
+
} }, t('tasks.answer')) : null
|
|
1003
|
+
if (handle !== null) return element(TaskQuestionView, { handle, fontSize, fallback, t, retry: () => setAttempt(value => value + 1) })
|
|
1004
|
+
if (error === '') return element('div', { role: 'status', className: 'dbt-question-notice' }, t('tasks.questionLoading'))
|
|
1005
|
+
return element('div', { role: 'alert', className: 'dbt-question-notice' }, error,
|
|
1006
|
+
element('button', { type: 'button', className: 'dbt-task-answer', onClick: () => setAttempt(value => value + 1) }, t('tasks.retryInput')), fallback())
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function TaskQuestionView({ handle, fontSize, fallback, t, retry }) {
|
|
1010
|
+
const React = requireModule('react')
|
|
1011
|
+
const state = React.useSyncExternalStore(listener => handle.state.subscribe(listener), () => handle.state.getSnapshot())
|
|
1012
|
+
if (state.phase === 'closed') return null
|
|
1013
|
+
if (state.phase === 'unsupported') return fallback()
|
|
1014
|
+
if (state.phase === 'error') return element('div', { className: 'dbt-question-notice', role: 'alert' }, state.message ?? t('tasks.answerFailed'),
|
|
1015
|
+
element('button', { type: 'button', className: 'dbt-task-answer', onClick: retry }, t('tasks.retryInput')), fallback())
|
|
1016
|
+
if (state.phase === 'waiting') return element('div', { className: 'dbt-question-notice', role: 'status' }, t('tasks.questionLoading'), fallback())
|
|
1017
|
+
return element(handle.View, { fontSize })
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function TaskActionDialog({ action, onClose, onSubmit, t }) {
|
|
1021
|
+
const React = requireModule('react')
|
|
1022
|
+
const { Modal, Button } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1023
|
+
const [draft, setDraft] = React.useState(action.title)
|
|
1024
|
+
const [busy, setBusy] = React.useState(false)
|
|
1025
|
+
const [error, setError] = React.useState('')
|
|
1026
|
+
const composing = React.useRef(false)
|
|
1027
|
+
const selectRenameInput = React.useCallback((node) => {
|
|
1028
|
+
if (node === null) return
|
|
1029
|
+
node.focus()
|
|
1030
|
+
node.select()
|
|
1031
|
+
}, [])
|
|
1032
|
+
const close = () => { if (!busy) onClose() }
|
|
1033
|
+
const submit = async () => {
|
|
1034
|
+
if (busy || draft.trim() === '') return
|
|
1035
|
+
setBusy(true)
|
|
1036
|
+
setError('')
|
|
1037
|
+
try { await onSubmit(action, draft); onClose() }
|
|
1038
|
+
catch (cause) { setError(String(cause?.message ?? cause)); setBusy(false) }
|
|
1039
|
+
}
|
|
1040
|
+
const inputProps = {
|
|
1041
|
+
className: 'dbt-action-input dbt-rename-input',
|
|
1042
|
+
value: draft,
|
|
1043
|
+
'aria-label': t('tasks.rename'),
|
|
1044
|
+
ref: selectRenameInput,
|
|
1045
|
+
disabled: busy,
|
|
1046
|
+
onChange: (event) => { setDraft(event.target.value); setError('') },
|
|
1047
|
+
onCompositionStart: () => { composing.current = true },
|
|
1048
|
+
onCompositionEnd: () => { composing.current = false },
|
|
1049
|
+
onKeyDown: (event) => {
|
|
1050
|
+
if (event.key === 'Enter' && !event.shiftKey && !composing.current && !event.nativeEvent?.isComposing) {
|
|
1051
|
+
event.preventDefault()
|
|
1052
|
+
submit()
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
}
|
|
1056
|
+
return element(Modal, {
|
|
1057
|
+
open: true,
|
|
1058
|
+
onClose: close,
|
|
1059
|
+
closeLabel: t('tasks.cancel'),
|
|
1060
|
+
title: t('tasks.rename'),
|
|
1061
|
+
footer: element(React.Fragment, null,
|
|
1062
|
+
element(Button, { variant: 'outline', disabled: busy, onClick: close }, t('tasks.cancel')),
|
|
1063
|
+
element(Button, { variant: 'primary', disabled: busy || draft.trim() === '', onClick: submit }, t('tasks.renameAction'))),
|
|
1064
|
+
},
|
|
1065
|
+
element('input', inputProps),
|
|
1066
|
+
error === '' ? null : element('div', { className: 'dbt-task-warning', role: 'alert' }, error))
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export function shouldToggleFinalInput(target) {
|
|
1070
|
+
return !target?.closest?.('a')
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
export function isTaskCardFinalShortcutTarget(target) {
|
|
1074
|
+
return !target?.closest?.('[data-no-card-drag],button,a,input,textarea,select,[contenteditable="true"],[role="button"],[role="switch"]')
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** A card gesture reads only the existing bounded, pinned-only Final endpoint. */
|
|
1078
|
+
export function createCardFinalExpansionController({ getTask, getCached, isExpanded, readFinal, onReady, expand, collapse }) {
|
|
1079
|
+
const pending = new Map()
|
|
1080
|
+
let disposed = false
|
|
1081
|
+
const eligible = (task) => task?.session.status === 'idle'
|
|
1082
|
+
const validPayload = (id, value) => value?.sessionId === id &&
|
|
1083
|
+
(value.final === null || typeof value.final?.text === 'string')
|
|
1084
|
+
const hasFinal = (value) => typeof value.final?.text === 'string' && value.final.text.trim() !== ''
|
|
1085
|
+
const cancel = (id) => {
|
|
1086
|
+
const request = pending.get(id)
|
|
1087
|
+
if (request === undefined) return
|
|
1088
|
+
pending.delete(id)
|
|
1089
|
+
request.controller.abort()
|
|
1090
|
+
}
|
|
1091
|
+
const current = (request) => {
|
|
1092
|
+
const task = getTask(request.id)
|
|
1093
|
+
return !disposed && !request.controller.signal.aborted && pending.get(request.id) === request &&
|
|
1094
|
+
eligible(task) && task.updatedAt === request.updatedAt
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
request(id) {
|
|
1098
|
+
const task = getTask(id)
|
|
1099
|
+
if (disposed || !eligible(task)) return Promise.resolve(false)
|
|
1100
|
+
if (isExpanded(id)) {
|
|
1101
|
+
// Closing is immediate, even while a preview is loading or failed.
|
|
1102
|
+
// Invalidate an older read so its late response cannot reopen Final.
|
|
1103
|
+
cancel(id)
|
|
1104
|
+
collapse(id)
|
|
1105
|
+
return Promise.resolve(true)
|
|
1106
|
+
}
|
|
1107
|
+
const prior = pending.get(id)
|
|
1108
|
+
if (prior?.updatedAt === task.updatedAt) return prior.promise
|
|
1109
|
+
cancel(id)
|
|
1110
|
+
const cached = getCached(id)
|
|
1111
|
+
if (cached?.status === 'ready' && cached.updatedAt === task.updatedAt && validPayload(id, cached.value)) {
|
|
1112
|
+
if (!hasFinal(cached.value)) return Promise.resolve(false)
|
|
1113
|
+
expand(id)
|
|
1114
|
+
return Promise.resolve(true)
|
|
1115
|
+
}
|
|
1116
|
+
const request = { id, updatedAt: task.updatedAt, controller: new AbortController() }
|
|
1117
|
+
pending.set(id, request)
|
|
1118
|
+
request.promise = Promise.resolve().then(() => {
|
|
1119
|
+
if (!current(request)) return undefined
|
|
1120
|
+
return readFinal(request, request.controller.signal)
|
|
1121
|
+
}).then((value) => {
|
|
1122
|
+
if (!current(request) || !validPayload(id, value)) return false
|
|
1123
|
+
onReady(request, value)
|
|
1124
|
+
if (!hasFinal(value)) return false
|
|
1125
|
+
// This asynchronous path only opens; it never toggles a newer choice.
|
|
1126
|
+
expand(id)
|
|
1127
|
+
return true
|
|
1128
|
+
}).catch(() => false).finally(() => {
|
|
1129
|
+
if (pending.get(id) === request) pending.delete(id)
|
|
1130
|
+
})
|
|
1131
|
+
return request.promise
|
|
1132
|
+
},
|
|
1133
|
+
cancel,
|
|
1134
|
+
reconcile() {
|
|
1135
|
+
for (const [id, request] of pending) if (!current(request)) cancel(id)
|
|
1136
|
+
},
|
|
1137
|
+
dispose() {
|
|
1138
|
+
disposed = true
|
|
1139
|
+
for (const id of pending.keys()) cancel(id)
|
|
1140
|
+
},
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const FINAL_DOUBLE_CLICK_WAIT_MS = 300
|
|
1145
|
+
|
|
1146
|
+
export function createFinalClickController(schedule = (callback, ms) => window.setTimeout(callback, ms), cancelTimer = (timer) => window.clearTimeout(timer), feedback = () => {}) {
|
|
1147
|
+
const pending = new Map()
|
|
1148
|
+
const cancel = (id) => {
|
|
1149
|
+
const timer = pending.get(id)
|
|
1150
|
+
if (timer === undefined) return
|
|
1151
|
+
cancelTimer(timer)
|
|
1152
|
+
pending.delete(id)
|
|
1153
|
+
feedback(id, false)
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
click(id, action) {
|
|
1157
|
+
const prior = pending.get(id)
|
|
1158
|
+
if (prior !== undefined) cancelTimer(prior)
|
|
1159
|
+
else feedback(id, true)
|
|
1160
|
+
const timer = schedule(() => {
|
|
1161
|
+
pending.delete(id)
|
|
1162
|
+
feedback(id, false)
|
|
1163
|
+
action()
|
|
1164
|
+
}, FINAL_DOUBLE_CLICK_WAIT_MS)
|
|
1165
|
+
pending.set(id, timer)
|
|
1166
|
+
},
|
|
1167
|
+
doubleClick(id, action) {
|
|
1168
|
+
if (!pending.has(id)) return false
|
|
1169
|
+
cancel(id)
|
|
1170
|
+
action()
|
|
1171
|
+
return true
|
|
1172
|
+
},
|
|
1173
|
+
cancel,
|
|
1174
|
+
dispose() {
|
|
1175
|
+
for (const timer of pending.values()) cancelTimer(timer)
|
|
1176
|
+
pending.clear()
|
|
1177
|
+
},
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function TaskSwimlane({ useSessions, useWorkspaces, useSessionStatus, useTaskConnection, usePanelInfo, useTaskPins, preferences, openSession, moveTaskBefore, unpinTask, revealQuestion, canRevealQuestion, findTaskQuestion, createTaskQuestion, twoColumns, t, submitTaskAction, createTaskInput }) {
|
|
1182
|
+
const React = requireModule('react')
|
|
1183
|
+
const {
|
|
1184
|
+
MarkdownText,
|
|
1185
|
+
StateDot,
|
|
1186
|
+
relativeTime,
|
|
1187
|
+
} = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1188
|
+
const list = useSessions((snapshot) => snapshot)
|
|
1189
|
+
const panelActive = usePanelInfo((info) => info.activePanelId !== null)
|
|
1190
|
+
const workspaces = useWorkspaces((snapshot) => snapshot)
|
|
1191
|
+
// 0.2 carries the pending interaction on the Session status snapshot —
|
|
1192
|
+
// `Map<sessionId, { running, pendingInteraction, completionUnread }>`, reached
|
|
1193
|
+
// through the global standard prop every slot entry receives. The 0.1.5
|
|
1194
|
+
// pending-interaction hook was removed in 0.2, and reading it there yielded
|
|
1195
|
+
// `undefined` at render time: the TypeError retired this slot entry as an
|
|
1196
|
+
// abdicating crash and the whole sidebar column vanished.
|
|
1197
|
+
const sessionStatuses = useSessionStatus((snapshot) => snapshot)
|
|
1198
|
+
const connectionState = useTaskConnection((snapshot) => snapshot)
|
|
1199
|
+
const taskPins = useTaskPins((snapshot) => snapshot)
|
|
1200
|
+
const pendingInteractions = React.useMemo(() => {
|
|
1201
|
+
const next = new Map()
|
|
1202
|
+
// Metadata discovery is read-only; opening history is owned by an actual card.
|
|
1203
|
+
for (const id of taskPins.queue.sessionIds) {
|
|
1204
|
+
const target = findTaskQuestion?.(id)
|
|
1205
|
+
if (target !== undefined) next.set(id, { ...target, key: target.requestKey })
|
|
1206
|
+
}
|
|
1207
|
+
for (const [id, status] of sessionStatuses) {
|
|
1208
|
+
const pending = status?.pendingInteraction
|
|
1209
|
+
if (pending != null) next.set(id, pending)
|
|
1210
|
+
}
|
|
1211
|
+
return next
|
|
1212
|
+
}, [sessionStatuses, list, taskPins.queue.sessionIds, findTaskQuestion])
|
|
1213
|
+
const appearanceState = useProjectAppearanceSnapshot()
|
|
1214
|
+
const [editingWorkspaceId, setEditingWorkspaceId] = React.useState(null)
|
|
1215
|
+
const [taskAction, setTaskAction] = React.useState(null)
|
|
1216
|
+
const [storedExpanded, setExpanded] = useStoredExpansion('dsh-better-tasks.projection-expansion.v2', 'dsh-better-tasks.projection-expansion.v1')
|
|
1217
|
+
const [finalAutoCycles, setFinalAutoCycles] = useStoredRecord('dsh-better-tasks.final-auto-open-cycle.v1')
|
|
1218
|
+
const [goalAutoCycles, setGoalAutoCycles] = useStoredRecord('dsh-better-tasks.goal-auto-open-cycle.v1')
|
|
1219
|
+
const [todoAutoCycles, setTodoAutoCycles] = useStoredRecord('dsh-better-tasks.todo-expansion-cycle.v1')
|
|
1220
|
+
const [drag, setDrag] = React.useState(null)
|
|
1221
|
+
const dragClickGuard = React.useRef(null)
|
|
1222
|
+
const dragOriginGuard = React.useRef(null)
|
|
1223
|
+
const [pendingFinalIds, setPendingFinalIds] = React.useState(() => new Set())
|
|
1224
|
+
const finalClicks = React.useRef(null)
|
|
1225
|
+
if (finalClicks.current === null) finalClicks.current = createFinalClickController(undefined, undefined, (id, active) => {
|
|
1226
|
+
setPendingFinalIds((current) => {
|
|
1227
|
+
if (current.has(id) === active) return current
|
|
1228
|
+
const next = new Set(current)
|
|
1229
|
+
if (active) next.add(id)
|
|
1230
|
+
else next.delete(id)
|
|
1231
|
+
return next
|
|
1232
|
+
})
|
|
1233
|
+
})
|
|
1234
|
+
React.useEffect(() => () => finalClicks.current.dispose(), [])
|
|
1235
|
+
const [moveError, setMoveError] = React.useState('')
|
|
1236
|
+
const [actionWarning, setActionWarning] = React.useState('')
|
|
1237
|
+
|
|
1238
|
+
const tasks = derivePinnedTasks({
|
|
1239
|
+
list,
|
|
1240
|
+
panelActive,
|
|
1241
|
+
pinnedSessionIds: taskPins.queue.sessionIds,
|
|
1242
|
+
pendingInteractions,
|
|
1243
|
+
sessionStatuses,
|
|
1244
|
+
})
|
|
1245
|
+
const taskIds = tasks.map((task) => task.id)
|
|
1246
|
+
const goalTasks = tasks.filter((task) => task.goal.availability === 'ready' && typeof task.goal.id === 'string').map((task) => ({ id: task.id, goalId: task.goal.id }))
|
|
1247
|
+
const endedGoalTaskIds = tasks.filter((task) => task.goal.availability === 'none').map((task) => task.id)
|
|
1248
|
+
const goalCycleKey = `${goalTasks.map((task) => `${task.id}:${task.goalId}`).join('|')}::${endedGoalTaskIds.join('|')}`
|
|
1249
|
+
React.useEffect(() => {
|
|
1250
|
+
setGoalAutoCycles((cycles) => reconcileGoalExpansionCycles(cycles, goalTasks, endedGoalTaskIds, {}).cycles)
|
|
1251
|
+
setExpanded((value) => reconcileGoalExpansionCycles(goalAutoCycles, goalTasks, endedGoalTaskIds, value).expanded)
|
|
1252
|
+
}, [goalCycleKey])
|
|
1253
|
+
const todoCycleKey = JSON.stringify(tasks.map((task) => [task.id, todoExpansionCycle(task)]))
|
|
1254
|
+
React.useEffect(() => {
|
|
1255
|
+
setTodoAutoCycles((cycles) => reconcileTodoExpansionCycles(cycles, tasks, {}).cycles)
|
|
1256
|
+
// Functional updates preserve Goal/Final changes from this same commit.
|
|
1257
|
+
setExpanded((value) => reconcileTodoExpansionCycles(todoAutoCycles, tasks, value).expanded)
|
|
1258
|
+
}, [todoCycleKey])
|
|
1259
|
+
const idleTasks = tasks.filter((task) => task.session.status === 'idle').map((task) => ({ id: task.id, updatedAt: task.updatedAt, current: task.current }))
|
|
1260
|
+
const idleCycleKey = JSON.stringify(idleTasks)
|
|
1261
|
+
// Resolve BEFORE effects: display, lazy reads and manual gesture checks must
|
|
1262
|
+
// agree on current-cycle suppression, without a flash or an eager Final read.
|
|
1263
|
+
const expanded = expandFinalForIdleCycles(finalAutoCycles, idleTasks, storedExpanded).expanded
|
|
1264
|
+
React.useEffect(() => {
|
|
1265
|
+
const unseen = idleTasks.filter((task) => isNewFinalIdleCycle(finalAutoCycles, task))
|
|
1266
|
+
if (unseen.length === 0) return
|
|
1267
|
+
setFinalAutoCycles((cycles) => expandFinalForIdleCycles(cycles, unseen, {}).cycles)
|
|
1268
|
+
setExpanded((value) => expandFinalForIdleCycles(finalAutoCycles, unseen, value).expanded)
|
|
1269
|
+
}, [idleCycleKey])
|
|
1270
|
+
const [finalStates, setFinalStates] = React.useState(loadFinalMessageCache)
|
|
1271
|
+
React.useEffect(() => {
|
|
1272
|
+
writeStorage(FINAL_CACHE_KEY, serializeFinalMessageCache(finalStates))
|
|
1273
|
+
}, [finalStates])
|
|
1274
|
+
const finalRequests = tasks.filter((task) => {
|
|
1275
|
+
if (task.session.status !== 'idle' || !isProjectionExpanded(expanded, `${task.id}:final`, preferences.defaultFinalExpanded)) return false
|
|
1276
|
+
const cached = finalStates[task.id]
|
|
1277
|
+
return cached?.status !== 'ready' || cached.updatedAt !== task.updatedAt
|
|
1278
|
+
}).map((task) => ({ id: task.id, updatedAt: task.updatedAt }))
|
|
1279
|
+
const finalRequestKey = finalRequests.map((request) => `${request.id}:${request.updatedAt}`).join('|')
|
|
1280
|
+
const [finalRetry, setFinalRetry] = React.useState(0)
|
|
1281
|
+
React.useEffect(() => {
|
|
1282
|
+
if (finalRequests.length === 0) return undefined
|
|
1283
|
+
let disposed = false
|
|
1284
|
+
const controllers = finalRequests.map((request) => ({ request, controller: new AbortController() }))
|
|
1285
|
+
setFinalStates((current) => {
|
|
1286
|
+
const next = { ...current }
|
|
1287
|
+
for (const { request } of controllers) next[request.id] = { status: 'loading', updatedAt: request.updatedAt }
|
|
1288
|
+
return next
|
|
1289
|
+
})
|
|
1290
|
+
for (const { request, controller } of controllers) {
|
|
1291
|
+
fetch(`/api/better-tasks/final-message?sessionId=${encodeURIComponent(request.id)}`, { cache: 'no-store', signal: controller.signal })
|
|
1292
|
+
.then(async (response) => {
|
|
1293
|
+
const body = await response.json()
|
|
1294
|
+
if (!response.ok) throw new Error(body?.message ?? body?.code ?? `HTTP ${response.status}`)
|
|
1295
|
+
return body
|
|
1296
|
+
})
|
|
1297
|
+
.then((value) => {
|
|
1298
|
+
if (disposed) return
|
|
1299
|
+
setFinalStates((current) => current[request.id]?.updatedAt === request.updatedAt ? { ...current, [request.id]: { status: 'ready', updatedAt: request.updatedAt, value } } : current)
|
|
1300
|
+
})
|
|
1301
|
+
.catch((error) => {
|
|
1302
|
+
if (disposed || error?.name === 'AbortError') return
|
|
1303
|
+
setFinalStates((current) => current[request.id]?.updatedAt === request.updatedAt ? { ...current, [request.id]: { status: 'error', updatedAt: request.updatedAt, message: String(error?.message ?? error) } } : current)
|
|
1304
|
+
})
|
|
1305
|
+
}
|
|
1306
|
+
return () => {
|
|
1307
|
+
disposed = true
|
|
1308
|
+
for (const { controller } of controllers) controller.abort()
|
|
1309
|
+
}
|
|
1310
|
+
}, [finalRequestKey, finalRetry])
|
|
1311
|
+
const finalShortcutState = React.useRef(null)
|
|
1312
|
+
finalShortcutState.current = { tasks: new Map(tasks.map((task) => [task.id, task])), finalStates, expanded, preferences, finalAutoCycles }
|
|
1313
|
+
const cardFinalRequests = React.useRef(null)
|
|
1314
|
+
React.useEffect(() => {
|
|
1315
|
+
const changeFinalDisclosure = (id, visible) => {
|
|
1316
|
+
const snapshot = finalShortcutState.current
|
|
1317
|
+
const task = snapshot.tasks.get(id)
|
|
1318
|
+
if (task?.session.status !== 'idle') return
|
|
1319
|
+
finalClicks.current.cancel(id)
|
|
1320
|
+
const idleTask = { id, updatedAt: task.updatedAt, current: task.current }
|
|
1321
|
+
// Commit the same lifecycle and effective state the gesture observed;
|
|
1322
|
+
// otherwise a not-yet-persisted background cycle could erase manual close.
|
|
1323
|
+
setFinalAutoCycles((cycles) => expandFinalForIdleCycles(cycles, [idleTask], {}).cycles)
|
|
1324
|
+
setExpanded((current) => {
|
|
1325
|
+
const resolved = expandFinalForIdleCycles(snapshot.finalAutoCycles, [idleTask], current).expanded
|
|
1326
|
+
return resolved[`${id}:final`] === visible ? resolved : { ...resolved, [`${id}:final`]: visible }
|
|
1327
|
+
})
|
|
1328
|
+
}
|
|
1329
|
+
const operation = createCardFinalExpansionController({
|
|
1330
|
+
getTask: (id) => finalShortcutState.current.tasks.get(id),
|
|
1331
|
+
getCached: (id) => finalShortcutState.current.finalStates[id],
|
|
1332
|
+
isExpanded: (id) => isProjectionExpanded(finalShortcutState.current.expanded, `${id}:final`, finalShortcutState.current.preferences.defaultFinalExpanded),
|
|
1333
|
+
readFinal: async (request, signal) => {
|
|
1334
|
+
const response = await fetch(`/api/better-tasks/final-message?sessionId=${encodeURIComponent(request.id)}`, { cache: 'no-store', signal })
|
|
1335
|
+
const value = await response.json()
|
|
1336
|
+
if (!response.ok) throw new Error(value?.message ?? value?.code ?? `HTTP ${response.status}`)
|
|
1337
|
+
return value
|
|
1338
|
+
},
|
|
1339
|
+
onReady: (request, value) => setFinalStates((current) => {
|
|
1340
|
+
const task = finalShortcutState.current.tasks.get(request.id)
|
|
1341
|
+
if (task?.session.status !== 'idle' || task.updatedAt !== request.updatedAt) return current
|
|
1342
|
+
return { ...current, [request.id]: { status: 'ready', updatedAt: request.updatedAt, value } }
|
|
1343
|
+
}),
|
|
1344
|
+
expand: (id) => changeFinalDisclosure(id, true),
|
|
1345
|
+
collapse: (id) => changeFinalDisclosure(id, false),
|
|
1346
|
+
})
|
|
1347
|
+
cardFinalRequests.current = operation
|
|
1348
|
+
return () => {
|
|
1349
|
+
operation.dispose()
|
|
1350
|
+
if (cardFinalRequests.current === operation) cardFinalRequests.current = null
|
|
1351
|
+
}
|
|
1352
|
+
}, [])
|
|
1353
|
+
const finalShortcutKey = JSON.stringify(tasks.map((task) => [task.id, task.session.status, task.updatedAt]))
|
|
1354
|
+
React.useEffect(() => { cardFinalRequests.current?.reconcile() }, [finalShortcutKey])
|
|
1355
|
+
const workspaceBySession = indexWorkspacesBySession(workspaces.items)
|
|
1356
|
+
const appearanceByWorkspace = new Map(appearanceState.items.map((item) => [item.workspaceId, item]))
|
|
1357
|
+
const iconByKey = new Map(appearanceState.icons.map((icon) => [icon.key, icon]))
|
|
1358
|
+
const editingWorkspace = workspaces.items.find((workspace) => workspace.workspaceId === editingWorkspaceId)
|
|
1359
|
+
const pendingMoves = new Set(taskPins.pendingSessionIds)
|
|
1360
|
+
const now = Date.now()
|
|
1361
|
+
const toggleProjection = (sessionId, key, defaultExpanded) => {
|
|
1362
|
+
if (key === 'final') cardFinalRequests.current?.cancel(sessionId)
|
|
1363
|
+
const disclosureKey = `${sessionId}:${key}`
|
|
1364
|
+
setExpanded((current) => {
|
|
1365
|
+
const resolved = key === 'final' || key === 'input'
|
|
1366
|
+
? expandFinalForIdleCycles(finalAutoCycles, idleTasks, current).expanded
|
|
1367
|
+
: current
|
|
1368
|
+
return { ...resolved, [disclosureKey]: !isProjectionExpanded(resolved, disclosureKey, defaultExpanded) }
|
|
1369
|
+
})
|
|
1370
|
+
}
|
|
1371
|
+
const commitDrop = (overId, half) => {
|
|
1372
|
+
if (drag === null || taskPins.writable !== true) return
|
|
1373
|
+
const sessionId = drag.sessionId
|
|
1374
|
+
const beforeSessionId = beforeIdForTaskDrop(taskIds, overId, half)
|
|
1375
|
+
setDrag(null)
|
|
1376
|
+
if (beforeSessionId === sessionId) return
|
|
1377
|
+
setMoveError('')
|
|
1378
|
+
moveTaskBefore(sessionId, beforeSessionId).catch((error) => setMoveError(String(error?.message ?? error)))
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return element('section', {
|
|
1382
|
+
className: 'dbt-task-view',
|
|
1383
|
+
'aria-label': t('view.tasks'),
|
|
1384
|
+
style: { '--dbt-projection-font-size': `${preferences.detailFontSize}px` },
|
|
1385
|
+
},
|
|
1386
|
+
appearanceState.status === 'unavailable' ? element('div', { className: 'dbt-task-warning', role: 'status' }, t('tasks.appearanceUnavailable')) : null,
|
|
1387
|
+
['initializing', 'running'].includes(taskPins.migration) ? element('div', { className: 'dbt-task-warning', role: 'status' }, t('tasks.pinMigration')) : null,
|
|
1388
|
+
taskPins.error !== null || moveError !== '' ? element('div', { className: 'dbt-task-warning', role: 'status' }, moveError || taskPins.error) : null,
|
|
1389
|
+
actionWarning === '' ? null : element('div', { className: 'dbt-task-warning', role: 'alert' }, actionWarning),
|
|
1390
|
+
tasks.length === 0 ? element('div', { className: 'dbt-task-empty' }, t('tasks.empty')) :
|
|
1391
|
+
element('div', {
|
|
1392
|
+
className: 'dbt-task-list',
|
|
1393
|
+
'data-columns': twoColumns ? '2' : '1',
|
|
1394
|
+
'data-title-emphasis': preferences.titleEmphasis,
|
|
1395
|
+
onDragOver: (event) => {
|
|
1396
|
+
if (drag === null || event.target !== event.currentTarget) return
|
|
1397
|
+
event.preventDefault()
|
|
1398
|
+
const lastId = taskIds.at(-1)
|
|
1399
|
+
if (lastId !== undefined && (drag.overId !== lastId || drag.half !== 'after')) setDrag({ ...drag, overId: lastId, half: 'after' })
|
|
1400
|
+
},
|
|
1401
|
+
onDrop: (event) => {
|
|
1402
|
+
if (drag === null || event.target !== event.currentTarget) return
|
|
1403
|
+
event.preventDefault()
|
|
1404
|
+
const lastId = taskIds.at(-1)
|
|
1405
|
+
if (lastId !== undefined) commitDrop(lastId, 'after')
|
|
1406
|
+
},
|
|
1407
|
+
}, ...((cards) => twoColumns ? [
|
|
1408
|
+
element('div', { key: 'column-even', className: 'dbt-task-column', 'data-column': 'even' }, ...cards.filter((_, index) => index % 2 === 0)),
|
|
1409
|
+
element('div', { key: 'column-odd', className: 'dbt-task-column', 'data-column': 'odd' }, ...cards.filter((_, index) => index % 2 === 1)),
|
|
1410
|
+
] : cards)(tasks.map((task) => {
|
|
1411
|
+
const workspace = workspaceBySession.get(task.id)
|
|
1412
|
+
const projectName = workspace?.title ?? task.cwd ?? t('tasks.unknown')
|
|
1413
|
+
const appearance = workspace === undefined ? undefined : appearanceByWorkspace.get(workspace.workspaceId)
|
|
1414
|
+
const icon = appearance === undefined ? undefined : iconByKey.get(appearance.iconKey)
|
|
1415
|
+
const iconColor = COLOR_HEX[appearance?.colorKey] ?? 'var(--dsw-alias-label-secondary)'
|
|
1416
|
+
const goalAvailable = task.goal.availability === 'ready'
|
|
1417
|
+
const todoAvailable = task.todos.availability === 'ready'
|
|
1418
|
+
const todoVisible = !todoAvailable || task.todos.total > 0
|
|
1419
|
+
const goalExpanded = goalAvailable && isProjectionExpanded(expanded, `${task.id}:goal`, preferences.defaultGoalExpanded)
|
|
1420
|
+
const todoExpanded = todoAvailable && task.todos.total > 0 && isProjectionExpanded(expanded, `${task.id}:todo`, preferences.defaultTodoExpanded)
|
|
1421
|
+
const finalExpanded = task.session.status === 'idle' && isProjectionExpanded(expanded, `${task.id}:final`, preferences.defaultFinalExpanded)
|
|
1422
|
+
const inputExpanded = finalExpanded && isProjectionExpanded(expanded, `${task.id}:input`, preferences.defaultInputExpanded)
|
|
1423
|
+
const finalState = finalStates[task.id]
|
|
1424
|
+
const pendingInteraction = pendingInteractions.get(task.id)
|
|
1425
|
+
const questionTarget = pendingInteraction?.kind === 'question' || pendingInteraction?.kind === 'plan-review'
|
|
1426
|
+
? { sessionId: task.id, kind: pendingInteraction.kind,
|
|
1427
|
+
requestKey: pendingInteraction.key ?? pendingInteraction.requestKey, callId: pendingInteraction.callId }
|
|
1428
|
+
: undefined
|
|
1429
|
+
const answerCallId = questionTarget?.callId
|
|
1430
|
+
const inlineQuestion = questionTarget?.kind === 'question' &&
|
|
1431
|
+
(typeof questionTarget.requestKey === 'string' || typeof answerCallId === 'string')
|
|
1432
|
+
const answerable = inlineQuestion || typeof answerCallId === 'string' && canRevealQuestion()
|
|
1433
|
+
const goalLabel = task.goal.availability === 'ready' ? `${task.goal.phase}: ${task.goal.objective}` : availabilityLabel(task.goal, t)
|
|
1434
|
+
const todoLabel = task.todos.availability === 'ready' ? `${task.todos.completed}/${task.todos.total}` : availabilityLabel(task.todos, t)
|
|
1435
|
+
const executionWarning = taskExecutionWarning({
|
|
1436
|
+
...list.byId[task.id],
|
|
1437
|
+
running: task.session.status === 'running',
|
|
1438
|
+
}, pendingInteraction, connectionState, list.phase === undefined || list.phase === 'ready')
|
|
1439
|
+
const warningLabel = executionWarning === null ? '' : t(`tasks.execution.${executionWarning.kind}`)
|
|
1440
|
+
const stateLabel = executionWarning === null
|
|
1441
|
+
? (task.session.status === 'block' ? t('tasks.blockAria') : task.session.status === 'running' ? t('tasks.runningAria') : t('tasks.idleAria'))
|
|
1442
|
+
: [warningLabel, executionWarning.detail].filter(Boolean).join('\n')
|
|
1443
|
+
const dropHalf = drag?.overId === task.id ? drag.half : undefined
|
|
1444
|
+
const detailRows = []
|
|
1445
|
+
if (goalExpanded) detailRows.push(element('div', {
|
|
1446
|
+
key: 'goal',
|
|
1447
|
+
className: 'dbt-projection-detail',
|
|
1448
|
+
'data-tone': task.goal.phase ?? task.goal.availability,
|
|
1449
|
+
title: goalLabel,
|
|
1450
|
+
'aria-label': `${t('tasks.goal')}: ${goalLabel}`,
|
|
1451
|
+
},
|
|
1452
|
+
element(IconGoalOutlineRegular, { size: 14 }),
|
|
1453
|
+
element('div', { className: 'dbt-projection-copy' }, task.goal.availability === 'ready' ?
|
|
1454
|
+
element(React.Fragment, null,
|
|
1455
|
+
element('div', null, task.goal.objective),
|
|
1456
|
+
task.goal.blockedReason === undefined ? null : element('div', { title: task.goal.blockedReason.code }, task.goal.blockedReason.message)) :
|
|
1457
|
+
availabilityLabel(task.goal, t))))
|
|
1458
|
+
if (todoExpanded) detailRows.push(element('div', {
|
|
1459
|
+
key: 'todo',
|
|
1460
|
+
className: 'dbt-projection-detail',
|
|
1461
|
+
title: todoLabel,
|
|
1462
|
+
'aria-label': `${t('tasks.todo')}: ${todoLabel}`,
|
|
1463
|
+
},
|
|
1464
|
+
element(IconChecklistOutlineRegular, { size: 14 }),
|
|
1465
|
+
element('div', { className: 'dbt-projection-copy' }, task.todos.availability === 'ready' ?
|
|
1466
|
+
element('ul', { className: 'dbt-todo-list' }, ...task.todos.items.map((todo, index) => element('li', {
|
|
1467
|
+
key: `${index}:${todo.content}`,
|
|
1468
|
+
className: 'dbt-todo-item',
|
|
1469
|
+
title: todo.status,
|
|
1470
|
+
},
|
|
1471
|
+
element('span', { className: 'dbt-todo-glyph', role: 'img', 'aria-label': t(todo.status === 'completed' ? 'tasks.todoCompleted' : todo.status === 'in_progress' ? 'tasks.todoInProgress' : 'tasks.todoPending') },
|
|
1472
|
+
element(StateDot, { state: todo.status === 'completed' ? 'done' : todo.status === 'in_progress' ? 'ongoing' : 'idle' })),
|
|
1473
|
+
element('span', null, todo.content)))) : availabilityLabel(task.todos, t))))
|
|
1474
|
+
if (finalExpanded) {
|
|
1475
|
+
let finalContent
|
|
1476
|
+
if (finalState === undefined || finalState.status === 'loading') {
|
|
1477
|
+
finalContent = t('tasks.finalLoading')
|
|
1478
|
+
} else if (finalState.status === 'error') {
|
|
1479
|
+
finalContent = t('tasks.finalError')
|
|
1480
|
+
} else if (finalState.value.final === null) {
|
|
1481
|
+
finalContent = t('tasks.finalNone')
|
|
1482
|
+
} else {
|
|
1483
|
+
finalContent = element(React.Fragment, null,
|
|
1484
|
+
element('div', { className: 'dbt-final-copy' }, element(MarkdownText, {
|
|
1485
|
+
text: finalState.value.final.text,
|
|
1486
|
+
labels: { code: { copyLabel: t('tasks.copyCode'), copiedLabel: t('tasks.copiedCode') }, footnotes: t('tasks.footnotes') },
|
|
1487
|
+
})),
|
|
1488
|
+
finalState.value.truncated ? element('div', { className: 'dbt-final-meta' }, `${t('tasks.finalTruncated')} · ${finalState.value.totalCodePoints}`) : null)
|
|
1489
|
+
}
|
|
1490
|
+
detailRows.push(element('div', {
|
|
1491
|
+
key: 'final',
|
|
1492
|
+
className: 'dbt-projection-detail dbt-final-detail',
|
|
1493
|
+
'data-feedback': pendingFinalIds.has(task.id) ? 'pending' : undefined,
|
|
1494
|
+
'data-tone': finalState?.status ?? 'loading',
|
|
1495
|
+
'aria-label': t('tasks.final'),
|
|
1496
|
+
}, element('div', {
|
|
1497
|
+
className: 'dbt-final-input-toggle', role: 'button', tabIndex: 0,
|
|
1498
|
+
'data-no-card-drag': true, draggable: false,
|
|
1499
|
+
'aria-expanded': inputExpanded,
|
|
1500
|
+
'aria-label': t(inputExpanded ? 'tasks.collapseInput' : 'tasks.expandInput'),
|
|
1501
|
+
onClickCapture: (event) => {
|
|
1502
|
+
if (!shouldToggleFinalInput(event.target)) return
|
|
1503
|
+
if (event.detail === 2 && finalClicks.current.doubleClick(task.id, () => toggleProjection(task.id, 'final', preferences.defaultFinalExpanded))) {
|
|
1504
|
+
event.preventDefault()
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
if (event.detail > 2) return
|
|
1508
|
+
finalClicks.current.click(task.id, () => toggleProjection(task.id, 'input', preferences.defaultInputExpanded))
|
|
1509
|
+
},
|
|
1510
|
+
onKeyDown: (event) => {
|
|
1511
|
+
if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return
|
|
1512
|
+
event.preventDefault()
|
|
1513
|
+
event.stopPropagation()
|
|
1514
|
+
finalClicks.current.cancel(task.id)
|
|
1515
|
+
toggleProjection(task.id, 'input', preferences.defaultInputExpanded)
|
|
1516
|
+
},
|
|
1517
|
+
}, element('div', { className: 'dbt-projection-copy' }, finalContent)),
|
|
1518
|
+
finalState?.status === 'error' ? element('button', {
|
|
1519
|
+
type: 'button', className: 'dbt-final-retry', 'data-no-card-drag': true, draggable: false,
|
|
1520
|
+
title: finalState.message,
|
|
1521
|
+
onClick: () => setFinalRetry((value) => value + 1),
|
|
1522
|
+
}, t('tasks.finalRetry')) : null))
|
|
1523
|
+
}
|
|
1524
|
+
return element('article', {
|
|
1525
|
+
key: task.id,
|
|
1526
|
+
className: 'dbt-task-card',
|
|
1527
|
+
'data-session-id': task.id,
|
|
1528
|
+
'data-current': String(task.current),
|
|
1529
|
+
'data-state': task.session.status,
|
|
1530
|
+
'data-execution-warning': executionWarning?.kind,
|
|
1531
|
+
'data-drop': dropHalf,
|
|
1532
|
+
'aria-label': `${projectName}: ${stateLabel}`,
|
|
1533
|
+
title: stateLabel,
|
|
1534
|
+
tabIndex: 0,
|
|
1535
|
+
draggable: taskPins.writable === true && !pendingMoves.has(task.id),
|
|
1536
|
+
onPointerDownCapture: (event) => {
|
|
1537
|
+
dragOriginGuard.current = event.target?.closest?.('[data-no-card-drag]') ? task.id : null
|
|
1538
|
+
},
|
|
1539
|
+
onKeyDown: (event) => {
|
|
1540
|
+
if (event.target !== event.currentTarget || !['Enter', ' '].includes(event.key)) return
|
|
1541
|
+
event.preventDefault()
|
|
1542
|
+
openSession(task.id)
|
|
1543
|
+
},
|
|
1544
|
+
onClick: (event) => {
|
|
1545
|
+
const fromControl = event.detail !== 0 && dragOriginGuard.current === task.id
|
|
1546
|
+
dragOriginGuard.current = null
|
|
1547
|
+
if (fromControl || event.target?.closest?.('[data-no-card-drag]') || dragClickGuard.current === task.id) return
|
|
1548
|
+
openSession(task.id)
|
|
1549
|
+
// The browser's second click is the double-click gesture. Keep the
|
|
1550
|
+
// first click immediate; do not re-open Final after its own body
|
|
1551
|
+
// double-click unmounts that control before a later dblclick event.
|
|
1552
|
+
if (event.detail === 2 && isTaskCardFinalShortcutTarget(event.target)) {
|
|
1553
|
+
void cardFinalRequests.current?.request(task.id)
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
onDragStart: (event) => {
|
|
1557
|
+
if (taskPins.writable !== true) { event.preventDefault(); return }
|
|
1558
|
+
if (dragOriginGuard.current === task.id || event.target?.closest?.('[data-no-card-drag]')) {
|
|
1559
|
+
dragOriginGuard.current = null
|
|
1560
|
+
event.preventDefault()
|
|
1561
|
+
return
|
|
1562
|
+
}
|
|
1563
|
+
dragClickGuard.current = task.id
|
|
1564
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
1565
|
+
event.dataTransfer.setData('text/plain', task.id)
|
|
1566
|
+
setDrag({ sessionId: task.id, overId: task.id, half: 'before' })
|
|
1567
|
+
},
|
|
1568
|
+
onDragEnd: () => {
|
|
1569
|
+
setDrag(null)
|
|
1570
|
+
window.setTimeout(() => { if (dragClickGuard.current === task.id) dragClickGuard.current = null }, 0)
|
|
1571
|
+
},
|
|
1572
|
+
onDragOver: (event) => {
|
|
1573
|
+
if (drag === null) return
|
|
1574
|
+
event.preventDefault()
|
|
1575
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
1576
|
+
const half = twoColumns ? (event.clientX < rect.left + rect.width / 2 ? 'before' : 'after') : (event.clientY < rect.top + rect.height / 2 ? 'before' : 'after')
|
|
1577
|
+
if (drag.overId !== task.id || drag.half !== half) setDrag({ ...drag, overId: task.id, half })
|
|
1578
|
+
},
|
|
1579
|
+
onDrop: (event) => {
|
|
1580
|
+
event.preventDefault()
|
|
1581
|
+
if (drag !== null) commitDrop(task.id, drag.overId === task.id ? drag.half : 'before')
|
|
1582
|
+
},
|
|
1583
|
+
},
|
|
1584
|
+
element('div', { className: 'dbt-task-title-row' },
|
|
1585
|
+
element('button', {
|
|
1586
|
+
type: 'button',
|
|
1587
|
+
className: 'dbt-project-icon',
|
|
1588
|
+
style: { color: iconColor },
|
|
1589
|
+
'data-no-card-drag': true,
|
|
1590
|
+
draggable: false,
|
|
1591
|
+
disabled: workspace === undefined || appearanceState.status !== 'ready',
|
|
1592
|
+
title: t('tasks.editAppearance'),
|
|
1593
|
+
'aria-label': t('tasks.editAppearance'),
|
|
1594
|
+
onClick: () => setEditingWorkspaceId(workspace?.workspaceId ?? null),
|
|
1595
|
+
}, renderCatalogIcon(icon, 18)),
|
|
1596
|
+
element('div', { className: 'dbt-task-open' },
|
|
1597
|
+
element('span', { className: 'dbt-task-project' }, projectName),
|
|
1598
|
+
element('span', { className: 'dbt-task-session-line' },
|
|
1599
|
+
executionWarning !== null ? element('svg', {
|
|
1600
|
+
className: 'dbt-task-error-icon', width: 14, height: 14, viewBox: '0 0 24 24',
|
|
1601
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: 2, 'aria-hidden': true,
|
|
1602
|
+
}, element('path', { d: 'M12 3 2 21h20L12 3Z', strokeLinejoin: 'round' }),
|
|
1603
|
+
element('path', { d: 'M12 9v5m0 3v1' })) :
|
|
1604
|
+
task.session.status === 'running' ? element(StateDot, { state: 'ongoing' }) : null,
|
|
1605
|
+
element('button', { type: 'button', className: 'dbt-task-rename', 'data-no-card-drag': true, draggable: false,
|
|
1606
|
+
title: t('tasks.rename'), 'aria-label': `${t('tasks.rename')}: ${task.title}`,
|
|
1607
|
+
onClick: () => setTaskAction({ kind: 'rename', id: task.id, title: task.title }),
|
|
1608
|
+
}, task.title),
|
|
1609
|
+
element('span', { className: 'dbt-task-time' }, compactRelativeTime(task.updatedAt, now, t, relativeTime)))),
|
|
1610
|
+
element('button', {
|
|
1611
|
+
type: 'button',
|
|
1612
|
+
className: 'dbt-task-unpin',
|
|
1613
|
+
'data-no-card-drag': true,
|
|
1614
|
+
draggable: false,
|
|
1615
|
+
disabled: pendingMoves.has(task.id),
|
|
1616
|
+
title: t('tasks.unpin'),
|
|
1617
|
+
'aria-label': t('tasks.unpin'),
|
|
1618
|
+
'aria-pressed': true,
|
|
1619
|
+
onDragStart: (event) => event.preventDefault(),
|
|
1620
|
+
onClick: () => {
|
|
1621
|
+
setMoveError('')
|
|
1622
|
+
unpinTask(task.id).catch((error) => setMoveError(String(error?.message ?? error)))
|
|
1623
|
+
},
|
|
1624
|
+
}, renderOfficialPinIcon(14))),
|
|
1625
|
+
element('div', { className: 'dbt-task-projection-bar' },
|
|
1626
|
+
element('button', {
|
|
1627
|
+
type: 'button',
|
|
1628
|
+
className: 'dbt-projection-toggle',
|
|
1629
|
+
'data-no-card-drag': true,
|
|
1630
|
+
draggable: false,
|
|
1631
|
+
disabled: !goalAvailable,
|
|
1632
|
+
'aria-expanded': goalExpanded,
|
|
1633
|
+
'aria-label': t(goalExpanded ? 'tasks.collapseGoal' : 'tasks.expandGoal'),
|
|
1634
|
+
title: goalLabel,
|
|
1635
|
+
onClick: () => { if (goalAvailable) toggleProjection(task.id, 'goal', preferences.defaultGoalExpanded) },
|
|
1636
|
+
}, element(IconGoalOutlineRegular, { size: 14 }), element(goalExpanded ? IconChevronDownOutlineRegular : IconChevronRightOutlineRegular, { size: 14 })),
|
|
1637
|
+
todoVisible ? element('button', {
|
|
1638
|
+
type: 'button',
|
|
1639
|
+
className: 'dbt-projection-toggle',
|
|
1640
|
+
'data-no-card-drag': true,
|
|
1641
|
+
draggable: false,
|
|
1642
|
+
disabled: !todoAvailable,
|
|
1643
|
+
'aria-expanded': todoExpanded,
|
|
1644
|
+
'aria-label': t(todoExpanded ? 'tasks.collapseTodo' : 'tasks.expandTodo'),
|
|
1645
|
+
title: todoLabel,
|
|
1646
|
+
onClick: () => { if (todoAvailable) toggleProjection(task.id, 'todo', preferences.defaultTodoExpanded) },
|
|
1647
|
+
}, element(IconChecklistOutlineRegular, { size: 14 }), todoAvailable ? element('span', { className: 'dbt-todo-count' }, todoLabel) : null, element(todoExpanded ? IconChevronDownOutlineRegular : IconChevronRightOutlineRegular, { size: 14 })) : null,
|
|
1648
|
+
task.session.status !== 'idle' ? null : element('button', {
|
|
1649
|
+
type: 'button',
|
|
1650
|
+
className: 'dbt-projection-toggle',
|
|
1651
|
+
'data-no-card-drag': true,
|
|
1652
|
+
draggable: false,
|
|
1653
|
+
'aria-expanded': finalExpanded,
|
|
1654
|
+
'aria-label': t(finalExpanded ? 'tasks.collapseFinal' : 'tasks.expandFinal'),
|
|
1655
|
+
title: t('tasks.final'),
|
|
1656
|
+
onClick: () => {
|
|
1657
|
+
finalClicks.current.cancel(task.id)
|
|
1658
|
+
toggleProjection(task.id, 'final', preferences.defaultFinalExpanded)
|
|
1659
|
+
},
|
|
1660
|
+
}, element(IconSparkleRegular, { size: 14 }), element(finalExpanded ? IconChevronDownOutlineRegular : IconChevronRightOutlineRegular, { size: 14 }))),
|
|
1661
|
+
detailRows.length === 0 ? null : element('div', { className: 'dbt-projection-details' }, ...detailRows),
|
|
1662
|
+
task.session.status !== 'idle' ? null : element('div', {
|
|
1663
|
+
className: 'dbt-continue-shell', hidden: !inputExpanded,
|
|
1664
|
+
'data-no-card-drag': true, draggable: false,
|
|
1665
|
+
onDragStart: (event) => event.preventDefault(),
|
|
1666
|
+
}, element(TaskContinueInput, {
|
|
1667
|
+
sessionId: task.id,
|
|
1668
|
+
active: inputExpanded,
|
|
1669
|
+
createTaskInput,
|
|
1670
|
+
t,
|
|
1671
|
+
onWarning: (warning) => setActionWarning(`${t('tasks.goalResumeWarning')}: ${warning}`),
|
|
1672
|
+
})),
|
|
1673
|
+
answerable ? element('div', {
|
|
1674
|
+
className: 'dbt-question-surface',
|
|
1675
|
+
'data-no-card-drag': true,
|
|
1676
|
+
draggable: false,
|
|
1677
|
+
onDragStart: (event) => event.preventDefault(),
|
|
1678
|
+
},
|
|
1679
|
+
inlineQuestion ? element(TaskQuestionCard, {
|
|
1680
|
+
key: `${task.id}:${questionTarget.requestKey ?? questionTarget.callId}`,
|
|
1681
|
+
target: questionTarget, createTaskQuestion, fontSize: preferences.detailFontSize,
|
|
1682
|
+
revealQuestion, canRevealQuestion, t,
|
|
1683
|
+
}) : element('button', {
|
|
1684
|
+
type: 'button',
|
|
1685
|
+
className: 'dbt-task-answer',
|
|
1686
|
+
title: t('tasks.answerHint'),
|
|
1687
|
+
onClick: (event) => {
|
|
1688
|
+
event.stopPropagation()
|
|
1689
|
+
const revealed = revealQuestion(task.id, answerCallId)
|
|
1690
|
+
// `null` is "this composition has no question service" (already
|
|
1691
|
+
// filtered out above); `false` is "the panel exists but would not
|
|
1692
|
+
// open". Only the latter is worth telling the user about.
|
|
1693
|
+
if (revealed === false) setActionWarning(t('tasks.answerFailed'))
|
|
1694
|
+
},
|
|
1695
|
+
}, t('tasks.answer'))) : null)
|
|
1696
|
+
}))),
|
|
1697
|
+
editingWorkspace !== undefined ? element(AppearanceDialog, {
|
|
1698
|
+
workspace: editingWorkspace,
|
|
1699
|
+
appearance: appearanceByWorkspace.get(editingWorkspace.workspaceId),
|
|
1700
|
+
appearanceState,
|
|
1701
|
+
onClose: () => setEditingWorkspaceId(null),
|
|
1702
|
+
t,
|
|
1703
|
+
}) : null,
|
|
1704
|
+
taskAction === null ? null : element(TaskActionDialog, {
|
|
1705
|
+
key: `${taskAction.kind}:${taskAction.id}`,
|
|
1706
|
+
action: taskAction,
|
|
1707
|
+
onClose: () => setTaskAction(null),
|
|
1708
|
+
onSubmit: (action, text) => {
|
|
1709
|
+
const current = tasks.find((item) => item.id === action.id)
|
|
1710
|
+
if (current === undefined || action.kind === 'continue' && current.session.status !== 'idle') throw new Error(t('tasks.actionUnavailable'))
|
|
1711
|
+
return submitTaskAction(action, text)
|
|
1712
|
+
},
|
|
1713
|
+
t,
|
|
1714
|
+
}))
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
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 }) {
|
|
1718
|
+
if (!wide) {
|
|
1719
|
+
return renderSlot('sidebar.workspaces', { wide: false, expandSidebar: () => { if (collapsed) toggleSidebar() } })
|
|
1720
|
+
}
|
|
1721
|
+
return element('div', { className: 'dbt-region-shell' },
|
|
1722
|
+
element('div', { className: 'dbt-view-tabs', role: 'tablist', 'aria-label': 'Sidebar view' },
|
|
1723
|
+
element('button', { type: 'button', role: 'tab', className: 'dbt-view-tab', 'aria-selected': view === 'native', onClick: () => setView('native') }, t('view.native')),
|
|
1724
|
+
element('button', { type: 'button', role: 'tab', className: 'dbt-view-tab', 'aria-selected': view === 'tasks', onClick: () => setView('tasks') }, t('view.tasks'))),
|
|
1725
|
+
element('div', { className: 'dbt-region-panel', role: 'tabpanel' },
|
|
1726
|
+
view === 'native' ? renderSlot('sidebar.workspaces', { wide: true, expandSidebar: () => {} }) : element(TaskSwimlane, {
|
|
1727
|
+
useSessions,
|
|
1728
|
+
useWorkspaces,
|
|
1729
|
+
useSessionStatus, useTaskConnection,
|
|
1730
|
+
usePanelInfo,
|
|
1731
|
+
useTaskPins,
|
|
1732
|
+
openSession,
|
|
1733
|
+
moveTaskBefore,
|
|
1734
|
+
unpinTask,
|
|
1735
|
+
revealQuestion,
|
|
1736
|
+
canRevealQuestion,
|
|
1737
|
+
findTaskQuestion,
|
|
1738
|
+
createTaskQuestion,
|
|
1739
|
+
submitTaskAction,
|
|
1740
|
+
createTaskInput,
|
|
1741
|
+
twoColumns,
|
|
1742
|
+
preferences,
|
|
1743
|
+
t,
|
|
1744
|
+
})))
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
export function PanelRow({ panel, usePanelInfo, selectPanel, renderSlot, renderFactorySlot, canUnpin, unpinLabel, setPanelPinned }) {
|
|
1748
|
+
const { Tooltip, IconPinFillRegular, focusWithoutRing } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1749
|
+
const active = usePanelInfo((info) => info.activePanelId === panel.id)
|
|
1750
|
+
return element('div', { className: 'dbt-panel-entry' },
|
|
1751
|
+
element(Tooltip, { label: panel.label, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, side: 'bottom', portal: true,
|
|
1752
|
+
children: element('button', {
|
|
1753
|
+
type: 'button', className: 'dbt-panel-row',
|
|
1754
|
+
'aria-label': panel.label, 'aria-current': active ? 'page' : undefined,
|
|
1755
|
+
onClick: () => selectPanel(panel.id),
|
|
1756
|
+
}, element('span', { className: 'dbt-panel-glyph', 'aria-hidden': 'true' },
|
|
1757
|
+
typeof renderFactorySlot === 'function'
|
|
1758
|
+
? renderFactorySlot('sidebar.panel.icon', { id: panel.id, size: SIDEBAR_LAYOUT.iconSize, active })
|
|
1759
|
+
: renderSlot('sidebar.panellist', { size: SIDEBAR_LAYOUT.iconSize, active }, { only: panel.id }))) }),
|
|
1760
|
+
// Native More lists only UNPINNED entries. Keep a separate unpin affordance,
|
|
1761
|
+
// or the menu would have no way to acquire its first hidden launcher.
|
|
1762
|
+
canUnpin ? element(Tooltip, { label: unpinLabel, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, side: 'bottom', portal: true,
|
|
1763
|
+
children: element('button', { type: 'button', className: 'dbt-panel-unpin', 'aria-label': unpinLabel,
|
|
1764
|
+
onClick: (event) => {
|
|
1765
|
+
const more = event.currentTarget.closest('nav')?.querySelector('button[aria-haspopup="menu"]')
|
|
1766
|
+
if (more) { if (typeof focusWithoutRing === 'function') focusWithoutRing(more); else more.focus() }
|
|
1767
|
+
setPanelPinned(panel.id, false)
|
|
1768
|
+
},
|
|
1769
|
+
}, element(IconPinFillRegular, { size: 12, 'aria-hidden': true })) }) : null)
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
export function PanelMoreMenu({ entries, label, onOpen, onPinnedChange, renderSlot }) {
|
|
1773
|
+
const React = requireModule('react')
|
|
1774
|
+
const { Menu, Tooltip, IconAppsAddOutlineRegular } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1775
|
+
const [open, setOpen] = React.useState(false)
|
|
1776
|
+
const onDismiss = () => setOpen(false)
|
|
1777
|
+
return element(Menu, {
|
|
1778
|
+
open, onClose: onDismiss, portal: true, autoFocus: true,
|
|
1779
|
+
className: 'dbt-panel-more-anchor', listClassName: 'dbt-panel-more-menu',
|
|
1780
|
+
anchor: element(Tooltip, { label, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, side: 'bottom', portal: true,
|
|
1781
|
+
children: element('button', { type: 'button', className: 'dbt-panel-row',
|
|
1782
|
+
'aria-label': label, 'aria-haspopup': 'menu', 'aria-expanded': open,
|
|
1783
|
+
onClick: () => setOpen(value => !value),
|
|
1784
|
+
}, element(IconAppsAddOutlineRegular, { size: SIDEBAR_LAYOUT.iconSize, 'aria-hidden': true })) }),
|
|
1785
|
+
}, renderSlot('sidebar.panellist.actions', { entries, onOpen, onPinnedChange, onDismiss }))
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Guarded root: the component actually handed to `ctx.slots.register`.
|
|
1790
|
+
*
|
|
1791
|
+
* Two narrow jobs:
|
|
1792
|
+
* 1. Forensics — record whether a hook can legally run in this frame. That one
|
|
1793
|
+
* fact separates "React called us" from "something called the component as a
|
|
1794
|
+
* plain function", which is the difference between a props bug and an
|
|
1795
|
+
* unsupported call site.
|
|
1796
|
+
* 2. Containment — a bare call (no arguments) from anywhere outside React gets a
|
|
1797
|
+
* visible diagnostic block instead of throwing. A throw during render is what
|
|
1798
|
+
* makes the slot retire the occupant and blank the whole column.
|
|
1799
|
+
*
|
|
1800
|
+
* It deliberately owns no hook except the probe, so the short circuit below can
|
|
1801
|
+
* never change the real component's hook order.
|
|
1802
|
+
*/
|
|
1803
|
+
export function SidebarRootGuard(props) {
|
|
1804
|
+
const React = requireModule('react')
|
|
1805
|
+
const startedAt = Date.now()
|
|
1806
|
+
// Probe FIRST: a guard that returned before this would hide the very bare call
|
|
1807
|
+
// the probe exists to observe, and B would be misread as "did not crash".
|
|
1808
|
+
let hookOk = true
|
|
1809
|
+
try { React.useState(0) } catch { hookOk = false }
|
|
1810
|
+
const zeroArg = arguments.length === 0
|
|
1811
|
+
// Captured here, while this frame is still the top of the stack: inside the
|
|
1812
|
+
// caller, not after createElement. Empty for a React render, which needs none.
|
|
1813
|
+
const stack = zeroArg && !hookOk ? captureCallStack() : ''
|
|
1814
|
+
if (RENDER_PROBE) {
|
|
1815
|
+
writeRenderProbe({
|
|
1816
|
+
zeroArg,
|
|
1817
|
+
isElement: React.isValidElement(props),
|
|
1818
|
+
propKeys: props !== null && typeof props === 'object' ? Object.keys(props).slice(0, 8) : [],
|
|
1819
|
+
durationMs: Date.now() - startedAt,
|
|
1820
|
+
hookOk,
|
|
1821
|
+
stack,
|
|
1822
|
+
})
|
|
1823
|
+
if (renderProbeCount === 1) {
|
|
1824
|
+
writeStorage(RENDER_ENV_KEY, JSON.stringify({
|
|
1825
|
+
revision: BUILD_REVISION,
|
|
1826
|
+
reactVersion: React.version,
|
|
1827
|
+
propsNull: props === null || props === undefined,
|
|
1828
|
+
argCount: arguments.length,
|
|
1829
|
+
}))
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
// Dry-run guard, after the probe.
|
|
1833
|
+
if (zeroArg && !hookOk) {
|
|
1834
|
+
return React.createElement('div', {
|
|
1835
|
+
'data-better-tasks-diagnostic': 'bare-call',
|
|
1836
|
+
style: { padding: '10px', font: '12px/1.5 ui-monospace, monospace', color: '#ff8f8f' },
|
|
1837
|
+
}, `Better Tasks: sidebar root invoked outside React (revision ${BUILD_REVISION})`)
|
|
1838
|
+
}
|
|
1839
|
+
// Safe defaults keep the real component's hook sequence stable.
|
|
1840
|
+
const safe = props === null || props === undefined ? {} : props
|
|
1841
|
+
// Rendered as an element so the real component keeps its own fiber. The
|
|
1842
|
+
// occupant's throw came from an undefined element type (React #130) among the
|
|
1843
|
+
// descendant icons, which is fixed at the source; no boundary is carried here.
|
|
1844
|
+
return React.createElement(BetterSidebarRoot, safe)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/** AppFrame mounts this root-scoped chrome seat only while the macOS rail is hidden. */
|
|
1848
|
+
export function SidebarReopenControls({ toggleSidebar, startSession, useRemoteAccess, t }) {
|
|
1849
|
+
const { Tooltip, IconPanelLeftOutlineRegular: PanelIcon, IconNewChatOutlineRegular: NewIcon } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1850
|
+
return element('div', { className: 'dbt-shell-leading' },
|
|
1851
|
+
element(Tooltip, {
|
|
1852
|
+
label: t('toggle.open'), delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, portal: true,
|
|
1853
|
+
children: element('button', {
|
|
1854
|
+
type: 'button', className: 'dbt-icon-button',
|
|
1855
|
+
'aria-label': t('toggle.open'), 'aria-expanded': false,
|
|
1856
|
+
onClick: toggleSidebar,
|
|
1857
|
+
}, element(PanelIcon, { size: 16 })),
|
|
1858
|
+
}),
|
|
1859
|
+
(typeof useRemoteAccess !== 'function' || useRemoteAccess('session/create')) ? element(Tooltip, {
|
|
1860
|
+
label: t('session.new.label'), delayMs: SIDEBAR_LAYOUT.tooltipDelayMs, portal: true,
|
|
1861
|
+
children: element('button', {
|
|
1862
|
+
type: 'button', className: 'dbt-icon-button',
|
|
1863
|
+
'aria-label': t('session.new.label'),
|
|
1864
|
+
onClick: () => startSession(),
|
|
1865
|
+
}, element(NewIcon, { size: 16 })),
|
|
1866
|
+
}) : null)
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
export function BetterSidebarRoot({
|
|
1870
|
+
collapsed,
|
|
1871
|
+
width,
|
|
1872
|
+
startSession,
|
|
1873
|
+
toggleSidebar,
|
|
1874
|
+
selectPanel,
|
|
1875
|
+
usePanels,
|
|
1876
|
+
usePanelInfo,
|
|
1877
|
+
useEntryPins,
|
|
1878
|
+
useEntryActions,
|
|
1879
|
+
useShortcuts,
|
|
1880
|
+
useRemoteAccess,
|
|
1881
|
+
setPanelPinned,
|
|
1882
|
+
nativePanelIcons = false,
|
|
1883
|
+
useSessions,
|
|
1884
|
+
useWorkspaces,
|
|
1885
|
+
useSessionStatus, useTaskConnection,
|
|
1886
|
+
useTaskPins,
|
|
1887
|
+
useTaskPreferences,
|
|
1888
|
+
setTaskPreference,
|
|
1889
|
+
openSession,
|
|
1890
|
+
moveTaskBefore,
|
|
1891
|
+
unpinTask,
|
|
1892
|
+
revealQuestion,
|
|
1893
|
+
canRevealQuestion,
|
|
1894
|
+
findTaskQuestion,
|
|
1895
|
+
createTaskQuestion,
|
|
1896
|
+
submitTaskAction,
|
|
1897
|
+
createTaskInput,
|
|
1898
|
+
t,
|
|
1899
|
+
renderSlot,
|
|
1900
|
+
renderFactorySlot,
|
|
1901
|
+
}) {
|
|
1902
|
+
const React = requireModule('react')
|
|
1903
|
+
const { Tooltip, IconNewChatOutlineMedium, IconNewChatOutlineRegular, ShortcutKeys } = requireModule('@deepseek-ai/dsh-client-ui-primitives')
|
|
1904
|
+
const panels = usePanels((value) => value)
|
|
1905
|
+
const unpinned = useEntryPins?.((value) => value) ?? []
|
|
1906
|
+
const hasEntryActions = useEntryActions?.((value) => value) ?? false
|
|
1907
|
+
const activePanelId = usePanelInfo?.((info) => info.activePanelId)
|
|
1908
|
+
const newShortcut = useShortcuts?.((rows) => rows.find(row => row.id === 'session.new'))
|
|
1909
|
+
const canStartSession = typeof useRemoteAccess !== 'function' || useRemoteAccess('session/create')
|
|
1910
|
+
const preferences = useTaskPreferences((snapshot) => snapshot.value)
|
|
1911
|
+
// D1: the view is a Config preference rather than plugin-local localStorage, so
|
|
1912
|
+
// the Host persists it and 0.2 and 0.1.5 reach it through the same path. The old
|
|
1913
|
+
// localStorage value is folded in once by migrateLegacySidebarView().
|
|
1914
|
+
const view = preferences.sidebarView === 'tasks' ? 'tasks' : 'native'
|
|
1915
|
+
const setView = (next) => {
|
|
1916
|
+
void Promise.resolve(setTaskPreference('sidebarView', next === 'tasks' ? 'tasks' : 'native')).catch(() => {})
|
|
1917
|
+
}
|
|
1918
|
+
const windowsTitlebar = typeof document !== 'undefined' && document.documentElement.hasAttribute('data-windows-titlebar')
|
|
1919
|
+
const [settled, setSettled] = React.useState(collapsed)
|
|
1920
|
+
React.useEffect(() => {
|
|
1921
|
+
if (!collapsed) {
|
|
1922
|
+
setSettled(false)
|
|
1923
|
+
return undefined
|
|
1924
|
+
}
|
|
1925
|
+
const timer = window.setTimeout(() => setSettled(true), 150)
|
|
1926
|
+
return () => window.clearTimeout(timer)
|
|
1927
|
+
}, [collapsed])
|
|
1928
|
+
const wide = windowsTitlebar ? !collapsed : !collapsed || !settled
|
|
1929
|
+
const twoColumns = !collapsed && (preferences.columnLayout === 'double' || (preferences.columnLayout === 'auto' && width > TASK_GRID_TWO_COLUMN_THRESHOLD))
|
|
1930
|
+
const lastWideWidth = React.useRef(width)
|
|
1931
|
+
if (!collapsed) lastWideWidth.current = width
|
|
1932
|
+
const toggleLabel = collapsed ? t('toggle.open') : t('toggle.collapse')
|
|
1933
|
+
const newLabel = t('session.new.label')
|
|
1934
|
+
const startNewSession = () => startSession()
|
|
1935
|
+
|
|
1936
|
+
return element('aside', {
|
|
1937
|
+
className: 'dbt-sidebar',
|
|
1938
|
+
'data-wide': String(wide),
|
|
1939
|
+
'data-collapsing': String(collapsed && wide),
|
|
1940
|
+
style: wide ? { width: collapsed ? lastWideWidth.current : width } : undefined,
|
|
1941
|
+
'aria-label': 'DSH sidebar',
|
|
1942
|
+
},
|
|
1943
|
+
element('div', { className: 'dbt-brand-row' },
|
|
1944
|
+
wide ? element(canStartSession ? 'button' : 'span', {
|
|
1945
|
+
type: canStartSession ? 'button' : undefined,
|
|
1946
|
+
className: 'dbt-brand',
|
|
1947
|
+
'aria-label': canStartSession ? newLabel : undefined,
|
|
1948
|
+
'aria-keyshortcuts': canStartSession ? newShortcut?.aria : undefined,
|
|
1949
|
+
onClick: canStartSession ? startNewSession : undefined,
|
|
1950
|
+
},
|
|
1951
|
+
element('span', { className: 'dbt-brand-mark', 'aria-hidden': 'true' },
|
|
1952
|
+
renderSlot('sidebar.brand.mark', { size: 24 }, { fallback: element('span', null, '◆') })),
|
|
1953
|
+
element('span', { className: 'dbt-brand-name' },
|
|
1954
|
+
renderSlot('sidebar.brand.name', {}, { fallback: element('span', null, t('brand.fallback')) }))) : null,
|
|
1955
|
+
element(Tooltip, {
|
|
1956
|
+
label: toggleLabel,
|
|
1957
|
+
delayMs: SIDEBAR_LAYOUT.tooltipDelayMs,
|
|
1958
|
+
side: windowsTitlebar ? 'bottom' : 'right',
|
|
1959
|
+
portal: windowsTitlebar,
|
|
1960
|
+
children: element('button', {
|
|
1961
|
+
type: 'button',
|
|
1962
|
+
className: 'dbt-icon-button dbt-toggle',
|
|
1963
|
+
'aria-label': toggleLabel,
|
|
1964
|
+
'aria-expanded': !collapsed,
|
|
1965
|
+
onClick: toggleSidebar,
|
|
1966
|
+
},
|
|
1967
|
+
!wide && !windowsTitlebar ? element('span', { className: 'dbt-rail-mark', 'aria-hidden': 'true' },
|
|
1968
|
+
renderSlot('sidebar.brand.mark', { size: 24 }, { fallback: element('span', null, '◆') })) : null,
|
|
1969
|
+
element(IconPanelLeftOutlineRegular, { className: 'dbt-panel-icon', size: wide || windowsTitlebar ? 16 : 18 }))
|
|
1970
|
+
})),
|
|
1971
|
+
// rc.3 HOC: owner content is an option, not an owner prop or a fallback.
|
|
1972
|
+
// The renderer keeps this button when no optional action is registered.
|
|
1973
|
+
canStartSession ? element('div', { className: 'dbt-new-session-row' },
|
|
1974
|
+
renderSlot('sidebar.session.action', { wide }, { children: element(Tooltip, {
|
|
1975
|
+
label: newLabel, shortcutKeys: newShortcut?.keys, delayMs: SIDEBAR_LAYOUT.tooltipDelayMs,
|
|
1976
|
+
side: windowsTitlebar ? 'bottom' : 'right', disabled: wide,
|
|
1977
|
+
children: element('button', {
|
|
1978
|
+
type: 'button', className: 'dbt-new-session',
|
|
1979
|
+
'aria-label': newLabel, 'aria-keyshortcuts': newShortcut?.aria, onClick: startNewSession,
|
|
1980
|
+
}, element('span', { className: 'dbt-new-label-mask' },
|
|
1981
|
+
element('span', { className: 'dbt-new-content' },
|
|
1982
|
+
element(wide ? IconNewChatOutlineMedium ?? IconNewChatOutlineRegular : IconNewChatOutlineRegular,
|
|
1983
|
+
{ size: wide ? 14 : windowsTitlebar ? 16 : 18, 'aria-hidden': true }),
|
|
1984
|
+
wide ? element('span', { className: 'dbt-new-label' }, t('session.new')) : null)),
|
|
1985
|
+
wide && newShortcut?.keys?.length > 0 ? element('span', { className: 'dbt-new-shortcut', 'aria-hidden': true },
|
|
1986
|
+
element(ShortcutKeys, { keys: newShortcut.keys })) : null),
|
|
1987
|
+
}) })) : null,
|
|
1988
|
+
panels.length > 0 ? element('nav', { className: 'dbt-panel-list', 'aria-label': t('panels.label') },
|
|
1989
|
+
...panels.filter(panel => panel.id === 'plugins' || !hasEntryActions || !unpinned.includes(panel.id)).map((panel) => element(PanelRow, {
|
|
1990
|
+
key: panel.id, panel, usePanelInfo, selectPanel, renderSlot, setPanelPinned,
|
|
1991
|
+
canUnpin: hasEntryActions && panel.id !== 'plugins', unpinLabel: t('panels.unpin', { name: panel.label }),
|
|
1992
|
+
renderFactorySlot: nativePanelIcons ? renderFactorySlot : undefined,
|
|
1993
|
+
})),
|
|
1994
|
+
hasEntryActions ? element(PanelMoreMenu, {
|
|
1995
|
+
label: t('panels.more'),
|
|
1996
|
+
entries: panels.filter(panel => panel.id !== 'plugins').map(panel => ({
|
|
1997
|
+
id: panel.id, label: panel.label, pinned: !unpinned.includes(panel.id), active: panel.id === activePanelId,
|
|
1998
|
+
})),
|
|
1999
|
+
onOpen: selectPanel, onPinnedChange: setPanelPinned, renderSlot,
|
|
2000
|
+
}) : null) : null,
|
|
2001
|
+
element('div', { className: 'dbt-workspaces' },
|
|
2002
|
+
element(SidebarRegion, {
|
|
2003
|
+
wide,
|
|
2004
|
+
collapsed,
|
|
2005
|
+
view,
|
|
2006
|
+
setView,
|
|
2007
|
+
twoColumns,
|
|
2008
|
+
preferences,
|
|
2009
|
+
toggleSidebar,
|
|
2010
|
+
renderSlot,
|
|
2011
|
+
useSessions,
|
|
2012
|
+
useWorkspaces,
|
|
2013
|
+
useSessionStatus, useTaskConnection,
|
|
2014
|
+
usePanelInfo,
|
|
2015
|
+
useTaskPins,
|
|
2016
|
+
openSession,
|
|
2017
|
+
moveTaskBefore,
|
|
2018
|
+
unpinTask,
|
|
2019
|
+
revealQuestion,
|
|
2020
|
+
canRevealQuestion,
|
|
2021
|
+
findTaskQuestion,
|
|
2022
|
+
createTaskQuestion,
|
|
2023
|
+
submitTaskAction,
|
|
2024
|
+
createTaskInput,
|
|
2025
|
+
t,
|
|
2026
|
+
})),
|
|
2027
|
+
element('div', { className: 'dbt-foot' },
|
|
2028
|
+
element('div', { className: 'dbt-footer-actions' }, renderSlot('sidebar.footer.action', { wide })),
|
|
2029
|
+
element('div', { className: 'dbt-settings' }, renderSlot('sidebar.settings', { wide }))))
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function TaskSettingsSection({ useTaskPreferences, setTaskPreference, t }) {
|
|
2033
|
+
const React = requireModule('react')
|
|
2034
|
+
const element = React.createElement
|
|
2035
|
+
const snapshot = useTaskPreferences((value) => value)
|
|
2036
|
+
const [draft, setDraft] = React.useState(snapshot.value)
|
|
2037
|
+
const [pending, setPending] = React.useState(false)
|
|
2038
|
+
const [failed, setFailed] = React.useState(false)
|
|
2039
|
+
React.useEffect(() => setDraft(snapshot.value), [snapshot.value])
|
|
2040
|
+
const writable = snapshot.status === 'ready' && snapshot.writable && !pending
|
|
2041
|
+
const update = (field, value) => {
|
|
2042
|
+
if (!writable) return
|
|
2043
|
+
setDraft((current) => ({ ...current, [field]: value }))
|
|
2044
|
+
setFailed(false)
|
|
2045
|
+
setPending(true)
|
|
2046
|
+
Promise.resolve(setTaskPreference(field, value)).catch(() => {
|
|
2047
|
+
setDraft(snapshot.value)
|
|
2048
|
+
setFailed(true)
|
|
2049
|
+
}).finally(() => setPending(false))
|
|
2050
|
+
}
|
|
2051
|
+
const switchRow = (field, label) => element('div', { className: 'dbt-settings-switch-row', key: field },
|
|
2052
|
+
element('span', null, label),
|
|
2053
|
+
element('button', {
|
|
2054
|
+
type: 'button',
|
|
2055
|
+
role: 'switch',
|
|
2056
|
+
className: 'dbt-settings-switch',
|
|
2057
|
+
'aria-label': label,
|
|
2058
|
+
'aria-checked': String(draft[field]),
|
|
2059
|
+
disabled: !writable,
|
|
2060
|
+
onClick: () => update(field, !draft[field]),
|
|
2061
|
+
}))
|
|
2062
|
+
const status = failed
|
|
2063
|
+
? element('div', { className: 'dbt-settings-status', 'data-error': true }, t('settings.writeFailed'))
|
|
2064
|
+
: snapshot.status === 'loading'
|
|
2065
|
+
? element('div', { className: 'dbt-settings-status' }, t('settings.loading'))
|
|
2066
|
+
: snapshot.status === 'unavailable'
|
|
2067
|
+
? element('div', { className: 'dbt-settings-status' }, t('settings.unavailable'))
|
|
2068
|
+
: snapshot.status === 'ready' && !snapshot.writable
|
|
2069
|
+
? element('div', { className: 'dbt-settings-status' }, t('settings.readOnly'))
|
|
2070
|
+
: null
|
|
2071
|
+
return element('section', { className: 'dbt-settings-section', 'aria-labelledby': 'dbt-settings-title' },
|
|
2072
|
+
element('div', { className: 'dbt-settings-heading' },
|
|
2073
|
+
element('h2', { id: 'dbt-settings-title' }, t('settings.title')),
|
|
2074
|
+
element('p', null, t('settings.description'))),
|
|
2075
|
+
status,
|
|
2076
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2077
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.fontSize')),
|
|
2078
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2079
|
+
element('div', { className: 'dbt-settings-font-options' },
|
|
2080
|
+
...[11, 12, 13, 14, 15, 16].map((size) => element('button', {
|
|
2081
|
+
key: size,
|
|
2082
|
+
type: 'button',
|
|
2083
|
+
className: 'dbt-settings-font-option',
|
|
2084
|
+
'aria-label': `${size}px`,
|
|
2085
|
+
'aria-pressed': String(draft.detailFontSize === size),
|
|
2086
|
+
disabled: !writable,
|
|
2087
|
+
onClick: () => update('detailFontSize', size),
|
|
2088
|
+
}, `${size}px`))),
|
|
2089
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.fontSizeHint')))),
|
|
2090
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2091
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.titleEmphasis')),
|
|
2092
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2093
|
+
element('div', { className: 'dbt-settings-segments', 'data-options': '2' },
|
|
2094
|
+
...['project', 'task'].map((emphasis) => element('button', {
|
|
2095
|
+
key: emphasis,
|
|
2096
|
+
type: 'button',
|
|
2097
|
+
className: 'dbt-settings-segment',
|
|
2098
|
+
'aria-pressed': String(draft.titleEmphasis === emphasis),
|
|
2099
|
+
disabled: !writable,
|
|
2100
|
+
onClick: () => update('titleEmphasis', emphasis),
|
|
2101
|
+
}, t(`settings.titleEmphasis.${emphasis}`)))),
|
|
2102
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.titleEmphasisHint')))),
|
|
2103
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2104
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.defaultExpansion')),
|
|
2105
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2106
|
+
switchRow('defaultTodoExpanded', t('settings.todoExpanded')),
|
|
2107
|
+
switchRow('defaultGoalExpanded', t('settings.goalExpanded')),
|
|
2108
|
+
switchRow('defaultFinalExpanded', t('settings.finalExpanded')),
|
|
2109
|
+
switchRow('defaultInputExpanded', t('settings.inputExpanded')),
|
|
2110
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.expansionHint')))),
|
|
2111
|
+
element('div', { className: 'dbt-settings-group' },
|
|
2112
|
+
element('div', { className: 'dbt-settings-group-title' }, t('settings.columns')),
|
|
2113
|
+
element('div', { className: 'dbt-settings-card' },
|
|
2114
|
+
element('div', { className: 'dbt-settings-segments' },
|
|
2115
|
+
...['auto', 'single', 'double'].map((layout) => element('button', {
|
|
2116
|
+
key: layout,
|
|
2117
|
+
type: 'button',
|
|
2118
|
+
className: 'dbt-settings-segment',
|
|
2119
|
+
'aria-pressed': String(draft.columnLayout === layout),
|
|
2120
|
+
disabled: !writable,
|
|
2121
|
+
onClick: () => update('columnLayout', layout),
|
|
2122
|
+
}, t(`settings.columns.${layout}`)))),
|
|
2123
|
+
element('p', { className: 'dbt-settings-hint' }, t('settings.columnsHint')))))
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function mountTaskSettingsSection(ctx, taskPreferences) {
|
|
2127
|
+
const injected = () => ({
|
|
2128
|
+
hooks: { taskPreferences: taskPreferences.source },
|
|
2129
|
+
setTaskPreference: (field, value) => taskPreferences.set(field, value),
|
|
2130
|
+
})
|
|
2131
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
2132
|
+
name: 'settings.section',
|
|
2133
|
+
id: 'better-tasks',
|
|
2134
|
+
order: 45,
|
|
2135
|
+
label: () => ctx.locale.bind(NS)('settings.nav'),
|
|
2136
|
+
locale: NS,
|
|
2137
|
+
inject: injected,
|
|
2138
|
+
}, TaskSettingsSection))
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function mountSidebar(ctx, taskPins, taskPreferences) {
|
|
2142
|
+
// One confirmed native membership source for the official list and task cards.
|
|
2143
|
+
// uiWorkspace adds the official sidebar pin-fronting policy without navigation.
|
|
2144
|
+
ctx.effect(() => taskPins.connectNative({
|
|
2145
|
+
source: ctx.get?.('workspaces')?.list,
|
|
2146
|
+
sessions: ctx.get?.('sessions')?.list,
|
|
2147
|
+
pinSession: (id) => ctx.uiWorkspace.pinSession(id),
|
|
2148
|
+
unpinSession: (id) => ctx.uiWorkspace.unpinSession(id),
|
|
2149
|
+
}), 'dsh-better-tasks: native Pin projection')
|
|
2150
|
+
ctx.effect(() => ctx.uiWorkspace.onSessionCreated((sessionId) => taskPins.pinCreated(sessionId)), 'dsh-better-tasks: auto-pin created sessions')
|
|
2151
|
+
|
|
2152
|
+
// 0.2's catalog is metadata-only until projections are explicitly requested.
|
|
2153
|
+
// Read the Host-owned snapshots for pinned sessions, including unopened ones;
|
|
2154
|
+
// sessions.refreshProjections deduplicates per connection and needs no retain.
|
|
2155
|
+
ctx.effect(() => {
|
|
2156
|
+
const refresh = () => {
|
|
2157
|
+
const sessions = ctx.get?.('sessions')
|
|
2158
|
+
if (typeof sessions?.refreshProjections !== 'function') return
|
|
2159
|
+
for (const id of taskPins.source.getSnapshot().queue.sessionIds) {
|
|
2160
|
+
void sessions.refreshProjections(id).catch(() => {})
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const stop = taskPins.source.subscribe(refresh)
|
|
2164
|
+
refresh()
|
|
2165
|
+
return stop
|
|
2166
|
+
}, 'dsh-better-tasks: pinned session projections')
|
|
2167
|
+
|
|
2168
|
+
const panels = createSnapshotStore([])
|
|
2169
|
+
const panelPins = createPanelLauncherPins()
|
|
2170
|
+
const nativePanelIcons = typeof ctx.slots.registerFactory === 'function'
|
|
2171
|
+
const entryActions = {
|
|
2172
|
+
getSnapshot: () => nativePanelIcons && ctx.slots.entriesOfSlot('sidebar.panellist.actions').length > 0,
|
|
2173
|
+
subscribe: (listener) => ctx.slots.subscribe('sidebar.panellist.actions', listener),
|
|
2174
|
+
}
|
|
2175
|
+
const emptyShortcuts = createSnapshotStore([])
|
|
2176
|
+
const syncPanels = () => {
|
|
2177
|
+
const next = ctx.slots.entriesOfSlot('sidebar.panellist').map(({ options }, index) => ({
|
|
2178
|
+
id: options.id,
|
|
2179
|
+
order: options.order ?? 0,
|
|
2180
|
+
registrationIndex: index,
|
|
2181
|
+
label: resolveLabel(options.label, options.id),
|
|
2182
|
+
})).filter((panel) => typeof panel.id === 'string' && panel.id !== '')
|
|
2183
|
+
.sort((left, right) => left.order - right.order || left.registrationIndex - right.registrationIndex)
|
|
2184
|
+
panels.set(next)
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
ctx.effect(() => ctx.slots.subscribe('sidebar.panellist', syncPanels), 'dsh-better-tasks: panel entries')
|
|
2188
|
+
ctx.effect(() => ctx.locale.subscribe(syncPanels), 'dsh-better-tasks: panel labels')
|
|
2189
|
+
|
|
2190
|
+
// Optional factory lookup keeps startup independent of the input providers.
|
|
2191
|
+
// The surface owns its retained generation; metadata reads are not editor leases.
|
|
2192
|
+
const taskInputs = createTaskInputManager({
|
|
2193
|
+
getConversation: () => ctx.get('conversation'),
|
|
2194
|
+
getSessions: () => ctx.get('sessions'),
|
|
2195
|
+
getStatus: (id) => ctx.get('uiSession')?.sessionStatus.getSnapshot().get(id),
|
|
2196
|
+
getGoals: () => ctx.get('remote.goals') ?? ctx.get('remote')?.goals,
|
|
2197
|
+
isPinned: (id) => taskPins.source.getSnapshot().queue.sessionIds.includes(id),
|
|
2198
|
+
})
|
|
2199
|
+
const createTaskInput = (sessionId, options) => taskInputs.create(sessionId, options)
|
|
2200
|
+
const findTaskQuestion = (sessionId) => {
|
|
2201
|
+
try { return ctx.get('questionSurface')?.find?.(sessionId) }
|
|
2202
|
+
catch { return undefined } // Optional discovery cannot retire the sidebar root.
|
|
2203
|
+
}
|
|
2204
|
+
const createTaskQuestion = (options) => {
|
|
2205
|
+
const provider = ctx.get('questionSurface')
|
|
2206
|
+
if (typeof provider?.createCard !== 'function') throw new Error('Inline question surface is unavailable')
|
|
2207
|
+
return provider.createCard(options)
|
|
2208
|
+
}
|
|
2209
|
+
ctx.effect(() => () => taskInputs.dispose(), 'dsh-better-tasks: input surfaces')
|
|
2210
|
+
|
|
2211
|
+
const taskConnection = ctx.get?.('connection')?.state ?? createSnapshotStore(undefined)
|
|
2212
|
+
const injectProps = () => ({
|
|
2213
|
+
setTaskPreference: (field, value) => taskPreferences.set(field, value),
|
|
2214
|
+
startSession: (workspaceId, beforeOpen) => ctx.uiWorkspace.startSession(workspaceId, beforeOpen),
|
|
2215
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
2216
|
+
selectPanel: (panelId) => {
|
|
2217
|
+
if (panels.getSnapshot().some(panel => panel.id === panelId)) ctx.layout.selectPanel(panelId)
|
|
2218
|
+
},
|
|
2219
|
+
setPanelPinned: (panelId, pinned) => {
|
|
2220
|
+
if (panels.getSnapshot().some(panel => panel.id === panelId)) panelPins.setPinned(panelId, pinned)
|
|
2221
|
+
},
|
|
2222
|
+
nativePanelIcons,
|
|
2223
|
+
openSession: (sessionId) => ctx.uiWorkspace.openSession(sessionId),
|
|
2224
|
+
moveTaskBefore: (sessionId, beforeSessionId) => taskPins.moveBefore(sessionId, beforeSessionId),
|
|
2225
|
+
unpinTask: (sessionId) => taskPins.setPinned(sessionId, false),
|
|
2226
|
+
createTaskInput,
|
|
2227
|
+
findTaskQuestion,
|
|
2228
|
+
createTaskQuestion,
|
|
2229
|
+
submitTaskAction: (action, text) => {
|
|
2230
|
+
if (action.kind !== 'rename') throw new Error('Messages must use the native input surface')
|
|
2231
|
+
return taskInputs.rename(action.id, text)
|
|
2232
|
+
},
|
|
2233
|
+
// Optional by design: a composition without `ui-user-questions` (or without
|
|
2234
|
+
// this Session's call identity) hides the action instead of rendering a dead
|
|
2235
|
+
// button. Probed per render, not memoized: the service may load later.
|
|
2236
|
+
canRevealQuestion: () => ctx.get('userQuestionPanels') !== undefined,
|
|
2237
|
+
revealQuestion: (sessionId, callId) => ctx.get('userQuestionPanels')?.reveal(sessionId, callId) ?? null,
|
|
2238
|
+
hooks: { panels, entryPins: panelPins.source, entryActions,
|
|
2239
|
+
shortcuts: ctx.get?.('shortcuts')?.catalog ?? ctx.shortcuts?.catalog ?? emptyShortcuts,
|
|
2240
|
+
taskPins: taskPins.source, taskPreferences: taskPreferences.source, taskConnection },
|
|
2241
|
+
})
|
|
2242
|
+
|
|
2243
|
+
ctx.slots.inject('sidebar', () => {
|
|
2244
|
+
// The factory is the sole owner of panellist on rc.2; keep the direct legacy
|
|
2245
|
+
// declaration only on renderers without factories. No duplicate slot owners.
|
|
2246
|
+
const stopIcon = nativePanelIcons ? ctx.slots.registerFactory({
|
|
2247
|
+
name: 'sidebar.panel.icon', scope: 'root',
|
|
2248
|
+
children: { 'sidebar.panellist': { kind: 'list', scope: 'root' } },
|
|
2249
|
+
}, SidebarPanelIcon) : undefined
|
|
2250
|
+
const stopSidebar = ctx.slots.register({
|
|
2251
|
+
name: 'sidebar', locale: NS,
|
|
2252
|
+
children: createSidebarChildDeclarations({ iconsInFactory: nativePanelIcons }),
|
|
2253
|
+
inject: injectProps,
|
|
2254
|
+
}, SidebarRootGuard)
|
|
2255
|
+
return () => { stopSidebar(); stopIcon?.() }
|
|
2256
|
+
})
|
|
2257
|
+
// Disabling native ui-sidebar removes more than its column root. Restore its
|
|
2258
|
+
// outside-the-column reopen seat without taking over the native frame.
|
|
2259
|
+
ctx.slots.inject('shell.leading', () => ctx.slots.register({
|
|
2260
|
+
name: 'shell.leading',
|
|
2261
|
+
locale: NS,
|
|
2262
|
+
inject: () => ({
|
|
2263
|
+
toggleSidebar: () => ctx.layout.toggleSidebar(),
|
|
2264
|
+
startSession: () => ctx.uiWorkspace.startSession(),
|
|
2265
|
+
}),
|
|
2266
|
+
}, SidebarReopenControls))
|
|
2267
|
+
|
|
2268
|
+
syncPanels()
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
export function apply(ctx) {
|
|
2272
|
+
ctx.effect(() => installStyles(), 'dsh-better-tasks: styles')
|
|
2273
|
+
ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-better-tasks: dictionaries')
|
|
2274
|
+
const taskPins = createTaskPinsClient()
|
|
2275
|
+
const taskPreferences = createTaskPreferencesStore()
|
|
2276
|
+
ctx.provide('betterTaskPins', taskPins)
|
|
2277
|
+
ctx.effect(() => taskPins.start(), 'dsh-better-tasks: pin projection')
|
|
2278
|
+
// Settings, two generations, neither name in the module-level `inject`:
|
|
2279
|
+
// 0.1.5 -> `settingsScope.bind({ namespace })` returns the read/write scope.
|
|
2280
|
+
// 0.2 -> that service is gone; `configForms.get(entryId)` returns a
|
|
2281
|
+
// ConfigFormController whose getSnapshot/subscribe/set are the same
|
|
2282
|
+
// shape. The namespace is the plugin's own entry id, which is why
|
|
2283
|
+
// TASK_SETTINGS_NAMESPACE is both the 0.1.5 namespace and (by
|
|
2284
|
+
// install convention, as every in-box plugin does) the 0.2 row id.
|
|
2285
|
+
// A generation that lacks one name still activates: inject is non-blocking, and
|
|
2286
|
+
// only the matching probe fires.
|
|
2287
|
+
let settingsBound = false
|
|
2288
|
+
/**
|
|
2289
|
+
* Fold the pre-D1 localStorage view preference into the Config once.
|
|
2290
|
+
*
|
|
2291
|
+
* The Config is the single source of truth from now on, so the legacy key is
|
|
2292
|
+
* removed only after the Host write lands; a failed or unwritable Host leaves it
|
|
2293
|
+
* in place for the next start. Runs behind the settings inject, which is why it
|
|
2294
|
+
* cannot be a render-time effect.
|
|
2295
|
+
* @param store - the preferences store.
|
|
2296
|
+
*/
|
|
2297
|
+
let migrationPending = false
|
|
2298
|
+
const migrateLegacySidebarView = (store) => {
|
|
2299
|
+
if (migrationPending) return
|
|
2300
|
+
let legacy = null
|
|
2301
|
+
try {
|
|
2302
|
+
if (localStorage.getItem(VIEW_MIGRATED_KEY) !== null) return
|
|
2303
|
+
legacy = localStorage.getItem(LEGACY_VIEW_KEY)
|
|
2304
|
+
} catch {
|
|
2305
|
+
return
|
|
2306
|
+
}
|
|
2307
|
+
if (legacy !== 'tasks' && legacy !== 'native') {
|
|
2308
|
+
writeStorage(VIEW_MIGRATED_KEY, '1')
|
|
2309
|
+
return
|
|
2310
|
+
}
|
|
2311
|
+
if (store.source.getSnapshot().writable !== true) return
|
|
2312
|
+
migrationPending = true
|
|
2313
|
+
void Promise.resolve().then(() => store.set('sidebarView', legacy)).then(
|
|
2314
|
+
() => {
|
|
2315
|
+
try { localStorage.removeItem(LEGACY_VIEW_KEY) } catch {}
|
|
2316
|
+
writeStorage(VIEW_MIGRATED_KEY, '1')
|
|
2317
|
+
},
|
|
2318
|
+
() => {},
|
|
2319
|
+
).finally(() => { migrationPending = false })
|
|
2320
|
+
}
|
|
2321
|
+
const bindSettings = (scope) => {
|
|
2322
|
+
if (settingsBound) return
|
|
2323
|
+
const settings = typeof scope.configForms?.get === 'function'
|
|
2324
|
+
? scope.configForms.get(TASK_SETTINGS_ENTRY_ID)
|
|
2325
|
+
: typeof scope.settingsScope?.bind === 'function'
|
|
2326
|
+
? scope.settingsScope.bind({ namespace: TASK_SETTINGS_NAMESPACE })
|
|
2327
|
+
: undefined
|
|
2328
|
+
if (settings === undefined) return
|
|
2329
|
+
settingsBound = true
|
|
2330
|
+
scope.effect(() => taskPreferences.connect(settings), 'dsh-better-tasks: settings projection')
|
|
2331
|
+
// Config forms load asynchronously. Run migration once a writable snapshot
|
|
2332
|
+
// arrives, not just while the controller still reports `loading`.
|
|
2333
|
+
scope.effect(() => taskPreferences.source.subscribe(() => migrateLegacySidebarView(taskPreferences)), 'dsh-better-tasks: legacy view migration')
|
|
2334
|
+
migrateLegacySidebarView(taskPreferences)
|
|
2335
|
+
mountTaskSettingsSection(scope, taskPreferences)
|
|
2336
|
+
}
|
|
2337
|
+
ctx.inject(['configForms'], bindSettings)
|
|
2338
|
+
ctx.inject(['settingsScope'], bindSettings)
|
|
2339
|
+
ctx.inject(['uiWorkspace'], (scope) => mountSidebar(scope, taskPins, taskPreferences))
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
export const contract = Object.freeze({
|
|
2343
|
+
activityRanges: ACTIVITY_RANGE_DAYS,
|
|
2344
|
+
childSlots: SIDEBAR_CHILD_SLOTS,
|
|
2345
|
+
beforeIdForTaskDrop,
|
|
2346
|
+
createSidebarChildDeclarations,
|
|
2347
|
+
derivePinnedTasks,
|
|
2348
|
+
deriveRecentTasks,
|
|
2349
|
+
indexWorkspacesBySession,
|
|
2350
|
+
loadProjectAppearances,
|
|
2351
|
+
summarizeGoal,
|
|
2352
|
+
summarizeSession,
|
|
2353
|
+
summarizeTodos,
|
|
2354
|
+
taskExecutionWarning,
|
|
2355
|
+
})
|