dsh-taskboard 0.7.1 → 0.7.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/package.json CHANGED
@@ -1,92 +1,91 @@
1
- {
2
- "name": "dsh-taskboard",
3
- "description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.7.1",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./invariant": "./lib/invariant.js",
10
- "./client": "./lib/client.js",
11
- "./package.json": "./package.json"
12
- },
13
- "dsh": {
14
- "bundle": {
15
- "patch": "./cordis.patch.yml"
16
- },
17
- "client": {
18
- "inject": [],
19
- "platform": "web"
20
- },
21
- "compatibility": {
22
- "dshReleases": {
23
- "0.1.0-rc.8": "unknown",
24
- "0.1.1-rc.1": "unknown",
25
- "0.1.1-rc.2": "compatible",
26
- "0.1.2-alpha.2": "compatible",
27
- "0.1.2-alpha.3": "compatible",
28
- "0.1.2-alpha.4": "compatible",
1
+ {
2
+ "name": "dsh-taskboard",
3
+ "description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
4
+ "version": "0.7.4",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./invariant": "./lib/invariant.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "dsh": {
14
+ "bundle": {
15
+ "patch": "./cordis.patch.yml"
16
+ },
17
+ "client": {
18
+ "inject": [],
19
+ "platform": "web"
20
+ },
21
+ "compatibility": {
22
+ "dshReleases": {
23
+ "0.1.0-rc.8": "unknown",
24
+ "0.1.1-rc.1": "unknown",
25
+ "0.1.1-rc.2": "compatible",
26
+ "0.1.2-alpha.2": "compatible",
27
+ "0.1.2-alpha.3": "compatible",
28
+ "0.1.2-alpha.4": "compatible",
29
29
  "0.1.2-alpha.5": "compatible",
30
30
  "0.1.2-rc.1": "compatible",
31
31
  "0.1.5-rc.1": "compatible"
32
- }
33
- }
34
- },
35
- "files": [
36
- "lib/**/*.js",
37
- "lib/**/*.js.map",
38
- "src",
39
- "cordis.patch.yml",
40
- "LICENSE"
41
- ],
42
- "license": "Apache-2.0",
43
- "engines": {
44
- "node": ">=22"
45
- },
46
- "author": "cloader",
47
- "repository": {
48
- "type": "git",
49
- "url": "git+https://github.com/cloader/dsh-taskboard.git"
50
- },
51
- "bugs": {
52
- "url": "https://github.com/cloader/dsh-taskboard/issues"
53
- },
54
- "homepage": "https://github.com/cloader/dsh-taskboard#readme",
55
- "keywords": [
56
- "dsh",
57
- "deepseek-harness",
58
- "dsh-plugin",
59
- "taskboard",
60
- "kanban",
61
- "agent"
62
- ],
63
- "scripts": {
64
- "build": "node scripts/clean-lib.mjs && npm run build:host && npm run build:client",
65
- "build:host": "tsdown -c tsdown.host.config.ts",
66
- "build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
67
- "watch": "tsdown -c tsdown.host.config.ts --watch",
68
- "typecheck": "tsc --noEmit",
69
- "test": "vitest run"
70
- },
71
- "devDependencies": {
72
- "@deepseek-ai/cordis": "^4.0.2",
73
- "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
74
- "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.2",
75
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
76
- "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
77
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
78
- "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
79
- "@deepseek-ai/schemastery": "^3.18.2",
80
- "@types/node": "^22.20.1",
81
- "@types/react": "~18.3.1",
82
- "@types/react-dom": "^18.3.7",
83
- "jsdom": "^25.0.1",
84
- "puppeteer-core": "^25.8.0",
85
- "react": "^18.3.1",
86
- "react-dom": "^18.3.1",
87
- "tsdown": "0.22.2",
88
- "typescript": "~5.7.2",
89
- "vitest": "^3.0.0"
90
- }
91
- }
92
-
32
+ }
33
+ }
34
+ },
35
+ "files": [
36
+ "lib/**/*.js",
37
+ "lib/**/*.js.map",
38
+ "src",
39
+ "cordis.patch.yml",
40
+ "LICENSE"
41
+ ],
42
+ "license": "Apache-2.0",
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "author": "cloader",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/cloader/dsh-taskboard.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/cloader/dsh-taskboard/issues"
53
+ },
54
+ "homepage": "https://github.com/cloader/dsh-taskboard#readme",
55
+ "keywords": [
56
+ "dsh",
57
+ "deepseek-harness",
58
+ "dsh-plugin",
59
+ "taskboard",
60
+ "kanban",
61
+ "agent"
62
+ ],
63
+ "scripts": {
64
+ "build": "node scripts/clean-lib.mjs && npm run build:host && npm run build:client",
65
+ "build:host": "tsdown -c tsdown.host.config.ts",
66
+ "build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
67
+ "watch": "tsdown -c tsdown.host.config.ts --watch",
68
+ "typecheck": "tsc --noEmit",
69
+ "test": "vitest run"
70
+ },
71
+ "devDependencies": {
72
+ "@deepseek-ai/cordis": "^4.0.2",
73
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
74
+ "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.2",
75
+ "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
76
+ "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
77
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
78
+ "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
79
+ "@deepseek-ai/schemastery": "^3.18.2",
80
+ "@types/node": "^22.20.1",
81
+ "@types/react": "~18.3.1",
82
+ "@types/react-dom": "^18.3.7",
83
+ "jsdom": "^25.0.1",
84
+ "puppeteer-core": "^25.8.0",
85
+ "react": "^18.3.1",
86
+ "react-dom": "^18.3.1",
87
+ "tsdown": "0.22.2",
88
+ "typescript": "~5.7.2",
89
+ "vitest": "^3.0.0"
90
+ }
91
+ }
@@ -178,8 +178,20 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
178
178
  const [prompt, setPrompt] = useState(task?.prompt ?? prefill?.prompt ?? '')
179
179
  const [workspaceId, setWorkspaceId] = useState(task?.workspaceId ?? state.filters.workspaceId ?? state.workspaces[0]?.id ?? '')
180
180
  const [urgency, setUrgency] = useState<Urgency>(task?.urgency ?? (prefill?.urgency === 'urgent' || prefill?.urgency === 'relaxed' ? prefill.urgency : 'normal'))
181
- const [mode, setMode] = useState<'claim' | 'scheduled'>(task?.execution.mode === 'scheduled' || prefill?.execution?.mode === 'scheduled' ? 'scheduled' : 'claim')
181
+ const initExec = task?.execution ?? prefill?.execution
182
+ const [mode, setMode] = useState<'claim' | 'once' | 'periodic'>(
183
+ initExec?.mode === 'scheduled' ? (initExec.cron !== undefined ? 'periodic' : 'once') : 'claim',
184
+ )
182
185
  const [cron, setCron] = useState(task?.execution.cron ?? prefill?.execution?.cron ?? '0 9 * * *')
186
+ // One-shot trigger (定时执行): datetime-local string; '' = unset.
187
+ const initRunAt = initExec?.mode === 'scheduled' && initExec.runAt !== undefined ? initExec.runAt : undefined
188
+ const [runAt, setRunAt] = useState(() => {
189
+ if (initRunAt === undefined) return ''
190
+ const d = new Date(initRunAt)
191
+ if (Number.isNaN(d.getTime())) return ''
192
+ const pad = (n: number): string => String(n).padStart(2, '0')
193
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
194
+ })
183
195
  const [catalog, setCatalog] = useState<CatalogModel[]>([])
184
196
 
185
197
  // Model & reasoning effort selection:
@@ -245,10 +257,19 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
245
257
  }, [controller, editing, task?.presetId, initialPreset])
246
258
 
247
259
  // Live cron validation + next-run preview (same math as the host).
248
- const cronMatch = mode === 'scheduled' ? parseCron(cron.trim()) : null
260
+ const cronMatch = mode === 'periodic' ? parseCron(cron.trim()) : null
249
261
  const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null
250
- const cronBad = mode === 'scheduled' && (cronMatch === null || nextRun === null)
251
- const valid = title.trim().length > 0 && workspaceId !== '' && !cronBad
262
+ const cronBad = mode === 'periodic' && (cronMatch === null || nextRun === null)
263
+ const runAtMs = mode === 'once' && runAt !== '' ? new Date(runAt).getTime() : NaN
264
+ const runAtBad = mode === 'once' && (runAt === '' || Number.isNaN(runAtMs) || runAtMs <= Date.now())
265
+ const valid = title.trim().length > 0 && workspaceId !== '' && !cronBad && !runAtBad
266
+
267
+ /** Execution payload for submit: claim | periodic (cron) | one-shot (runAt ISO). */
268
+ const executionPayload = (): { mode: 'claim' | 'scheduled'; cron?: string; runAt?: string } => {
269
+ if (mode === 'periodic') return { mode: 'scheduled', cron: cron.trim() }
270
+ if (mode === 'once') return { mode: 'scheduled', runAt: new Date(runAt).toISOString() }
271
+ return { mode: 'claim' }
272
+ }
252
273
 
253
274
  // A task already in progress cannot be run again (host rejects it).
254
275
  const runBlocked = editing && task.status === 'in_progress'
@@ -309,7 +330,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
309
330
  prompt,
310
331
  urgency,
311
332
  workspaceId,
312
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
333
+ execution: executionPayload(),
313
334
  // '' in edit mode clears the pinned model back to the default.
314
335
  model: picked ?? null,
315
336
  ...(isolationOut !== undefined && !isolationLocked ? { isolation: isolationOut } : {}),
@@ -324,7 +345,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
324
345
  urgency,
325
346
  description: description.length > 0 ? description : undefined,
326
347
  prompt: prompt.length > 0 ? prompt : undefined,
327
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
348
+ execution: executionPayload(),
328
349
  model: picked,
329
350
  ...(isolationOut !== undefined ? { isolation: isolationOut } : {}),
330
351
  ...(presetOut !== undefined ? { presetId: presetOut } : {}),
@@ -351,7 +372,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
351
372
  prompt,
352
373
  urgency,
353
374
  workspaceId,
354
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
375
+ execution: executionPayload(),
355
376
  model: picked ?? null,
356
377
  ...(isolationOut !== undefined && !isolationLocked ? { isolation: isolationOut } : {}),
357
378
  presetId: presetOut ?? null,
@@ -366,7 +387,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
366
387
  urgency,
367
388
  description: description.length > 0 ? description : undefined,
368
389
  prompt: prompt.length > 0 ? prompt : undefined,
369
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
390
+ execution: executionPayload(),
370
391
  model: picked,
371
392
  ...(isolationOut !== undefined ? { isolation: isolationOut } : {}),
372
393
  ...(presetOut !== undefined ? { presetId: presetOut } : {}),
@@ -379,9 +400,15 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
379
400
  }
380
401
 
381
402
  const hint = !valid
382
- ? (title.trim().length === 0 ? t('form.hint.needTitle') : workspaceId === '' ? t('form.hint.needProject') : t('form.hint.cronBad'))
383
- : mode === 'scheduled' && nextRun !== null
403
+ ? (title.trim().length === 0
404
+ ? t('form.hint.needTitle')
405
+ : workspaceId === ''
406
+ ? t('form.hint.needProject')
407
+ : runAtBad ? t('form.hint.runAtBad') : t('form.hint.cronBad'))
408
+ : mode === 'periodic' && nextRun !== null
384
409
  ? t('form.hint.nextRun', { time: fmtTime(nextRun) })
410
+ : mode === 'once' && !Number.isNaN(runAtMs)
411
+ ? t('form.hint.onceAt', { time: fmtTime(runAtMs) })
385
412
  : editing
386
413
  ? t('form.hint.saveVersion', { v: task.version, next: task.version + 1 })
387
414
  : t('form.hint.createClaim')
@@ -516,19 +543,35 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
516
543
  </Field>
517
544
 
518
545
  <Field label={t('form.field.mode')} full>
519
- <div className="dsh-atb-mode-picker">
546
+ <div className="dsh-atb-mode-picker" data-exec="true">
520
547
  <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'claim'} onClick={() => setMode('claim')}>
521
548
  <span className="dsh-atb-mode-name">{t('form.mode.claim')}</span>
522
549
  <span className="dsh-atb-mode-hint">{t('form.mode.claimHint')}</span>
523
550
  </button>
524
- <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'scheduled'} onClick={() => setMode('scheduled')}>
525
- <span className="dsh-atb-mode-name">{t('form.mode.scheduled')}</span>
526
- <span className="dsh-atb-mode-hint">{t('form.mode.scheduledHint')}</span>
551
+ <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'once'} onClick={() => setMode('once')}>
552
+ <span className="dsh-atb-mode-name">{t('form.mode.once')}</span>
553
+ <span className="dsh-atb-mode-hint">{t('form.mode.onceHint')}</span>
554
+ </button>
555
+ <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'periodic'} onClick={() => setMode('periodic')}>
556
+ <span className="dsh-atb-mode-name">{t('form.mode.periodic')}</span>
557
+ <span className="dsh-atb-mode-hint">{t('form.mode.periodicHint')}</span>
527
558
  </button>
528
559
  </div>
529
560
  </Field>
530
561
 
531
- {mode === 'scheduled' && (
562
+ {mode === 'once' && (
563
+ <Field label={t('form.field.runAt')} required full>
564
+ <input
565
+ type="datetime-local"
566
+ className={runAtBad ? 'dsh-atb-input-bad' : undefined}
567
+ value={runAt}
568
+ onChange={e => setRunAt(e.target.value)}
569
+ spellCheck={false}
570
+ />
571
+ </Field>
572
+ )}
573
+
574
+ {mode === 'periodic' && (
532
575
  <Field label={t('form.field.cron')} required full>
533
576
  <input
534
577
  className={cronBad ? 'dsh-atb-input-bad' : undefined}
@@ -653,7 +696,7 @@ interface TaskRecordLike {
653
696
  prompt: string
654
697
  workspaceId: string
655
698
  urgency: Urgency
656
- execution: { mode: 'claim' | 'scheduled'; cron?: string }
699
+ execution: { mode: 'claim' | 'scheduled'; cron?: string; runAt?: number }
657
700
  model?: { provider: string; model: string; reasoningEffort?: string }
658
701
  isolation?: IsolationMode
659
702
  presetId?: string
@@ -693,7 +693,9 @@ export class BoardController {
693
693
  prompt: task.prompt.length > 0 ? task.prompt : undefined,
694
694
  execution: task.execution.mode === 'scheduled' && task.execution.cron !== undefined
695
695
  ? { mode: 'scheduled', cron: task.execution.cron }
696
- : { mode: 'claim' },
696
+ : task.execution.mode === 'scheduled' && task.execution.runAt !== undefined && task.execution.runAt > Date.now()
697
+ ? { mode: 'scheduled', runAt: new Date(task.execution.runAt).toISOString() }
698
+ : { mode: 'claim' },
697
699
  model: task.model,
698
700
  isolation: task.isolation,
699
701
  ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
@@ -766,7 +768,9 @@ export class BoardController {
766
768
  urgency: task.urgency,
767
769
  execution: task.execution.mode === 'scheduled' && task.execution.cron !== undefined
768
770
  ? { mode: 'scheduled', cron: task.execution.cron }
769
- : { mode: 'claim' },
771
+ : task.execution.mode === 'scheduled' && task.execution.runAt !== undefined
772
+ ? { mode: 'scheduled', runAt: new Date(task.execution.runAt).toISOString() }
773
+ : { mode: 'claim' },
770
774
  model: task.model,
771
775
  isolation: task.isolation,
772
776
  ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
@@ -280,6 +280,11 @@ export const en: TaskboardDict = {
280
280
  'form.field.mode': 'Execution mode',
281
281
  'form.mode.claim': '🤝 Claim-based',
282
282
  'form.mode.claimHint': 'Sessions in the project claim it',
283
+ 'form.mode.once': '⏰ One-shot',
284
+ 'form.mode.onceHint': 'Runs once at a set time',
285
+ 'form.mode.periodic': '🔁 Periodic',
286
+ 'form.mode.periodicHint': 'Repeats on a cron schedule',
287
+ 'form.field.runAt': 'Trigger time',
283
288
  'form.mode.scheduled': '⏰ Scheduled',
284
289
  'form.mode.scheduledHint': 'Starts automatically on schedule',
285
290
  'form.field.cron': 'Cron expression',
@@ -314,7 +319,9 @@ export const en: TaskboardDict = {
314
319
  'form.hint.needTitle': 'Enter a title',
315
320
  'form.hint.needProject': 'Select a project',
316
321
  'form.hint.cronBad': 'Invalid cron expression (min hour day month weekday)',
322
+ 'form.hint.runAtBad': 'Pick a future trigger time',
317
323
  'form.hint.nextRun': 'Next run {time}',
324
+ 'form.hint.onceAt': 'Fires once at {time}',
318
325
  'form.hint.saveVersion': 'On save: v{v} → v{next}',
319
326
  'form.hint.createClaim': 'After creation, sessions in the project can claim and run it',
320
327
  'form.action.runBlockedTitle': 'The task is running; cannot start another',
@@ -488,6 +495,9 @@ export const en: TaskboardDict = {
488
495
  'sys.sessionError': '[System] Session execution error: {error}; the task was returned to todo.',
489
496
  'sys.sessionDone': '[System] The session finished; automatically moved to in review.',
490
497
  'sys.cronDead': '[System] The cron expression {cron} has no trigger time within 4 years; scheduling disabled — fix the cron and re-enable.',
498
+ 'sys.periodicHandoff': '[System] Periodic run finished; the schedule now continues on the new todo card {nextTaskId}. Review this card, then accept.',
499
+ 'sys.spawnedFrom': '[System] The previous periodic run finished (execution {executionId}); this card carries the schedule onward. See {sourceId} for the finished round.',
500
+ 'sys.runAtMissed': '[System] The one-shot trigger time was missed (host was down); it will not re-fire. Run manually or reschedule.',
491
501
  'sys.mergeSingle': '[System] Branch {branch} merged into the main worktree (--no-ff).',
492
502
  'sys.mergeMulti': '[System] Branches merged per repo (--no-ff): {summary}',
493
503
  }
@@ -282,6 +282,11 @@ export const zh = {
282
282
  'form.field.mode': '执行方式',
283
283
  'form.mode.claim': '🤝 认领制',
284
284
  'form.mode.claimHint': '项目内会话认领',
285
+ 'form.mode.once': '⏰ 定时执行',
286
+ 'form.mode.onceHint': '指定时间只跑一次',
287
+ 'form.mode.periodic': '🔁 定期执行',
288
+ 'form.mode.periodicHint': '按 cron 到点重复开跑',
289
+ 'form.field.runAt': '触发时间',
285
290
  'form.mode.scheduled': '⏰ 定时执行',
286
291
  'form.mode.scheduledHint': '到点自动开跑',
287
292
  'form.field.cron': 'Cron 表达式',
@@ -316,7 +321,9 @@ export const zh = {
316
321
  'form.hint.needTitle': '请填写标题',
317
322
  'form.hint.needProject': '请选择项目',
318
323
  'form.hint.cronBad': 'Cron 表达式无效(分 时 日 月 周)',
324
+ 'form.hint.runAtBad': '请选择未来的触发时间',
319
325
  'form.hint.nextRun': '下次运行 {time}',
326
+ 'form.hint.onceAt': '将于 {time} 触发一次',
320
327
  'form.hint.saveVersion': '保存后版本 v{v} → v{next}',
321
328
  'form.hint.createClaim': '创建后项目内会话可认领执行',
322
329
  'form.action.runBlockedTitle': '任务正在执行中,不能重复发起',
@@ -490,6 +497,9 @@ export const zh = {
490
497
  'sys.sessionError': '[系统] 会话执行异常:{error};任务已退回待办。',
491
498
  'sys.sessionDone': '[系统] 会话执行完毕,已自动进入待验收。',
492
499
  'sys.cronDead': '[系统] 定时表达式 {cron} 在 4 年内没有可触发时间,已停用定时;请修正 cron 后重新开启。',
500
+ 'sys.periodicHandoff': '[系统] 定期任务本轮执行完毕,定时已由新待办卡 {nextTaskId} 承接,请审查本卡后验收。',
501
+ 'sys.spawnedFrom': '[系统] 定期任务上一轮执行完毕(执行 {executionId}),本卡承接定时继续下一轮;上一轮成果见 {sourceId} 的待验收。',
502
+ 'sys.runAtMissed': '[系统] 定时执行错过触发时间(主机当时未运行),本次不再补跑;可手动执行或修改定时。',
493
503
  'sys.mergeSingle': '[系统] 分支 {branch} 已合并到主工作区(--no-ff)。',
494
504
  'sys.mergeMulti': '[系统] 分支已按仓库合并(--no-ff):{summary}',
495
505
  } as const
@@ -609,6 +609,11 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
609
609
  .dsh-atb-urgency-opt[data-on="true"][data-urgency="relaxed"] { border-color: rgba(62,99,221,.65); background: rgba(62,99,221,.1); }
610
610
 
611
611
  .dsh-atb-mode-picker { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
612
+ /* Execution mode has THREE options (认领 / 定时 / 定期): keep them on one row. */
613
+ .dsh-atb-mode-picker[data-exec="true"] { grid-template-columns: repeat(3, 1fr); }
614
+ .dsh-atb-mode-picker[data-exec="true"] .dsh-atb-mode-opt { padding: 7px 8px; }
615
+ .dsh-atb-mode-picker[data-exec="true"] .dsh-atb-mode-name { font-size: 12px; }
616
+ .dsh-atb-mode-picker[data-exec="true"] .dsh-atb-mode-hint { font-size: 10px; }
612
617
  .dsh-atb-mode-opt {
613
618
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
614
619
  padding: 8px 10px; border-radius: 9px; cursor: pointer;