@goodandready/dsh-kanban 0.2.39 → 0.2.40

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.
Files changed (3) hide show
  1. package/lib/client.js +2139 -2120
  2. package/lib/config.js +2 -0
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -1075,6 +1075,8 @@ window.__ModuleLoader__.load({
1075
1075
  'field.notificationsEnabled': 'Desktop Notifications',
1076
1076
  'hint.notificationsEnabled': 'Show native desktop push notifications on key board events.',
1077
1077
  'field.audioAlertsEnabled': 'Audio Alerts',
1078
+ 'field.showHeaderChip': 'Chat Header Task Indicator',
1079
+ 'hint.showHeaderChip': 'Display active task indicator and quick actions in the session chat header.',
1078
1080
  'hint.audioAlertsEnabled': 'Play synthesized Web Audio chimes on task events.',
1079
1081
  'dup.title': 'Duplicate Tasks',
1080
1082
  'dup.found': 'Similar tasks found:',
@@ -1580,6 +1582,8 @@ window.__ModuleLoader__.load({
1580
1582
  'field.notificationsEnabled': '桌面通知',
1581
1583
  'hint.notificationsEnabled': '在关键看板事件发生时发送桌面推送通知。',
1582
1584
  'field.audioAlertsEnabled': '提示音效',
1585
+ 'field.showHeaderChip': '会话顶部任务指示器',
1586
+ 'hint.showHeaderChip': '在会话聊天顶部栏显示当前任务状态指示器及快捷操作。',
1583
1587
  'hint.audioAlertsEnabled': '在任务状态变化时播放 Web Audio 合成音效。',
1584
1588
  'dup.title': '重复任务',
1585
1589
  'dup.found': '发现相似任务:',
@@ -1655,6 +1659,7 @@ window.__ModuleLoader__.load({
1655
1659
  { field: 'timeMachineAutoSnapshot', kind: 'bool', group: 'timeMachine' },
1656
1660
  { field: 'notificationsEnabled', kind: 'bool', group: 'general' },
1657
1661
  { field: 'audioAlertsEnabled', kind: 'bool', group: 'general' },
1662
+ { field: 'showHeaderChip', kind: 'bool', group: 'general' },
1658
1663
  ]
1659
1664
 
1660
1665
  function hintKey(spec) {
@@ -2341,17 +2346,17 @@ window.__ModuleLoader__.load({
2341
2346
  if (!tTitle) return
2342
2347
  setCheckingDup(true)
2343
2348
  try {
2344
- const res = await api('/dsh-kanban/task/find-duplicates', {
2345
- method: 'POST', body: JSON.stringify({ title: tTitle, description: tDesc, board: currentBoard, excludeId: openTask?.id })
2346
- })
2347
- setDuplicates(res?.duplicates || [])
2349
+ const res = await api('/dsh-kanban/task/find-duplicates', {
2350
+ method: 'POST', body: JSON.stringify({ title: tTitle, description: tDesc, board: currentBoard, excludeId: openTask?.id })
2351
+ })
2352
+ setDuplicates(res?.duplicates || [])
2348
2353
  } catch (_) {} finally { setCheckingDup(false) }
2349
2354
  }
2350
2355
  const runTaskAction = async (path, body, okMsg) => {
2351
2356
  const res = await api(path, { method: 'POST', body: JSON.stringify(body || {}) })
2352
2357
  if (res?.task) {
2353
- setBoard((b) => b ? { ...b, tasks: (b.tasks || []).map((x) => x.id === res.task.id ? res.task : x) } : b)
2354
- setOpenTask(res.task)
2358
+ setBoard((b) => b ? { ...b, tasks: (b.tasks || []).map((x) => x.id === res.task.id ? res.task : x) } : b)
2359
+ setOpenTask(res.task)
2355
2360
  }
2356
2361
  if (okMsg) showNotice(okMsg)
2357
2362
  return res
@@ -2366,8 +2371,8 @@ window.__ModuleLoader__.load({
2366
2371
  const onSaveSpec = async () => {
2367
2372
  if (!openTask) return
2368
2373
  try {
2369
- await runTaskAction(`/dsh-kanban/task/${openTask.id}`, { spec: specText }, t('spec.saved'))
2370
- setSpecEdit(false)
2374
+ await runTaskAction(`/dsh-kanban/task/${openTask.id}`, { spec: specText }, t('spec.saved'))
2375
+ setSpecEdit(false)
2371
2376
  } catch (e) { showNotice('Save failed: ' + (e?.message || e), 'error') }
2372
2377
  }
2373
2378
  const onGenPr = async () => {
@@ -2381,8 +2386,8 @@ window.__ModuleLoader__.load({
2381
2386
  if (!openTask) return
2382
2387
  setBusyPr(true)
2383
2388
  try {
2384
- const res = await runTaskAction(`/dsh-kanban/task/${openTask.id}/create-pr`, { board: currentBoard })
2385
- showNotice(res?.prUrl ? 'PR created: ' + res.prUrl : t('pr.created'))
2389
+ const res = await runTaskAction(`/dsh-kanban/task/${openTask.id}/create-pr`, { board: currentBoard })
2390
+ showNotice(res?.prUrl ? 'PR created: ' + res.prUrl : t('pr.created'))
2386
2391
  } catch (e) { showNotice(t('pr.failed') + (e?.message || e), 'error') }
2387
2392
  finally { setBusyPr(false) }
2388
2393
  }
@@ -2433,16 +2438,16 @@ window.__ModuleLoader__.load({
2433
2438
  if (!openTask) return
2434
2439
  setDiffLoading(true)
2435
2440
  try {
2436
- const [dRes, cRes] = await Promise.all([
2437
- api('/task/' + encodeURIComponent(openTask.id) + '/diff'),
2438
- api('/task/' + encodeURIComponent(openTask.id) + '/commits'),
2439
- ])
2440
- setDiffData(dRes)
2441
- setCommitsData(cRes?.commits || [])
2441
+ const [dRes, cRes] = await Promise.all([
2442
+ api('/task/' + encodeURIComponent(openTask.id) + '/diff'),
2443
+ api('/task/' + encodeURIComponent(openTask.id) + '/commits'),
2444
+ ])
2445
+ setDiffData(dRes)
2446
+ setCommitsData(cRes?.commits || [])
2442
2447
  } catch (e) {
2443
- setError(t(e.key || e.error || 'error.unknown'))
2448
+ setError(t(e.key || e.error || 'error.unknown'))
2444
2449
  } finally {
2445
- setDiffLoading(false)
2450
+ setDiffLoading(false)
2446
2451
  }
2447
2452
  }, [openTask, t])
2448
2453
 
@@ -2472,11 +2477,11 @@ window.__ModuleLoader__.load({
2472
2477
 
2473
2478
  const load = React.useCallback(async (nextBoard) => {
2474
2479
  try {
2475
- const query = '?board=' + encodeURIComponent(nextBoard || 'main')
2476
- setState(normalizeBoard(await api('/board' + query)))
2477
- setError('')
2480
+ const query = '?board=' + encodeURIComponent(nextBoard || 'main')
2481
+ setState(normalizeBoard(await api('/board' + query)))
2482
+ setError('')
2478
2483
  } catch (e) {
2479
- setError(t(e.key || 'error.unknown'))
2484
+ setError(t(e.key || 'error.unknown'))
2480
2485
  }
2481
2486
  }, [t])
2482
2487
 
@@ -2486,21 +2491,21 @@ window.__ModuleLoader__.load({
2486
2491
  if (!nudgeTask || !nudgePrompt.trim() || nudgeSending) return
2487
2492
  setNudgeSending(true)
2488
2493
  try {
2489
- await api('/task/' + encodeURIComponent(nudgeTask.id) + '/nudge', {
2490
- method: 'POST',
2491
- body: JSON.stringify({ prompt: nudgePrompt.trim() }),
2492
- })
2493
- setNudgeSuccess(true)
2494
- setTimeout(() => {
2495
- setNudgeTask(null)
2496
- setNudgePrompt('')
2497
- setNudgeSuccess(false)
2498
- }, 1200)
2499
- load(board)
2494
+ await api('/task/' + encodeURIComponent(nudgeTask.id) + '/nudge', {
2495
+ method: 'POST',
2496
+ body: JSON.stringify({ prompt: nudgePrompt.trim() }),
2497
+ })
2498
+ setNudgeSuccess(true)
2499
+ setTimeout(() => {
2500
+ setNudgeTask(null)
2501
+ setNudgePrompt('')
2502
+ setNudgeSuccess(false)
2503
+ }, 1200)
2504
+ load(board)
2500
2505
  } catch (e) {
2501
- setError(t(e.key || e.error || 'nudge.failed'))
2506
+ setError(t(e.key || e.error || 'nudge.failed'))
2502
2507
  } finally {
2503
- setNudgeSending(false)
2508
+ setNudgeSending(false)
2504
2509
  }
2505
2510
  }, [nudgeTask, nudgePrompt, nudgeSending, load, board, t])
2506
2511
 
@@ -2510,41 +2515,41 @@ window.__ModuleLoader__.load({
2510
2515
  let reconnectTimer = null
2511
2516
 
2512
2517
  const connect = () => {
2513
- try {
2514
- es = new window.EventSource('/dsh-kanban/events')
2515
- let reloadTimer = null
2516
- const scheduleReload = () => {
2517
- if (reloadTimer) return
2518
- reloadTimer = setTimeout(() => {
2519
- reloadTimer = null
2520
- load(board)
2521
- }, 60)
2518
+ try {
2519
+ es = new window.EventSource('/dsh-kanban/events')
2520
+ let reloadTimer = null
2521
+ const scheduleReload = () => {
2522
+ if (reloadTimer) return
2523
+ reloadTimer = setTimeout(() => {
2524
+ reloadTimer = null
2525
+ load(board)
2526
+ }, 60)
2527
+ }
2528
+ es.addEventListener('board:change', scheduleReload)
2529
+ es.addEventListener('task:change', scheduleReload)
2530
+ es.onerror = () => {
2531
+ if (es) {
2532
+ try { es.close() } catch { }
2533
+ es = null
2522
2534
  }
2523
- es.addEventListener('board:change', scheduleReload)
2524
- es.addEventListener('task:change', scheduleReload)
2525
- es.onerror = () => {
2526
- if (es) {
2527
- try { es.close() } catch { }
2528
- es = null
2529
- }
2530
- if (!reconnectTimer) {
2531
- reconnectTimer = setTimeout(() => {
2532
- reconnectTimer = null
2533
- connect()
2534
- }, 5000)
2535
- }
2535
+ if (!reconnectTimer) {
2536
+ reconnectTimer = setTimeout(() => {
2537
+ reconnectTimer = null
2538
+ connect()
2539
+ }, 5000)
2536
2540
  }
2537
- } catch { }
2541
+ }
2542
+ } catch { }
2538
2543
  }
2539
2544
 
2540
2545
  connect()
2541
2546
 
2542
2547
  return () => {
2543
- if (reconnectTimer) clearTimeout(reconnectTimer)
2544
- if (es) {
2545
- try { es.close() } catch { }
2546
- es = null
2547
- }
2548
+ if (reconnectTimer) clearTimeout(reconnectTimer)
2549
+ if (es) {
2550
+ try { es.close() } catch { }
2551
+ es = null
2552
+ }
2548
2553
  }
2549
2554
  }, [load, board])
2550
2555
 
@@ -2553,10 +2558,10 @@ window.__ModuleLoader__.load({
2553
2558
  const toggle = props.toggle
2554
2559
  if (!toggle || typeof toggle.takeWanted !== 'function') return undefined
2555
2560
  const pull = () => {
2556
- const asked = toggle.takeWanted()
2557
- if (!asked || !asked.id) return
2558
- setBoard(asked.board)
2559
- setWanted(asked.id)
2561
+ const asked = toggle.takeWanted()
2562
+ if (!asked || !asked.id) return
2563
+ setBoard(asked.board)
2564
+ setWanted(asked.id)
2560
2565
  }
2561
2566
  pull()
2562
2567
  return toggle.subscribe(pull)
@@ -2572,10 +2577,10 @@ window.__ModuleLoader__.load({
2572
2577
 
2573
2578
  React.useEffect(() => {
2574
2579
  const onKey = (e) => {
2575
- if (e.key !== 'Escape') return
2576
- if (dialog !== null) { setDialog(null); return }
2577
- if (openTask !== null) { setOpenTask(null); return }
2578
- if (props.onClose) props.onClose()
2580
+ if (e.key !== 'Escape') return
2581
+ if (dialog !== null) { setDialog(null); return }
2582
+ if (openTask !== null) { setOpenTask(null); return }
2583
+ if (props.onClose) props.onClose()
2579
2584
  }
2580
2585
  document.addEventListener('keydown', onKey)
2581
2586
  return () => document.removeEventListener('keydown', onKey)
@@ -2584,40 +2589,40 @@ window.__ModuleLoader__.load({
2584
2589
  React.useEffect(() => {
2585
2590
  let alive = true
2586
2591
  api('/models')
2587
- .then((out) => {
2588
- if (!alive || !out) return
2589
- const list = out.providers || []
2590
- setProviders(list)
2591
- const saved = recallLaunch(board)
2592
- const provider = stillOffered(saved.provider, list.map((p) => p.id), '')
2593
- if (provider !== '') {
2594
- setProvider(provider)
2595
- setModel(typeof saved.model === 'string' ? saved.model : '')
2596
- } else if (out.current) {
2597
- setProvider(out.current.provider || '')
2598
- setModel(out.current.model || '')
2599
- }
2600
- })
2601
- .catch(() => {})
2592
+ .then((out) => {
2593
+ if (!alive || !out) return
2594
+ const list = out.providers || []
2595
+ setProviders(list)
2596
+ const saved = recallLaunch(board)
2597
+ const provider = stillOffered(saved.provider, list.map((p) => p.id), '')
2598
+ if (provider !== '') {
2599
+ setProvider(provider)
2600
+ setModel(typeof saved.model === 'string' ? saved.model : '')
2601
+ } else if (out.current) {
2602
+ setProvider(out.current.provider || '')
2603
+ setModel(out.current.model || '')
2604
+ }
2605
+ })
2606
+ .catch(() => {})
2602
2607
  return () => { alive = false }
2603
2608
  }, [])
2604
2609
 
2605
2610
  React.useEffect(() => {
2606
2611
  let alive = true
2607
2612
  api('/whoami')
2608
- .then((out) => { if (alive && out) setMe(typeof out.login === 'string' ? out.login : '') })
2609
- .catch(() => {})
2613
+ .then((out) => { if (alive && out) setMe(typeof out.login === 'string' ? out.login : '') })
2614
+ .catch(() => {})
2610
2615
  api('/presets')
2611
- .then((out) => {
2612
- if (!alive || !out) return
2613
- const rows = out.agentPresets || []
2614
- const access = out.access || []
2615
- setPresets({ agentPresets: rows, access })
2616
- const saved = recallLaunch(board)
2617
- setAgentPreset(stillOffered(saved.agentPreset, rows.map((x) => x.id), out.agentPresetDefault || ''))
2618
- setPermission(stillOffered(saved.permission, access.map((x) => x.value), out.accessDefault || ''))
2619
- })
2620
- .catch(() => {})
2616
+ .then((out) => {
2617
+ if (!alive || !out) return
2618
+ const rows = out.agentPresets || []
2619
+ const access = out.access || []
2620
+ setPresets({ agentPresets: rows, access })
2621
+ const saved = recallLaunch(board)
2622
+ setAgentPreset(stillOffered(saved.agentPreset, rows.map((x) => x.id), out.agentPresetDefault || ''))
2623
+ setPermission(stillOffered(saved.permission, access.map((x) => x.value), out.accessDefault || ''))
2624
+ })
2625
+ .catch(() => {})
2621
2626
  return () => { alive = false }
2622
2627
  }, [])
2623
2628
 
@@ -2625,26 +2630,26 @@ window.__ModuleLoader__.load({
2625
2630
  let alive = true
2626
2631
  if (provider === '') { setModels([]); return () => { alive = false } }
2627
2632
  api('/models?provider=' + encodeURIComponent(provider))
2628
- .then((out) => {
2629
- if (!alive || !out) return
2630
- const list = out.models || []
2631
- setModels(list)
2632
- setModel((cur) => stillOffered(cur, list.map((m) => m.id), ''))
2633
- })
2634
- .catch(() => { if (alive) setModels([]) })
2633
+ .then((out) => {
2634
+ if (!alive || !out) return
2635
+ const list = out.models || []
2636
+ setModels(list)
2637
+ setModel((cur) => stillOffered(cur, list.map((m) => m.id), ''))
2638
+ })
2639
+ .catch(() => { if (alive) setModels([]) })
2635
2640
  return () => { alive = false }
2636
2641
  }, [provider])
2637
2642
 
2638
2643
  const commitMove = React.useCallback(async (task, column, where) => {
2639
2644
  try {
2640
- await api('/task/' + encodeURIComponent(task.id) + '/move', {
2641
- method: 'POST',
2642
- body: JSON.stringify({ column, beforeId: where.beforeId, afterId: where.afterId }),
2643
- })
2644
- await load(board)
2645
+ await api('/task/' + encodeURIComponent(task.id) + '/move', {
2646
+ method: 'POST',
2647
+ body: JSON.stringify({ column, beforeId: where.beforeId, afterId: where.afterId }),
2648
+ })
2649
+ await load(board)
2645
2650
  } catch (e) {
2646
- setError(t(e.key || 'error.unknown'))
2647
- await load(board)
2651
+ setError(t(e.key || 'error.unknown'))
2652
+ await load(board)
2648
2653
  }
2649
2654
  }, [load, t])
2650
2655
 
@@ -2672,8 +2677,8 @@ window.__ModuleLoader__.load({
2672
2677
  setEditPriority(typeof task?.priority === 'string' ? task.priority : '')
2673
2678
  setEditLabels(Array.isArray(task?.labels) ? task.labels.join(', ') : '')
2674
2679
  setEditDue(typeof task?.dueAt === 'number' && task.dueAt > 0
2675
- ? new Date(task.dueAt).toISOString().slice(0, 10)
2676
- : '')
2680
+ ? new Date(task.dueAt).toISOString().slice(0, 10)
2681
+ : '')
2677
2682
  setEditCron(typeof task?.cron === 'string' ? task.cron : '')
2678
2683
  setTriageOpen(false)
2679
2684
  setTriageText('')
@@ -2682,17 +2687,17 @@ window.__ModuleLoader__.load({
2682
2687
  setNewSubtaskTitle('')
2683
2688
  setSubtasks([])
2684
2689
  api('/task/' + encodeURIComponent(task.id) + '/subtasks')
2685
- .then((out) => setSubtasks((out && out.subtasks) || []))
2686
- .catch(() => setSubtasks([]))
2690
+ .then((out) => setSubtasks((out && out.subtasks) || []))
2691
+ .catch(() => setSubtasks([]))
2687
2692
  api('/task/' + encodeURIComponent(task.id) + '/body')
2688
- .then((out) => setBody((out && out.blocks) || []))
2689
- .catch(() => setBody([]))
2693
+ .then((out) => setBody((out && out.blocks) || []))
2694
+ .catch(() => setBody([]))
2690
2695
  api('/task/' + encodeURIComponent(task.id) + '/message')
2691
- .then((out) => setDraftText((out && out.text) || ''))
2692
- .catch(() => setDraftText(''))
2696
+ .then((out) => setDraftText((out && out.text) || ''))
2697
+ .catch(() => setDraftText(''))
2693
2698
  try {
2694
- const out = await api('/task/' + encodeURIComponent(task.id) + '/log')
2695
- setLog(out.transitions || [])
2699
+ const out = await api('/task/' + encodeURIComponent(task.id) + '/log')
2700
+ setLog(out.transitions || [])
2696
2701
  } catch { setLog([]) }
2697
2702
  }, [])
2698
2703
  openPanelRef.current = openPanel
@@ -2702,927 +2707,907 @@ window.__ModuleLoader__.load({
2702
2707
 
2703
2708
  if (state === null) {
2704
2709
  return h('div', { className: 'dkb-screen' },
2705
- h('p', { className: 'dkb-note' }, error || t('board.loading')))
2710
+ h('p', { className: 'dkb-note' }, error || t('board.loading')))
2706
2711
  }
2707
2712
 
2708
2713
  const boards = (state && state.boards) || []
2709
2714
  const bar = h('div', { className: 'dkb-bar' },
2710
2715
  h('button', {
2711
- type: 'button', className: 'dkb-back', title: t('board.backHint'),
2712
- onClick: () => { if (props.onClose) props.onClose() },
2716
+ type: 'button', className: 'dkb-back', title: t('board.backHint'),
2717
+ onClick: () => { if (props.onClose) props.onClose() },
2713
2718
  },
2714
- h('span', {
2715
- className: 'dkb-backIcon',
2716
- dangerouslySetInnerHTML: { __html: BACK_ICON },
2717
- }),
2718
- h('span', null, t('board.back')),
2719
+ h('span', {
2720
+ className: 'dkb-backIcon',
2721
+ dangerouslySetInnerHTML: { __html: BACK_ICON },
2722
+ }),
2723
+ h('span', null, t('board.back')),
2719
2724
  ),
2720
2725
  h('span', { className: 'dkb-barTitle' }, t('section.label')),
2721
2726
  state && state.remoteMode
2722
- ? h('span', {
2723
- className: 'dkb-tag',
2724
- style: {
2725
- background: 'var(--dsw-alias-state-business-tertiary, #ddf4ff)',
2726
- color: 'var(--dsw-alias-state-business-primary, #0969da)',
2727
- borderColor: 'var(--dsw-alias-state-business-tertiary, #54aeff)',
2728
- fontWeight: '600',
2729
- fontSize: '11px',
2730
- },
2731
- title: 'Operating in remote mode against server 111',
2732
- }, '🌐 ' + t('remote.badge'))
2733
- : null,
2734
- boards.length > 1
2735
- ? h('select', {
2736
- className: 'dkb-input', style: { width: 'auto' }, value: board,
2737
- onChange: (e) => {
2738
- setOpenTask(null); setCollapsed({}); setFilters({}); setBoard(e.target.value)
2727
+ ? h('span', {
2728
+ className: 'dkb-tag',
2729
+ style: {
2730
+ background: 'var(--dsw-alias-state-business-tertiary, #ddf4ff)',
2731
+ color: 'var(--dsw-alias-state-business-primary, #0969da)',
2732
+ borderColor: 'var(--dsw-alias-state-business-tertiary, #54aeff)',
2733
+ fontWeight: '600',
2734
+ fontSize: '11px',
2739
2735
  },
2740
- }, boards.map((b) => h('option', { value: b.id, key: b.id }, boardTitle(b, t))))
2741
- : null,
2736
+ title: 'Operating in remote mode against server 111',
2737
+ }, '🌐 ' + t('remote.badge'))
2738
+ : null,
2739
+ boards.length > 1
2740
+ ? h('select', {
2741
+ className: 'dkb-input', style: { width: 'auto' }, value: board,
2742
+ onChange: (e) => {
2743
+ setOpenTask(null); setCollapsed({}); setFilters({}); setBoard(e.target.value)
2744
+ },
2745
+ }, boards.map((b) => h('option', { value: b.id, key: b.id }, boardTitle(b, t))))
2746
+ : null,
2742
2747
  (state.facets || []).map((facet) => {
2743
- const picked = filters[facet.ns] || []
2744
- const label = facetLabel(facet.ns, t)
2745
- return h('div', { className: 'dkb-facet', key: facet.ns },
2746
- h('button', {
2747
- type: 'button',
2748
- className: 'dkb-facetHead' + (picked.length ? ' dkb-facetOn' : ''),
2749
- 'aria-expanded': openFacet === facet.ns,
2750
- onClick: () => setOpenFacet((cur) => (cur === facet.ns ? '' : facet.ns)),
2751
- }, picked.length ? label + ' · ' + picked.length : label),
2752
- openFacet === facet.ns
2753
- ? h('div', { className: 'dkb-facetList' },
2754
- facet.values.map((v) => h('label', {
2755
- className: 'dkb-facetRow', key: v.value,
2756
- },
2757
- h('input', {
2758
- type: 'checkbox',
2759
- checked: picked.indexOf(v.value) >= 0,
2760
- onChange: () => setFilters((cur) => toggleValue(cur, facet.ns, v.value)),
2761
- }),
2762
- h('span', { className: 'dkb-facetName' }, v.value),
2763
- h('span', { className: 'dkb-count' }, String(v.count)),
2764
- )))
2765
- : null,
2766
- )
2748
+ const picked = filters[facet.ns] || []
2749
+ const label = facetLabel(facet.ns, t)
2750
+ return h('div', { className: 'dkb-facet', key: facet.ns },
2751
+ h('button', {
2752
+ type: 'button',
2753
+ className: 'dkb-facetHead' + (picked.length ? ' dkb-facetOn' : ''),
2754
+ 'aria-expanded': openFacet === facet.ns,
2755
+ onClick: () => setOpenFacet((cur) => (cur === facet.ns ? '' : facet.ns)),
2756
+ }, picked.length ? label + ' · ' + picked.length : label),
2757
+ openFacet === facet.ns
2758
+ ? h('div', { className: 'dkb-facetList' },
2759
+ facet.values.map((v) => h('label', {
2760
+ className: 'dkb-facetRow', key: v.value,
2761
+ },
2762
+ h('input', {
2763
+ type: 'checkbox',
2764
+ checked: picked.indexOf(v.value) >= 0,
2765
+ onChange: () => setFilters((cur) => toggleValue(cur, facet.ns, v.value)),
2766
+ }),
2767
+ h('span', { className: 'dkb-facetName' }, v.value),
2768
+ h('span', { className: 'dkb-count' }, String(v.count)),
2769
+ )))
2770
+ : null,
2771
+ )
2767
2772
  }),
2768
2773
  (() => {
2769
- const waiting = state.tasks.filter((x) => x.state === 'waiting').length
2770
- if (waiting === 0) return null
2771
- return h('button', {
2772
- type: 'button',
2773
- className: 'dkb-waitCount' + (onlyWaiting ? ' dkb-save' : ''),
2774
- title: t('board.waitCountHint'),
2775
- onClick: () => setOnlyWaiting((cur) => !cur),
2776
- }, t('board.waitCount', { n: waiting }))
2774
+ const waiting = state.tasks.filter((x) => x.state === 'waiting').length
2775
+ if (waiting === 0) return null
2776
+ return h('button', {
2777
+ type: 'button',
2778
+ className: 'dkb-waitCount' + (onlyWaiting ? ' dkb-save' : ''),
2779
+ title: t('board.waitCountHint'),
2780
+ onClick: () => setOnlyWaiting((cur) => !cur),
2781
+ }, t('board.waitCount', { n: waiting }))
2777
2782
  })(),
2778
2783
  (() => {
2779
- const overdue = state.tasks.filter((x) => x.overdue === true).length
2780
- if (overdue === 0) return null
2781
- return h('button', {
2782
- type: 'button',
2783
- className: 'dkb-waitCount' + (onlyOverdue ? ' dkb-save' : ''),
2784
- title: t('board.overdueHint'),
2785
- onClick: () => setOnlyOverdue((cur) => !cur),
2786
- }, t('board.overdueCount', { n: overdue }))
2784
+ const overdue = state.tasks.filter((x) => x.overdue === true).length
2785
+ if (overdue === 0) return null
2786
+ return h('button', {
2787
+ type: 'button',
2788
+ className: 'dkb-waitCount' + (onlyOverdue ? ' dkb-save' : ''),
2789
+ title: t('board.overdueHint'),
2790
+ onClick: () => setOnlyOverdue((cur) => !cur),
2791
+ }, t('board.overdueCount', { n: overdue }))
2787
2792
  })(),
2788
2793
  (() => {
2789
- if (me === '') return null
2790
- const mine = state.tasks.filter((x) => x.assignee === me).length
2791
- if (mine === 0) return null
2792
- return h('button', {
2793
- type: 'button',
2794
- className: 'dkb-waitCount' + (onlyMine ? ' dkb-save' : ''),
2795
- title: t('board.mineHint', { who: me }),
2796
- onClick: () => setOnlyMine((cur) => !cur),
2797
- }, t('board.mine', { n: mine }))
2794
+ if (me === '') return null
2795
+ const mine = state.tasks.filter((x) => x.assignee === me).length
2796
+ if (mine === 0) return null
2797
+ return h('button', {
2798
+ type: 'button',
2799
+ className: 'dkb-waitCount' + (onlyMine ? ' dkb-save' : ''),
2800
+ title: t('board.mineHint', { who: me }),
2801
+ onClick: () => setOnlyMine((cur) => !cur),
2802
+ }, t('board.mine', { n: mine }))
2798
2803
  })(),
2799
2804
  h('select', {
2800
- className: 'dkb-input', style: { width: 'auto' }, value: grouping,
2801
- onChange: (e) => setGrouping(e.target.value),
2805
+ className: 'dkb-input', style: { width: 'auto' }, value: grouping,
2806
+ onChange: (e) => setGrouping(e.target.value),
2802
2807
  },
2803
- h('option', { value: 'repo' }, t('group.repo')),
2804
- h('option', { value: 'assignee' }, t('group.assignee')),
2808
+ h('option', { value: 'repo' }, t('group.repo')),
2809
+ h('option', { value: 'assignee' }, t('group.assignee')),
2805
2810
  ),
2806
2811
  h('select', {
2807
- className: 'dkb-input', style: { width: 'auto' }, value: order,
2808
- onChange: (e) => setOrder(e.target.value),
2812
+ className: 'dkb-input', style: { width: 'auto' }, value: order,
2813
+ onChange: (e) => setOrder(e.target.value),
2809
2814
  },
2810
- h('option', { value: 'manual' }, t('order.manual')),
2811
- h('option', { value: 'old' }, t('order.old')),
2812
- h('option', { value: 'new' }, t('order.new')),
2815
+ h('option', { value: 'manual' }, t('order.manual')),
2816
+ h('option', { value: 'old' }, t('order.old')),
2817
+ h('option', { value: 'new' }, t('order.new')),
2813
2818
  ),
2814
2819
  h('button', {
2815
- type: 'button', className: metrics ? 'dkb-save' : 'dkb-discard',
2816
- onClick: async () => {
2817
- if (metrics) { setMetrics(null); return }
2818
- try {
2819
- setArchive(null)
2820
- setMetrics(await api('/metrics?board=' + encodeURIComponent(board)))
2821
- } catch (e) { setError(t(e.key || 'error.unknown')) }
2822
- },
2820
+ type: 'button', className: metrics ? 'dkb-save' : 'dkb-discard',
2821
+ onClick: async () => {
2822
+ if (metrics) { setMetrics(null); return }
2823
+ try {
2824
+ setArchive(null)
2825
+ setMetrics(await api('/metrics?board=' + encodeURIComponent(board)))
2826
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
2827
+ },
2823
2828
  }, t('board.metrics')),
2824
2829
  h('button', {
2825
- type: 'button', className: archive ? 'dkb-save' : 'dkb-discard',
2826
- onClick: async () => {
2827
- if (archive) { setArchive(null); return }
2828
- try {
2829
- setMetrics(null)
2830
- const out = await api('/archive')
2831
- setArchive(Array.isArray(out.tasks) ? out.tasks : [])
2832
- } catch (e) { setError(t(e.key || 'error.unknown')) }
2833
- },
2830
+ type: 'button', className: archive ? 'dkb-save' : 'dkb-discard',
2831
+ onClick: async () => {
2832
+ if (archive) { setArchive(null); return }
2833
+ try {
2834
+ setMetrics(null)
2835
+ const out = await api('/archive')
2836
+ setArchive(Array.isArray(out.tasks) ? out.tasks : [])
2837
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
2838
+ },
2834
2839
  }, t('board.archive')),
2835
2840
  h('button', {
2836
- type: 'button', className: 'dkb-discard',
2837
- title: t('board.exportHint'),
2838
- onClick: async () => {
2839
- try {
2840
- const out = await api('/snapshot')
2841
- const blob = new Blob([JSON.stringify(out, null, 2)], { type: 'application/json' })
2842
- const url = URL.createObjectURL(blob)
2843
- const a = document.createElement('a')
2844
- a.href = url
2845
- a.download = 'kanban-' + new Date().toISOString().slice(0, 10) + '.json'
2846
- a.click()
2847
- URL.revokeObjectURL(url)
2848
- } catch (e) { setError(t(e.key || 'error.unknown')) }
2849
- },
2841
+ type: 'button', className: 'dkb-discard',
2842
+ title: t('board.exportHint'),
2843
+ onClick: async () => {
2844
+ try {
2845
+ const out = await api('/snapshot')
2846
+ const blob = new Blob([JSON.stringify(out, null, 2)], { type: 'application/json' })
2847
+ const url = URL.createObjectURL(blob)
2848
+ const a = document.createElement('a')
2849
+ a.href = url
2850
+ a.download = 'kanban-' + new Date().toISOString().slice(0, 10) + '.json'
2851
+ a.click()
2852
+ URL.revokeObjectURL(url)
2853
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
2854
+ },
2850
2855
  }, t('board.export')),
2851
2856
  h('label', { className: 'dkb-discard dkb-import' },
2852
- t('board.import'),
2853
- h('input', {
2854
- type: 'file', accept: 'application/json,.json', style: { display: 'none' },
2855
- onChange: async (e) => {
2856
- const file = e.target.files && e.target.files[0]
2857
- e.target.value = ''
2858
- if (!file) return
2859
- try {
2860
- const input = JSON.parse(await file.text())
2861
- const out = await api('/snapshot', { method: 'POST', body: JSON.stringify(input) })
2862
- setError(out.error ? t('error.' + out.error) : t('board.imported', { n: out.imported }))
2863
- await load(board)
2864
- } catch { setError(t('error.badImport')) }
2865
- },
2866
- })),
2857
+ t('board.import'),
2858
+ h('input', {
2859
+ type: 'file', accept: 'application/json,.json', style: { display: 'none' },
2860
+ onChange: async (e) => {
2861
+ const file = e.target.files && e.target.files[0]
2862
+ e.target.value = ''
2863
+ if (!file) return
2864
+ try {
2865
+ const input = JSON.parse(await file.text())
2866
+ const out = await api('/snapshot', { method: 'POST', body: JSON.stringify(input) })
2867
+ setError(out.error ? t('error.' + out.error) : t('board.imported', { n: out.imported }))
2868
+ await load(board)
2869
+ } catch { setError(t('error.badImport')) }
2870
+ },
2871
+ })),
2867
2872
  anySelected(filters) || query.trim() !== ''
2868
- ? h('span', { className: 'dkb-shown' }, t('board.shown', {
2869
- shown: state.tasks.filter((x) => matchesFilters(x, filters) && matchesQuery(x, query)).length,
2870
- total: state.tasks.length,
2871
- }))
2872
- : null,
2873
+ ? h('span', { className: 'dkb-shown' }, t('board.shown', {
2874
+ shown: state.tasks.filter((x) => matchesFilters(x, filters) && matchesQuery(x, query)).length,
2875
+ total: state.tasks.length,
2876
+ }))
2877
+ : null,
2873
2878
  anySelected(filters)
2874
- ? h('button', {
2875
- type: 'button', className: 'dkb-discard',
2876
- onClick: () => { setFilters({}); setOpenFacet('') },
2877
- }, t('board.clearFilters'))
2878
- : null,
2879
+ ? h('button', {
2880
+ type: 'button', className: 'dkb-discard',
2881
+ onClick: () => { setFilters({}); setOpenFacet('') },
2882
+ }, t('board.clearFilters'))
2883
+ : null,
2879
2884
  h('input', {
2880
- className: 'dkb-search', value: query, placeholder: t('board.search'),
2881
- onChange: (e) => setQuery(e.target.value),
2885
+ className: 'dkb-search', value: query, placeholder: t('board.search'),
2886
+ onChange: (e) => setQuery(e.target.value),
2882
2887
  }),
2883
2888
  (() => {
2884
- const line = syncing
2885
- ? { tone: 'idle', text: t('sync.running'), title: '' }
2886
- : syncLine(state.sync, state.now, t)
2887
- return h('span', {
2888
- className: 'dkb-sync' + (line.tone === 'bad' ? ' dkb-syncBad' : ''),
2889
- title: line.title,
2890
- }, line.text)
2889
+ const line = syncing
2890
+ ? { tone: 'idle', text: t('sync.running'), title: '' }
2891
+ : syncLine(state.sync, state.now, t)
2892
+ return h('span', {
2893
+ className: 'dkb-sync' + (line.tone === 'bad' ? ' dkb-syncBad' : ''),
2894
+ title: line.title,
2895
+ }, line.text)
2891
2896
  })(),
2892
2897
  h('button', {
2893
- type: 'button', className: 'dkb-discard', disabled: syncing,
2894
- onClick: async () => {
2895
- setSyncing(true)
2896
- try {
2897
- await api('/sync', { method: 'POST', body: '{}' })
2898
- await load(board)
2899
- } catch (e) {
2900
- setError(t(e.key || 'error.unknown'))
2901
- } finally {
2902
- setSyncing(false)
2903
- }
2904
- },
2898
+ type: 'button', className: 'dkb-discard', disabled: syncing,
2899
+ onClick: async () => {
2900
+ setSyncing(true)
2901
+ try {
2902
+ await api('/sync', { method: 'POST', body: '{}' })
2903
+ await load(board)
2904
+ } catch (e) {
2905
+ setError(t(e.key || 'error.unknown'))
2906
+ } finally {
2907
+ setSyncing(false)
2908
+ }
2909
+ },
2905
2910
  }, syncing ? t('board.syncing') : t('board.sync')),
2906
2911
  h('button', { type: 'button', className: 'dkb-discard', onClick: () => load(board) }, t('board.refresh')),
2907
2912
  h('div', { className: 'dkb-quickFilterBar' },
2908
- [
2909
- { id: 'all', label: t('filter.all') },
2910
- { id: 'active', label: t('filter.active') },
2911
- { id: 'review', label: t('filter.review') },
2912
- { id: 'epics', label: t('filter.epics') },
2913
- { id: 'stale', label: t('filter.stale') },
2914
- ].map((chip) => h('button', {
2915
- type: 'button',
2916
- key: chip.id,
2917
- className: 'dkb-filterChip' + (quickFilter === chip.id ? ' dkb-filterChipActive' : ''),
2918
- onClick: () => setQuickFilter(chip.id),
2919
- }, chip.label))),
2913
+ [
2914
+ { id: 'all', label: t('filter.all') },
2915
+ { id: 'active', label: t('filter.active') },
2916
+ { id: 'review', label: t('filter.review') },
2917
+ { id: 'epics', label: t('filter.epics') },
2918
+ { id: 'stale', label: t('filter.stale') },
2919
+ ].map((chip) => h('button', {
2920
+ type: 'button',
2921
+ key: chip.id,
2922
+ className: 'dkb-filterChip' + (quickFilter === chip.id ? ' dkb-filterChipActive' : ''),
2923
+ onClick: () => setQuickFilter(chip.id),
2924
+ }, chip.label))),
2920
2925
  Object.keys(picked).length > 0
2921
- ? h('span', { className: 'dkb-shown' },
2922
- t('board.pickedCount', { n: Object.keys(picked).length }))
2923
- : null,
2926
+ ? h('span', { className: 'dkb-shown' },
2927
+ t('board.pickedCount', { n: Object.keys(picked).length }))
2928
+ : null,
2924
2929
  Object.keys(picked).length > 0
2925
- ? h('button', {
2926
- type: 'button', className: 'dkb-discard',
2927
- onClick: () => setPicked({}),
2928
- }, t('board.pickNone'))
2929
- : null,
2930
+ ? h('button', {
2931
+ type: 'button', className: 'dkb-discard',
2932
+ onClick: () => setPicked({}),
2933
+ }, t('board.pickNone'))
2934
+ : null,
2930
2935
  Object.keys(picked).length > 0
2931
- ? h('button', {
2932
- type: 'button', className: 'dkb-save',
2933
- onClick: () => setDialog({
2934
- mode: 'batch',
2935
- ids: Object.keys(picked),
2936
- provider: '', model: '', text: '', error: '',
2937
- }),
2938
- }, t('board.pickStart'))
2939
- : null,
2936
+ ? h('button', {
2937
+ type: 'button', className: 'dkb-save',
2938
+ onClick: () => setDialog({
2939
+ mode: 'batch',
2940
+ ids: Object.keys(picked),
2941
+ provider: '', model: '', text: '', error: '',
2942
+ }),
2943
+ }, t('board.pickStart'))
2944
+ : null,
2940
2945
  Object.keys(picked).length > 0
2941
- ? h('button', {
2942
- type: 'button', className: 'dkb-discard',
2943
- onClick: async () => {
2944
- const ids = Object.keys(picked)
2945
- const out = await applyToEach(ids, (id) => api(
2946
- '/task/' + encodeURIComponent(id) + '/archive', { method: 'POST', body: '{}' },
2947
- ))
2948
- if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2949
- else setPicked({})
2950
- await load(board)
2951
- },
2952
- }, t('board.pickArchive'))
2953
- : null,
2946
+ ? h('button', {
2947
+ type: 'button', className: 'dkb-discard',
2948
+ onClick: async () => {
2949
+ const ids = Object.keys(picked)
2950
+ const out = await applyToEach(ids, (id) => api(
2951
+ '/task/' + encodeURIComponent(id) + '/archive', { method: 'POST', body: '{}' },
2952
+ ))
2953
+ if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2954
+ else setPicked({})
2955
+ await load(board)
2956
+ },
2957
+ }, t('board.pickArchive'))
2958
+ : null,
2954
2959
  Object.keys(picked).length > 0
2955
- ? h('button', {
2956
- type: 'button', className: 'dkb-discard',
2957
- onClick: async () => {
2958
- const ids = Object.keys(picked)
2959
- const out = await applyToEach(ids, (id) => api(
2960
- '/task/' + encodeURIComponent(id) + '/unqueue', { method: 'POST', body: '{}' },
2961
- ))
2962
- if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2963
- else setPicked({})
2964
- await load(board)
2965
- },
2966
- }, t('board.pickUnqueue'))
2967
- : null,
2960
+ ? h('button', {
2961
+ type: 'button', className: 'dkb-discard',
2962
+ onClick: async () => {
2963
+ const ids = Object.keys(picked)
2964
+ const out = await applyToEach(ids, (id) => api(
2965
+ '/task/' + encodeURIComponent(id) + '/unqueue', { method: 'POST', body: '{}' },
2966
+ ))
2967
+ if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2968
+ else setPicked({})
2969
+ await load(board)
2970
+ },
2971
+ }, t('board.pickUnqueue'))
2972
+ : null,
2968
2973
  Object.keys(picked).length > 0
2969
- ? h('select', {
2970
- className: 'dkb-input',
2971
- onChange: async (e) => {
2972
- const column = e.target.value
2973
- if (!column) return
2974
- const ids = Object.keys(picked)
2975
- const out = await applyToEach(ids, (id) => api(
2976
- '/task/' + encodeURIComponent(id) + '/move',
2977
- { method: 'POST', body: JSON.stringify({ column }) },
2978
- ))
2979
- if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2980
- else setPicked({})
2981
- await load(board)
2982
- },
2974
+ ? h('select', {
2975
+ className: 'dkb-input',
2976
+ onChange: async (e) => {
2977
+ const column = e.target.value
2978
+ if (!column) return
2979
+ const ids = Object.keys(picked)
2980
+ const out = await applyToEach(ids, (id) => api(
2981
+ '/task/' + encodeURIComponent(id) + '/move',
2982
+ { method: 'POST', body: JSON.stringify({ column }) },
2983
+ ))
2984
+ if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2985
+ else setPicked({})
2986
+ await load(board)
2983
2987
  },
2984
- h('option', { value: '' }, t('board.pickMove')),
2985
- (state.columns || []).map((c) => h('option', {
2986
- value: c.id, key: c.id,
2987
- }, columnTitle(c, t))),
2988
- )
2989
- : null,
2988
+ },
2989
+ h('option', { value: '' }, t('board.pickMove')),
2990
+ (state.columns || []).map((c) => h('option', {
2991
+ value: c.id, key: c.id,
2992
+ }, columnTitle(c, t))),
2993
+ )
2994
+ : null,
2990
2995
  h('button', { type: 'button', className: 'dkb-save', onClick: () => setDialog({ mode: 'own', project: false, repo: '', newRepo: '', title: '', full: '', repos: null, issues: null, error: '' }) }, t('board.newTask')),
2991
2996
  )
2992
2997
 
2993
2998
  const packs = packInfo(state.tasks)
2994
2999
  const columns = h('div', { className: 'dkb-cols' },
2995
3000
  state.columns.map((col) => {
2996
- const all = tasksOf(state.tasks, col.id).filter((task) => matchesFilters(task, filters))
2997
- const items = sortTasks(
2998
- all.filter((task) => matchesQuery(task, debouncedQuery))
2999
- .filter((task) => !onlyWaiting || task.state === 'waiting')
3000
- .filter((task) => !onlyOverdue || task.overdue === true)
3001
- .filter((task) => !onlyMine || task.assignee === me)
3002
- .filter((task) => {
3003
- if (quickFilter === 'active') return task.column === 'in-progress'
3004
- if (quickFilter === 'review') return task.column === 'review'
3005
- if (quickFilter === 'epics') return task.isEpic === true
3006
- if (quickFilter === 'stale') return task.stale === true
3007
- return true
3008
- }),
3009
- order,
3010
- )
3011
- const groupKeyOf = (task) => (grouping === 'assignee'
3012
- ? (task.assignee || '')
3013
- : (task.repo || ''))
3014
- const ordered = grouping === 'assignee'
3015
- ? groupByAssignee(items).flatMap((one) => one.tasks)
3016
- : items
3017
- const limit = col.limit
3018
- const overLimit = limit !== undefined && limit !== null && all.length > limit
3019
- const slot = (index) => h('div', {
3020
- className: 'dkb-slot' + (dropAt && dropAt.column === col.id && dropAt.index === index ? ' dkb-slotOn' : ''),
3021
- key: 's' + index,
3022
- onDragOver: (e) => {
3023
- e.preventDefault(); e.stopPropagation()
3024
- setOverColumn(col.id)
3025
- setDropAt({ column: col.id, index })
3026
- },
3027
- onDrop: (e) => { e.preventDefault(); e.stopPropagation(); drop(col.id, index) },
3028
- })
3029
-
3030
- const rows = []
3031
- rows.push(slot(0))
3032
- let group = null
3033
- const sizes = new Map()
3034
- for (const x of ordered) sizes.set(groupKeyOf(x), (sizes.get(groupKeyOf(x)) || 0) + 1)
3035
- const manyGroups = sizes.size > 1
3036
-
3037
- ordered.forEach((task, i) => {
3038
- const repo = groupKeyOf(task)
3039
- if (manyGroups && repo !== group) {
3040
- group = repo
3041
- const shut = groups[col.id + '/' + repo] === true
3042
- rows.push(h('button', {
3043
- type: 'button', className: 'dkb-groupHead', key: 'g' + repo,
3044
- 'aria-expanded': !shut,
3045
- onClick: () => setGroups((cur) => Object.assign({}, cur, { [col.id + '/' + repo]: !shut })),
3046
- },
3047
- h('span', null,
3048
- repo || t(grouping === 'assignee' ? 'group.nobody' : 'board.noRepo')),
3049
- h('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
3050
- ))
3051
- }
3052
- if (manyGroups && groups[col.id + '/' + (task.repo || '')] === true) return
3053
- rows.push(h(TaskCard, {
3054
- key: task.id, task, t, now: state.now,
3055
- onNudge: (t) => { setNudgeTask(t); setNudgePrompt(''); setNudgeSuccess(false) },
3056
- columns: (state.columns || []).map((c) => c.id),
3057
- menuOpen: cardMenu,
3058
- onMenu: setCardMenu,
3059
- picked: picked[task.id] === true,
3060
- onPick: (x) => setPicked((cur) => {
3061
- const next = Object.assign({}, cur)
3062
- if (next[x.id]) delete next[x.id]
3063
- else next[x.id] = true
3064
- return next
3065
- }),
3066
- pack: packs[task.id],
3067
- sameSession: hoverSession !== '' && task.sessionId === hoverSession,
3068
- onHover: setHoverSession,
3069
- onRevive: async (x) => {
3070
- try {
3071
- const out = await api('/task/' + encodeURIComponent(x.id) + '/start', {
3072
- method: 'POST', body: JSON.stringify({ revive: true }),
3073
- })
3074
- setError(t(out && out.mode === 'created' ? 'revive.fresh' : 'revive.same'))
3075
- await load(board)
3076
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3077
- },
3078
- onStop: async (x) => {
3079
- try {
3080
- const out = await api('/task/' + encodeURIComponent(x.id) + '/stop', { method: 'POST', body: '{}' })
3081
- if (out && out.acted !== 'stopped') setError(t('stop.' + out.acted))
3082
- await load(board)
3083
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3084
- },
3085
- onMove: (x, column) => askMove(x, column),
3086
- onQueue: async (x) => {
3087
- try {
3088
- const out = await api('/sessions')
3089
- const live = (out && out.sessions) || []
3090
- if (live.length === 0) { setError(t('error.no-live-sessions')); return }
3091
- setDialog({ mode: 'queue', task: x, sessions: live, sessionId: live[0].sessionId, error: '' })
3092
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3093
- },
3094
- onUnqueue: async (x) => {
3095
- try {
3096
- await api('/task/' + encodeURIComponent(x.id) + '/unqueue', { method: 'POST', body: '{}' })
3097
- await load(board)
3098
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3099
- },
3100
- onOpenChat: (x) => {
3101
- if (state && state.remoteMode && x.sessionId) {
3102
- const url = 'http://192.168.1.111:3080/#/chat?sessionId=' + encodeURIComponent(x.sessionId)
3103
- if (typeof window !== 'undefined') window.open(url, '_blank')
3104
- return
3105
- }
3106
- if (!openSession(props.ctx, x.sessionId)) {
3107
- setError(t('error.sessionNotOpened', { id: x.sessionId }))
3108
- return
3109
- }
3110
- if (props.onClose) props.onClose()
3111
- },
3112
- onDragStart: (x) => { dragged.current = x },
3113
- onOpen: openPanel,
3114
- }))
3115
- rows.push(slot(i + 1))
3116
- })
3117
-
3118
- const shut = isCollapsed({ id: col.id, count: all.length }, collapsed)
3119
- const toggle = () => setCollapsed((prev) => Object.assign({}, prev, { [col.id]: !shut }))
3120
- const count = limit !== undefined && limit !== null
3121
- ? all.length + ' / ' + t('board.limit', { n: limit })
3122
- : String(all.length)
3123
-
3124
- return h('div', {
3125
- className: 'dkb-col'
3126
- + (overColumn === col.id ? ' dkb-colOver' : '')
3127
- + (shut ? ' dkb-colShut' : ''),
3128
- key: col.id,
3129
- onDragOver: (e) => { e.preventDefault(); setOverColumn(col.id) },
3130
- onDragLeave: () => { setOverColumn(''); setDropAt(null) },
3131
- onDrop: (e) => { e.preventDefault(); drop(col.id, items.length) },
3001
+ const all = tasksOf(state.tasks, col.id).filter((task) => matchesFilters(task, filters))
3002
+ const items = sortTasks(
3003
+ all.filter((task) => matchesQuery(task, debouncedQuery))
3004
+ .filter((task) => !onlyWaiting || task.state === 'waiting')
3005
+ .filter((task) => !onlyOverdue || task.overdue === true)
3006
+ .filter((task) => !onlyMine || task.assignee === me)
3007
+ .filter((task) => {
3008
+ if (quickFilter === 'active') return task.column === 'in-progress'
3009
+ if (quickFilter === 'review') return task.column === 'review'
3010
+ if (quickFilter === 'epics') return task.isEpic === true
3011
+ if (quickFilter === 'stale') return task.stale === true
3012
+ return true
3013
+ }),
3014
+ order,
3015
+ )
3016
+ const groupKeyOf = (task) => (grouping === 'assignee'
3017
+ ? (task.assignee || '')
3018
+ : (task.repo || ''))
3019
+ const ordered = grouping === 'assignee'
3020
+ ? groupByAssignee(items).flatMap((one) => one.tasks)
3021
+ : items
3022
+ const limit = col.limit
3023
+ const overLimit = limit !== undefined && limit !== null && all.length > limit
3024
+ const slot = (index) => h('div', {
3025
+ className: 'dkb-slot' + (dropAt && dropAt.column === col.id && dropAt.index === index ? ' dkb-slotOn' : ''),
3026
+ key: 's' + index,
3027
+ onDragOver: (e) => {
3028
+ e.preventDefault(); e.stopPropagation()
3029
+ setOverColumn(col.id)
3030
+ setDropAt({ column: col.id, index })
3132
3031
  },
3133
- h('button', {
3134
- type: 'button', className: 'dkb-colHead',
3032
+ onDrop: (e) => { e.preventDefault(); e.stopPropagation(); drop(col.id, index) },
3033
+ })
3034
+
3035
+ const rows = []
3036
+ rows.push(slot(0))
3037
+ let group = null
3038
+ const sizes = new Map()
3039
+ for (const x of ordered) sizes.set(groupKeyOf(x), (sizes.get(groupKeyOf(x)) || 0) + 1)
3040
+ const manyGroups = sizes.size > 1
3041
+
3042
+ ordered.forEach((task, i) => {
3043
+ const repo = groupKeyOf(task)
3044
+ if (manyGroups && repo !== group) {
3045
+ group = repo
3046
+ const shut = groups[col.id + '/' + repo] === true
3047
+ rows.push(h('button', {
3048
+ type: 'button', className: 'dkb-groupHead', key: 'g' + repo,
3135
3049
  'aria-expanded': !shut,
3136
- title: t(shut ? 'board.expand' : 'board.collapse'),
3137
- onClick: toggle,
3050
+ onClick: () => setGroups((cur) => Object.assign({}, cur, { [col.id + '/' + repo]: !shut })),
3138
3051
  },
3139
- h('span', { className: 'dkb-dot', 'data-col': col.id }),
3140
- h('span', { className: 'dkb-colName' }, columnTitle(col, t)),
3141
- h('span', {
3142
- className: 'dkb-count' + (overLimit ? ' dkb-countOver' : ''),
3143
- 'data-col': col.id,
3144
- }, count),
3145
- ),
3146
- shut ? null : h('div', { className: 'dkb-list' },
3147
- all.length === 0
3148
- ? h('div', { className: 'dkb-empty' }, t('board.empty'))
3149
- : items.length === 0
3150
- ? h('div', { className: 'dkb-empty' }, t('board.noMatch'))
3151
- : null,
3152
- rows,
3153
- ),
3154
- )
3052
+ h('span', null,
3053
+ repo || t(grouping === 'assignee' ? 'group.nobody' : 'board.noRepo')),
3054
+ h('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
3055
+ ))
3056
+ }
3057
+ if (manyGroups && groups[col.id + '/' + (task.repo || '')] === true) return
3058
+ rows.push(h(TaskCard, {
3059
+ key: task.id, task, t, now: state.now,
3060
+ onNudge: (t) => { setNudgeTask(t); setNudgePrompt(''); setNudgeSuccess(false) },
3061
+ columns: (state.columns || []).map((c) => c.id),
3062
+ menuOpen: cardMenu,
3063
+ onMenu: setCardMenu,
3064
+ picked: picked[task.id] === true,
3065
+ onPick: (x) => setPicked((cur) => {
3066
+ const next = Object.assign({}, cur)
3067
+ if (next[x.id]) delete next[x.id]
3068
+ else next[x.id] = true
3069
+ return next
3070
+ }),
3071
+ pack: packs[task.id],
3072
+ sameSession: hoverSession !== '' && task.sessionId === hoverSession,
3073
+ onHover: setHoverSession,
3074
+ onRevive: async (x) => {
3075
+ try {
3076
+ const out = await api('/task/' + encodeURIComponent(x.id) + '/start', {
3077
+ method: 'POST', body: JSON.stringify({ revive: true }),
3078
+ })
3079
+ setError(t(out && out.mode === 'created' ? 'revive.fresh' : 'revive.same'))
3080
+ await load(board)
3081
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3082
+ },
3083
+ onStop: async (x) => {
3084
+ try {
3085
+ const out = await api('/task/' + encodeURIComponent(x.id) + '/stop', { method: 'POST', body: '{}' })
3086
+ if (out && out.acted !== 'stopped') setError(t('stop.' + out.acted))
3087
+ await load(board)
3088
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3089
+ },
3090
+ onMove: (x, column) => askMove(x, column),
3091
+ onQueue: async (x) => {
3092
+ try {
3093
+ const out = await api('/sessions')
3094
+ const live = (out && out.sessions) || []
3095
+ if (live.length === 0) { setError(t('error.no-live-sessions')); return }
3096
+ setDialog({ mode: 'queue', task: x, sessions: live, sessionId: live[0].sessionId, error: '' })
3097
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3098
+ },
3099
+ onUnqueue: async (x) => {
3100
+ try {
3101
+ await api('/task/' + encodeURIComponent(x.id) + '/unqueue', { method: 'POST', body: '{}' })
3102
+ await load(board)
3103
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3104
+ },
3105
+ onOpenChat: (x) => {
3106
+ if (state && state.remoteMode && x.sessionId) {
3107
+ const url = 'http://192.168.1.111:3080/#/chat?sessionId=' + encodeURIComponent(x.sessionId)
3108
+ if (typeof window !== 'undefined') window.open(url, '_blank')
3109
+ return
3110
+ }
3111
+ if (!openSession(props.ctx, x.sessionId)) {
3112
+ setError(t('error.sessionNotOpened', { id: x.sessionId }))
3113
+ return
3114
+ }
3115
+ if (props.onClose) props.onClose()
3116
+ },
3117
+ onDragStart: (x) => { dragged.current = x },
3118
+ onOpen: openPanel,
3119
+ }))
3120
+ rows.push(slot(i + 1))
3121
+ })
3122
+
3123
+ const shut = isCollapsed({ id: col.id, count: all.length }, collapsed)
3124
+ const toggle = () => setCollapsed((prev) => Object.assign({}, prev, { [col.id]: !shut }))
3125
+ const count = limit !== undefined && limit !== null
3126
+ ? all.length + ' / ' + t('board.limit', { n: limit })
3127
+ : String(all.length)
3128
+
3129
+ return h('div', {
3130
+ className: 'dkb-col'
3131
+ + (overColumn === col.id ? ' dkb-colOver' : '')
3132
+ + (shut ? ' dkb-colShut' : ''),
3133
+ key: col.id,
3134
+ onDragOver: (e) => { e.preventDefault(); setOverColumn(col.id) },
3135
+ onDragLeave: () => { setOverColumn(''); setDropAt(null) },
3136
+ onDrop: (e) => { e.preventDefault(); drop(col.id, items.length) },
3137
+ },
3138
+ h('button', {
3139
+ type: 'button', className: 'dkb-colHead',
3140
+ 'aria-expanded': !shut,
3141
+ title: t(shut ? 'board.expand' : 'board.collapse'),
3142
+ onClick: toggle,
3143
+ },
3144
+ h('span', { className: 'dkb-dot', 'data-col': col.id }),
3145
+ h('span', { className: 'dkb-colName' }, columnTitle(col, t)),
3146
+ h('span', {
3147
+ className: 'dkb-count' + (overLimit ? ' dkb-countOver' : ''),
3148
+ 'data-col': col.id,
3149
+ }, count),
3150
+ ),
3151
+ shut ? null : h('div', { className: 'dkb-list' },
3152
+ all.length === 0
3153
+ ? h('div', { className: 'dkb-empty' }, t('board.empty'))
3154
+ : items.length === 0
3155
+ ? h('div', { className: 'dkb-empty' }, t('board.noMatch'))
3156
+ : null,
3157
+ rows,
3158
+ ),
3159
+ )
3155
3160
  }),
3156
3161
  )
3157
3162
 
3158
3163
  const panel = openTask
3159
3164
  ? h('div', {
3160
- className: 'dkb-modal',
3161
- onClick: () => setOpenTask(null),
3165
+ className: 'dkb-modal',
3166
+ onClick: () => setOpenTask(null),
3162
3167
  }, h('div', {
3163
- className: 'dkb-panel dkb-panelWide',
3164
- role: 'dialog', 'aria-modal': 'true',
3165
- onClick: (e) => e.stopPropagation(),
3168
+ className: 'dkb-panel dkb-panelWide',
3169
+ role: 'dialog', 'aria-modal': 'true',
3170
+ onClick: (e) => e.stopPropagation(),
3166
3171
  },
3167
- h('div', { className: 'dkb-row' },
3168
- h('label', { className: 'dkb-label' }, t('reasoning.label')),
3169
- h('select', {
3170
- className: 'dkb-select',
3171
- value: openTask.reasoningEffort || '',
3172
- onChange: async (e) => {
3173
- const val = e.target.value
3174
- setOpenTask({ ...openTask, reasoningEffort: val })
3175
- try {
3176
- await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3177
- method: 'POST',
3178
- body: JSON.stringify({ reasoningEffort: val }),
3179
- })
3180
- } catch { }
3181
- },
3172
+ h('div', { className: 'dkb-row' },
3173
+ h('label', { className: 'dkb-label' }, t('reasoning.label')),
3174
+ h('select', {
3175
+ className: 'dkb-select',
3176
+ value: openTask.reasoningEffort || '',
3177
+ onChange: async (e) => {
3178
+ const val = e.target.value
3179
+ setOpenTask({ ...openTask, reasoningEffort: val })
3180
+ try {
3181
+ await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3182
+ method: 'POST',
3183
+ body: JSON.stringify({ reasoningEffort: val }),
3184
+ })
3185
+ } catch { }
3182
3186
  },
3183
- h('option', { value: '' }, t('reasoning.default')),
3184
- h('option', { value: 'low' }, t('reasoning.low')),
3185
- h('option', { value: 'medium' }, t('reasoning.medium')),
3186
- h('option', { value: 'high' }, t('reasoning.high')),
3187
- ),
3188
- ),
3189
- h('div', { className: 'dkb-row' },
3190
- h('div', { className: 'dkb-taskHead', style: { flex: 1 } },
3191
- taskRef(openTask, t),
3192
- (openTask.labels || []).map((l) => h('span', {
3193
- className: 'dkb-tag', key: l, style: tagStyle(openTask, l),
3194
- }, l))),
3195
- h('button', {
3196
- type: 'button', className: 'dkb-discard', onClick: () => setOpenTask(null),
3197
- }, t('panel.close')),
3187
+ },
3188
+ h('option', { value: '' }, t('reasoning.default')),
3189
+ h('option', { value: 'low' }, t('reasoning.low')),
3190
+ h('option', { value: 'medium' }, t('reasoning.medium')),
3191
+ h('option', { value: 'high' }, t('reasoning.high')),
3198
3192
  ),
3199
- h('div', { className: 'dkb-title' }, openTask.title),
3200
- h('div', { className: 'dkb-note' },
3201
- t('panel.created', { ago: agoText(state.now, openTask.createdAt, t) })
3202
- + (openTask.assignee ? ' · ' + t('panel.assignee', { who: openTask.assignee }) : '')
3203
- + (openTask.author ? ' · ' + t('panel.author', { who: openTask.author }) : ''),
3204
- ' · ',
3205
- t('panel.updated', { ago: agoText(state.now, openTask.updatedAt, t) }),
3206
- openTask.issueUrl
3207
- ? h('a', {
3208
- className: 'dkb-issueLink', href: openTask.issueUrl,
3209
- target: '_blank', rel: 'noreferrer noopener',
3210
- }, ' · ' + t('panel.issue') + ' ↗')
3211
- : null),
3212
- openTask.isBlocked
3213
- ? h('div', {
3214
- style: {
3215
- margin: '6px 0', padding: '10px 12px',
3216
- background: 'var(--dsw-alias-state-error-secondary, #ffebe9)',
3217
- border: '1px solid var(--dsw-alias-state-error-primary, #cf222e)',
3218
- borderRadius: 8, fontSize: 12, display: 'flex', flexDirection: 'column', gap: 4,
3219
- },
3193
+ ),
3194
+ h('div', { className: 'dkb-row' },
3195
+ h('div', { className: 'dkb-taskHead', style: { flex: 1 } },
3196
+ taskRef(openTask, t),
3197
+ (openTask.labels || []).map((l) => h('span', {
3198
+ className: 'dkb-tag', key: l, style: tagStyle(openTask, l),
3199
+ }, l))),
3200
+ h('button', {
3201
+ type: 'button', className: 'dkb-discard', onClick: () => setOpenTask(null),
3202
+ }, t('panel.close')),
3203
+ ),
3204
+ h('div', { className: 'dkb-title' }, openTask.title),
3205
+ h('div', { className: 'dkb-note' },
3206
+ t('panel.created', { ago: agoText(state.now, openTask.createdAt, t) })
3207
+ + (openTask.assignee ? ' · ' + t('panel.assignee', { who: openTask.assignee }) : '')
3208
+ + (openTask.author ? ' · ' + t('panel.author', { who: openTask.author }) : ''),
3209
+ ' · ',
3210
+ t('panel.updated', { ago: agoText(state.now, openTask.updatedAt, t) }),
3211
+ openTask.issueUrl
3212
+ ? h('a', {
3213
+ className: 'dkb-issueLink', href: openTask.issueUrl,
3214
+ target: '_blank', rel: 'noreferrer noopener',
3215
+ }, ' · ' + t('panel.issue') + ' ↗')
3216
+ : null),
3217
+ openTask.isBlocked
3218
+ ? h('div', {
3219
+ style: {
3220
+ margin: '6px 0', padding: '10px 12px',
3221
+ background: 'var(--dsw-alias-state-error-secondary, #ffebe9)',
3222
+ border: '1px solid var(--dsw-alias-state-error-primary, #cf222e)',
3223
+ borderRadius: 8, fontSize: 12, display: 'flex', flexDirection: 'column', gap: 4,
3220
3224
  },
3221
- h('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-error-primary, #cf222e)' } },
3222
- '🔒 ' + (t('panel.blockedTitle') || 'Task Blocked by Dependencies')
3223
- ),
3224
- h('div', { style: { color: 'var(--dsw-alias-label-secondary)' } },
3225
- t('panel.blockedDesc') || 'All blocking tasks must reach "done" before this card can move to in-progress.'
3226
- )
3227
- )
3228
- : null,
3229
- h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3230
- h('div', { className: 'dkb-section-title' },
3231
- h('span', null, '🔗 ' + (t('panel.dependencies') || 'Dependencies')),
3232
- h('span', { className: 'dkb-section-badge' },
3233
- Array.isArray(openTask.blockedBy) && openTask.blockedBy.length > 0
3234
- ? openTask.blockedBy.length + ' blockers'
3235
- : 'No blockers'
3225
+ },
3226
+ h('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-error-primary, #cf222e)' } },
3227
+ '🔒 ' + (t('panel.blockedTitle') || 'Task Blocked by Dependencies')
3228
+ ),
3229
+ h('div', { style: { color: 'var(--dsw-alias-label-secondary)' } },
3230
+ t('panel.blockedDesc') || 'All blocking tasks must reach "done" before this card can move to in-progress.'
3236
3231
  )
3237
- ),
3238
- h('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px' } },
3232
+ )
3233
+ : null,
3234
+ h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3235
+ h('div', { className: 'dkb-section-title' },
3236
+ h('span', null, '🔗 ' + (t('panel.dependencies') || 'Dependencies')),
3237
+ h('span', { className: 'dkb-section-badge' },
3239
3238
  Array.isArray(openTask.blockedBy) && openTask.blockedBy.length > 0
3240
- ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '6px' } },
3241
- openTask.blockedBy.map((bId) => {
3242
- const bTask = (state.tasks || []).find((x) => x.id === bId)
3243
- const bLabel = bTask ? (bTask.title || bTask.id) : bId
3244
- const isDone = bTask && bTask.column === 'done'
3245
- return h('span', {
3246
- key: bId,
3247
- className: 'dkb-tag',
3248
- style: isDone ? { background: 'var(--dsw-alias-state-success-secondary, #dafbe1)', color: 'var(--dsw-alias-state-success-primary, #1a7f37)' } : undefined,
3249
- },
3250
- (isDone ? '✓ ' : '⏳ ') + bLabel,
3251
- h('button', {
3252
- type: 'button',
3253
- style: { marginLeft: '6px', background: 'none', border: 'none', cursor: 'pointer', color: 'inherit', font: 'inherit', padding: 0 },
3254
- onClick: async () => {
3255
- const next = (openTask.blockedBy || []).filter((x) => x !== bId)
3256
- setOpenTask({ ...openTask, blockedBy: next })
3257
- try {
3258
- await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3259
- method: 'POST',
3260
- body: JSON.stringify({ blockedBy: next }),
3261
- })
3262
- refreshBoard()
3263
- } catch (err) { alert(err.message || err) }
3264
- },
3265
- }, '×')
3266
- )
3267
- })
3268
- )
3269
- : h('span', { className: 'dkb-note' }, t('panel.noBlockers') || 'No dependencies configured'),
3270
- h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '4px' } },
3271
- h('select', {
3272
- className: 'dkb-select',
3273
- style: { fontSize: '12px', padding: '3px 8px', maxWidth: '320px' },
3274
- value: '',
3275
- onChange: async (e) => {
3276
- const chosenId = e.target.value
3277
- if (!chosenId) return
3278
- const cur = Array.isArray(openTask.blockedBy) ? openTask.blockedBy : []
3279
- if (cur.includes(chosenId)) return
3280
- const next = [...cur, chosenId]
3281
- setOpenTask({ ...openTask, blockedBy: next })
3282
- try {
3283
- await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3284
- method: 'POST',
3285
- body: JSON.stringify({ blockedBy: next }),
3286
- })
3287
- refreshBoard()
3288
- } catch (err) {
3289
- setOpenTask({ ...openTask, blockedBy: cur })
3290
- alert(err.message || err)
3291
- }
3292
- },
3293
- },
3294
- h('option', { value: '' }, t('panel.addBlocker') || 'Add blocking task...'),
3295
- (state.tasks || [])
3296
- .filter((x) => x.id !== openTask.id && !(openTask.blockedBy || []).includes(x.id))
3297
- .map((x) => h('option', { key: x.id, value: x.id }, (x.title || x.id) + ' (' + t('column.' + x.column) + ')'))
3298
- )
3299
- )
3239
+ ? openTask.blockedBy.length + ' blockers'
3240
+ : 'No blockers'
3300
3241
  )
3301
3242
  ),
3302
- h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3303
- h('div', { className: 'dkb-section-title' },
3304
- h('span', null, '⏱ ' + (t('tm.title') || 'Time Machine Snapshots')),
3305
- openTask.inceptionSnapshotId
3306
- ? h('span', { className: 'dkb-section-badge dkb-section-badge-ok' }, 'Inception: ' + openTask.inceptionSnapshotId.slice(0, 8))
3307
- : h('span', { className: 'dkb-section-badge' }, 'No Inception Snapshot')
3308
- ),
3309
- h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' } },
3310
- h('button', {
3311
- type: 'button',
3312
- className: 'dkb-save',
3313
- style: { fontSize: '12px', padding: '4px 10px' },
3314
- onClick: async () => {
3243
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '12px' } },
3244
+ Array.isArray(openTask.blockedBy) && openTask.blockedBy.length > 0
3245
+ ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '6px' } },
3246
+ openTask.blockedBy.map((bId) => {
3247
+ const bTask = (state.tasks || []).find((x) => x.id === bId)
3248
+ const bLabel = bTask ? (bTask.title || bTask.id) : bId
3249
+ const isDone = bTask && bTask.column === 'done'
3250
+ return h('span', {
3251
+ key: bId,
3252
+ className: 'dkb-tag',
3253
+ style: isDone ? { background: 'var(--dsw-alias-state-success-secondary, #dafbe1)', color: 'var(--dsw-alias-state-success-primary, #1a7f37)' } : undefined,
3254
+ },
3255
+ (isDone ? '✓ ' : '⏳ ') + bLabel,
3256
+ h('button', {
3257
+ type: 'button',
3258
+ style: { marginLeft: '6px', background: 'none', border: 'none', cursor: 'pointer', color: 'inherit', font: 'inherit', padding: 0 },
3259
+ onClick: async () => {
3260
+ const next = (openTask.blockedBy || []).filter((x) => x !== bId)
3261
+ setOpenTask({ ...openTask, blockedBy: next })
3262
+ try {
3263
+ await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3264
+ method: 'POST',
3265
+ body: JSON.stringify({ blockedBy: next }),
3266
+ })
3267
+ refreshBoard()
3268
+ } catch (err) { alert(err.message || err) }
3269
+ },
3270
+ }, '×')
3271
+ )
3272
+ })
3273
+ )
3274
+ : h('span', { className: 'dkb-note' }, t('panel.noBlockers') || 'No dependencies configured'),
3275
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '4px' } },
3276
+ h('select', {
3277
+ className: 'dkb-select',
3278
+ style: { fontSize: '12px', padding: '3px 8px', maxWidth: '320px' },
3279
+ value: '',
3280
+ onChange: async (e) => {
3281
+ const chosenId = e.target.value
3282
+ if (!chosenId) return
3283
+ const cur = Array.isArray(openTask.blockedBy) ? openTask.blockedBy : []
3284
+ if (cur.includes(chosenId)) return
3285
+ const next = [...cur, chosenId]
3286
+ setOpenTask({ ...openTask, blockedBy: next })
3315
3287
  try {
3316
- await api('/task/' + encodeURIComponent(openTask.id) + '/snapshot', {
3288
+ await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3317
3289
  method: 'POST',
3318
- body: JSON.stringify({ label: 'Manual checkpoint' }),
3290
+ body: JSON.stringify({ blockedBy: next }),
3319
3291
  })
3320
- alert(t('tm.snapshotCreated') || 'Checkpoint snapshot created!')
3292
+ refreshBoard()
3321
3293
  } catch (err) {
3322
- alert((t('tm.snapshotFailed') || 'Failed: ') + (err.message || err))
3294
+ setOpenTask({ ...openTask, blockedBy: cur })
3295
+ alert(err.message || err)
3323
3296
  }
3324
3297
  },
3325
- }, '📸 ' + (t('tm.createCheckpoint') || 'Create Checkpoint')),
3326
- openTask.inceptionSnapshotId
3327
- ? h('button', {
3328
- type: 'button',
3329
- className: 'dkb-discard',
3330
- style: { fontSize: '12px', padding: '4px 10px', color: 'var(--dsw-alias-state-error-primary, #cf222e)' },
3331
- onClick: async () => {
3332
- if (!confirm(t('tm.confirmRollback') || 'Roll back worktree to inception snapshot? Any uncommitted changes will be discarded.')) return
3333
- try {
3334
- await api('/task/' + encodeURIComponent(openTask.id) + '/rollback-snapshot', {
3335
- method: 'POST',
3336
- body: JSON.stringify({ snapshotId: openTask.inceptionSnapshotId }),
3337
- })
3338
- alert(t('tm.rollbackSuccess') || 'Task worktree successfully rolled back!')
3339
- refreshBoard()
3340
- } catch (err) {
3341
- alert((t('tm.rollbackFailed') || 'Rollback failed: ') + (err.message || err))
3342
- }
3343
- },
3344
- }, '↩ ' + (t('tm.rollbackInception') || 'Rollback to Inception'))
3345
- : null
3346
- )
3347
- ),
3348
- duplicates.length > 0
3349
- ? h('div', { className: 'dkb-section-card', style: { margin: '8px 0', borderColor: 'var(--dsw-alias-state-warn-primary, #9a6700)' } },
3350
- h('div', { className: 'dkb-section-title', style: { color: 'var(--dsw-alias-state-warn-primary, #9a6700)' } },
3351
- h('span', null, '⚠️ ' + (t('dup.found') || 'Similar tasks found:')),
3352
- h('button', { type: 'button', className: 'dkb-discard', style: { fontSize: 11, padding: '2px 6px' }, onClick: () => setDuplicates([]) }, '✕')
3353
- ),
3354
- h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
3355
- duplicates.map((d) => h('div', { key: d.id, style: { display: 'flex', justifyContent: 'space-between', fontSize: 12 } },
3356
- h('span', null, (d.issueNumber ? `#${d.issueNumber} ` : '') + d.title),
3357
- h('span', { className: 'dkb-tag' }, `${d.percent}%`)
3358
- ))
3359
- )
3298
+ },
3299
+ h('option', { value: '' }, t('panel.addBlocker') || 'Add blocking task...'),
3300
+ (state.tasks || [])
3301
+ .filter((x) => x.id !== openTask.id && !(openTask.blockedBy || []).includes(x.id))
3302
+ .map((x) => h('option', { key: x.id, value: x.id }, (x.title || x.id) + ' (' + t('column.' + x.column) + ')'))
3360
3303
  )
3361
- : null,
3362
- h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3363
- h('div', { className: 'dkb-section-title' },
3364
- h('span', null, '📋 ' + (t('spec.title') || 'Technical Specification & Plan')),
3365
- openTask.spec ? h('span', { className: 'dkb-section-badge dkb-section-badge-ok' }, 'Spec Ready') : null
3366
- ),
3367
- openTask.spec && !specEdit
3368
- ? h('pre', { className: 'dkb-consolePre', style: { maxHeight: 160 } }, openTask.spec)
3369
- : (specEdit ? h('textarea', { className: 'dkb-area', value: specText, onChange: (e) => setSpecText(e.target.value) }) : null),
3370
- h('div', { className: 'dkb-row', style: { marginTop: 4 } },
3371
- h('button', { type: 'button', className: 'dkb-save', disabled: busySpec, onClick: onGenPlan }, busySpec ? t('spec.generating') : t('spec.generate')),
3372
- h('button', {
3373
- type: 'button', className: 'dkb-discard',
3374
- disabled: checkingDup,
3375
- onClick: () => onCheckDuplicates(openTask.title, openTask.body || openTask.description)
3376
- }, checkingDup ? t('dup.checking') : t('dup.check')),
3377
- openTask.spec ? h('button', {
3378
- type: 'button', className: 'dkb-discard',
3379
- onClick: () => { if (specEdit) onSaveSpec(); else { setSpecText(openTask.spec || ''); setSpecEdit(true) } }
3380
- }, specEdit ? t('spec.save') : t('spec.edit')) : null
3381
- )
3382
- ),
3383
- h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3384
- h('div', { className: 'dkb-section-title' },
3385
- h('span', null, '🔀 ' + (t('pr.title') || 'Pull Request & Release Notes')),
3386
- openTask.prDescription ? h('span', { className: 'dkb-section-badge dkb-section-badge-ok' }, 'PR Ready') : null
3387
- ),
3388
- openTask.prDescription ? h('pre', { className: 'dkb-consolePre', style: { maxHeight: 160 } }, openTask.prDescription) : null,
3389
- h('div', { className: 'dkb-row', style: { marginTop: 4 } },
3390
- h('button', { type: 'button', className: 'dkb-save', disabled: busyPr, onClick: onGenPr }, busyPr ? t('pr.generate') : t('pr.generate')),
3391
- openTask.prDescription ? h('button', {
3392
- type: 'button', className: 'dkb-discard',
3393
- onClick: () => { navigator.clipboard?.writeText(openTask.prDescription); showNotice(t('pr.copied')) }
3394
- }, t('pr.copy')) : null,
3395
- openTask.branch ? h('button', { type: 'button', className: 'dkb-save', disabled: busyPr, onClick: onCreatePrGitea }, t('pr.create')) : null
3396
3304
  )
3305
+ )
3306
+ ),
3307
+ h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3308
+ h('div', { className: 'dkb-section-title' },
3309
+ h('span', null, '⏱ ' + (t('tm.title') || 'Time Machine Snapshots')),
3310
+ openTask.inceptionSnapshotId
3311
+ ? h('span', { className: 'dkb-section-badge dkb-section-badge-ok' }, 'Inception: ' + openTask.inceptionSnapshotId.slice(0, 8))
3312
+ : h('span', { className: 'dkb-section-badge' }, 'No Inception Snapshot')
3397
3313
  ),
3398
- openTask.worktree
3399
- ? h('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
3400
- t('panel.worktree', { path: openTask.worktree })
3401
- + (openTask.branch ? ' (' + openTask.branch + ')' : ''))
3402
- : null,
3403
- openTask.report?.verification?.status === 'failed'
3404
- ? h('div', {
3405
- style: {
3406
- margin: '8px 0', padding: '10px 12px',
3407
- background: 'var(--dsw-alias-state-error-secondary, #ffebe9)',
3408
- border: '1px solid var(--dsw-alias-state-error-primary, #cf222e)',
3409
- borderRadius: 8, fontSize: 12, display: 'flex', flexDirection: 'column', gap: 6,
3410
- },
3411
- },
3412
- h('div', { style: { fontWeight: 'bold', color: 'var(--dsw-alias-state-error-primary, #cf222e)' } },
3413
- '✗ ' + (t('panel.verificationFailed') || 'Automated verification before Review failed') + (openTask.report.verification.command ? ' (' + openTask.report.verification.command + ')' : '')
3414
- ),
3415
- openTask.report.verification.log
3416
- ? h('pre', {
3417
- style: {
3418
- margin: 0, padding: 6, maxHeight: 150, overflowY: 'auto',
3419
- background: 'var(--dsw-alias-bg-layer-2)',
3420
- fontSize: 11, fontFamily: 'monospace', borderRadius: 4, whiteSpace: 'pre-wrap',
3421
- },
3422
- }, openTask.report.verification.log)
3423
- : null
3424
- )
3425
- : null,
3426
- openTask.permission && !openTask.permissionConfirmedAt
3427
- ? h('div', {
3428
- className: 'dkb-warnBox',
3429
- style: { margin: '4px 0', padding: 10, background: 'var(--dsw-alias-state-warn-secondary, #fff3cd)', border: '1px solid var(--dsw-alias-state-warn-secondary, #ffeeba)', borderRadius: 6 },
3430
- },
3431
- h('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
3432
- t('task.permissionGateWarning', { permission: openTask.permission })),
3433
- h('button', {
3434
- type: 'button', className: 'dkb-save',
3435
- onClick: async () => {
3436
- try {
3437
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/confirm-permission', { method: 'POST' })
3438
- if (out && out.task) setOpenTask(out.task)
3439
- await load(board)
3440
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3441
- },
3442
- }, t('task.confirmPermission')),
3443
- )
3444
- : null,
3445
- body === null
3446
- ? h('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
3447
- : body.length === 0
3448
- ? null
3449
- : h('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
3450
- h('div', { className: 'dkb-edit' },
3451
- h('div', { className: 'dkb-field' },
3452
- h('span', { className: 'dkb-label' }, t('panel.priority')),
3453
- h('select', {
3454
- className: 'dkb-input', value: editPriority,
3455
- onChange: (e) => setEditPriority(e.target.value),
3456
- },
3457
- h('option', { value: '' }, t('panel.priorityNone')),
3458
- ['high', 'medium', 'low'].map((p) => h('option', { value: p, key: p }, t('priority.' + p)))),
3459
- ),
3460
- h('div', { className: 'dkb-field' },
3461
- h('span', { className: 'dkb-label' }, t('panel.labels')),
3462
- h('input', {
3463
- className: 'dkb-input', value: editLabels,
3464
- placeholder: t('dialog.labelsHint'),
3465
- onChange: (e) => setEditLabels(e.target.value),
3466
- }),
3467
- ),
3468
- h('div', { className: 'dkb-field' },
3469
- h('span', { className: 'dkb-label' }, t('panel.due')),
3470
- h('input', {
3471
- type: 'date', className: 'dkb-input', value: editDue,
3472
- onChange: (e) => setEditDue(e.target.value),
3473
- }),
3474
- ),
3475
- h('div', { className: 'dkb-field' },
3476
- h('span', { className: 'dkb-label' }, t('task.cron')),
3477
- h('input', {
3478
- className: 'dkb-input', value: editCron,
3479
- placeholder: '* * * * *',
3480
- onChange: (e) => setEditCron(e.target.value),
3481
- }),
3482
- openTask.nextRunAt
3483
- ? h('p', { className: 'dkb-hint' },
3484
- t('task.nextRun', { date: new Date(openTask.nextRunAt).toLocaleString() }))
3485
- : null,
3486
- ),
3314
+ h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' } },
3487
3315
  h('button', {
3488
- type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
3316
+ type: 'button',
3317
+ className: 'dkb-save',
3318
+ style: { fontSize: '12px', padding: '4px 10px' },
3489
3319
  onClick: async () => {
3490
3320
  try {
3491
- const labels = editLabels.split(',').map((s) => s.trim()).filter(Boolean)
3492
- const dueAt = editDue === ''
3493
- ? 0
3494
- : new Date(editDue + 'T00:00:00').getTime()
3495
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3321
+ await api('/task/' + encodeURIComponent(openTask.id) + '/snapshot', {
3496
3322
  method: 'POST',
3497
- body: JSON.stringify({ priority: editPriority, labels, dueAt, cron: editCron }),
3323
+ body: JSON.stringify({ label: 'Manual checkpoint' }),
3498
3324
  })
3499
- setOpenTask(out.task)
3500
- await load(board)
3501
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3325
+ alert(t('tm.snapshotCreated') || 'Checkpoint snapshot created!')
3326
+ } catch (err) {
3327
+ alert((t('tm.snapshotFailed') || 'Failed: ') + (err.message || err))
3328
+ }
3502
3329
  },
3503
- }, t('panel.saveEdit')),
3504
- ),
3505
- openTask.progressDump
3506
- ? h('div', null,
3507
- h('div', { className: 'dkb-group' }, t('task.progressDump')),
3508
- h('pre', {
3509
- style: {
3510
- background: 'var(--dsw-alias-bg-layer-1, #f6f8fa)', padding: 8, borderRadius: 6, fontSize: 12,
3511
- whiteSpace: 'pre-wrap', maxHeight: 150, overflowY: 'auto', margin: 0,
3512
- },
3513
- }, openTask.progressDump),
3514
- )
3515
- : null,
3516
- h('div', { style: { margin: '6px 0', padding: 10, background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 6 } },
3517
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
3518
- h('span', { className: 'dkb-group', style: { margin: 0 } },
3519
- '👑 ' + t('epic.subtasks') + (subtasks.length > 0 ? ` (${subtasks.filter(s => s.column === 'done').length}/${subtasks.length})` : '')
3520
- ),
3521
- h('button', {
3522
- type: 'button', className: 'dkb-discard',
3523
- style: { padding: '2px 8px', fontSize: 11 },
3524
- onClick: () => setTriageOpen(!triageOpen),
3525
- }, triageOpen ? t('dialog.cancel') : '⚡ ' + t('epic.triage')),
3526
- ),
3527
- triageOpen ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, margin: '8px 0', padding: 8, background: 'var(--dsw-alias-bg-layer-3)', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l1)' } },
3528
- h('span', { style: { fontSize: 12, fontWeight: 600 } }, t('epic.triageTitle')),
3529
- h('span', { className: 'dkb-hint', style: { margin: 0 } }, t('epic.triageHelp')),
3530
- h('textarea', {
3531
- className: 'dkb-area',
3532
- rows: 4,
3533
- placeholder: 'Subtask 1\nSubtask 2\nSubtask 3...',
3534
- value: triageText,
3535
- onChange: (e) => setTriageText(e.target.value),
3536
- }),
3537
- openTask.owner && openTask.repo ? h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer' } },
3538
- h('input', {
3539
- type: 'checkbox',
3540
- checked: triageGitea,
3541
- onChange: (e) => setTriageGitea(e.target.checked),
3542
- }),
3543
- h('span', null, t('epic.createGiteaIssues'))
3544
- ) : null,
3545
- h('div', { style: { display: 'flex', gap: 6 } },
3546
- h('button', {
3547
- type: 'button', className: 'dkb-save',
3548
- disabled: triageSubmitting || !triageText.trim(),
3330
+ }, '📸 ' + (t('tm.createCheckpoint') || 'Create Checkpoint')),
3331
+ openTask.inceptionSnapshotId
3332
+ ? h('button', {
3333
+ type: 'button',
3334
+ className: 'dkb-discard',
3335
+ style: { fontSize: '12px', padding: '4px 10px', color: 'var(--dsw-alias-state-error-primary, #cf222e)' },
3549
3336
  onClick: async () => {
3550
- const lines = triageText.split('\n').map(s => s.trim()).filter(Boolean)
3551
- if (lines.length === 0) return
3552
- setTriageSubmitting(true)
3337
+ if (!confirm(t('tm.confirmRollback') || 'Roll back worktree to inception snapshot? Any uncommitted changes will be discarded.')) return
3553
3338
  try {
3554
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/triage', {
3339
+ await api('/task/' + encodeURIComponent(openTask.id) + '/rollback-snapshot', {
3555
3340
  method: 'POST',
3556
- body: JSON.stringify({
3557
- subtasks: lines.map(title => ({ title })),
3558
- createGiteaIssues: triageGitea,
3559
- }),
3341
+ body: JSON.stringify({ snapshotId: openTask.inceptionSnapshotId }),
3560
3342
  })
3561
- if (out && out.subtasks) setSubtasks(out.subtasks)
3562
- if (out && out.epic) setOpenTask(out.epic)
3563
- setTriageText('')
3564
- setTriageOpen(false)
3565
- await load(board)
3566
- } catch (err) { setError(t(err.key || 'error.unknown')) }
3567
- finally { setTriageSubmitting(false) }
3343
+ alert(t('tm.rollbackSuccess') || 'Task worktree successfully rolled back!')
3344
+ refreshBoard()
3345
+ } catch (err) {
3346
+ alert((t('tm.rollbackFailed') || 'Rollback failed: ') + (err.message || err))
3347
+ }
3568
3348
  },
3569
- }, triageSubmitting ? '...' : t('epic.triageSubmit')),
3570
- h('button', {
3571
- type: 'button', className: 'dkb-discard',
3572
- onClick: () => setTriageOpen(false),
3573
- }, t('dialog.cancel')),
3349
+ }, '↩ ' + (t('tm.rollbackInception') || 'Rollback to Inception'))
3350
+ : null
3351
+ )
3352
+ ),
3353
+ duplicates.length > 0
3354
+ ? h('div', { className: 'dkb-section-card', style: { margin: '8px 0', borderColor: 'var(--dsw-alias-state-warn-primary, #9a6700)' } },
3355
+ h('div', { className: 'dkb-section-title', style: { color: 'var(--dsw-alias-state-warn-primary, #9a6700)' } },
3356
+ h('span', null, '⚠️ ' + (t('dup.found') || 'Similar tasks found:')),
3357
+ h('button', { type: 'button', className: 'dkb-discard', style: { fontSize: 11, padding: '2px 6px' }, onClick: () => setDuplicates([]) }, '✕')
3358
+ ),
3359
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
3360
+ duplicates.map((d) => h('div', { key: d.id, style: { display: 'flex', justifyContent: 'space-between', fontSize: 12 } },
3361
+ h('span', null, (d.issueNumber ? `#${d.issueNumber} ` : '') + d.title),
3362
+ h('span', { className: 'dkb-tag' }, `${d.percent}%`)
3363
+ ))
3574
3364
  )
3575
- ) : null,
3576
- subtasks.length > 0 ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
3577
- subtasks.map((st) => h('li', {
3578
- key: st.id,
3365
+ )
3366
+ : null,
3367
+ h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3368
+ h('div', { className: 'dkb-section-title' },
3369
+ h('span', null, '📋 ' + (t('spec.title') || 'Technical Specification & Plan')),
3370
+ openTask.spec ? h('span', { className: 'dkb-section-badge dkb-section-badge-ok' }, 'Spec Ready') : null
3371
+ ),
3372
+ openTask.spec && !specEdit
3373
+ ? h('pre', { className: 'dkb-consolePre', style: { maxHeight: 160 } }, openTask.spec)
3374
+ : (specEdit ? h('textarea', { className: 'dkb-area', value: specText, onChange: (e) => setSpecText(e.target.value) }) : null),
3375
+ h('div', { className: 'dkb-row', style: { marginTop: 4 } },
3376
+ h('button', { type: 'button', className: 'dkb-save', disabled: busySpec, onClick: onGenPlan }, busySpec ? t('spec.generating') : t('spec.generate')),
3377
+ h('button', {
3378
+ type: 'button', className: 'dkb-discard',
3379
+ disabled: checkingDup,
3380
+ onClick: () => onCheckDuplicates(openTask.title, openTask.body || openTask.description)
3381
+ }, checkingDup ? t('dup.checking') : t('dup.check')),
3382
+ openTask.spec ? h('button', {
3383
+ type: 'button', className: 'dkb-discard',
3384
+ onClick: () => { if (specEdit) onSaveSpec(); else { setSpecText(openTask.spec || ''); setSpecEdit(true) } }
3385
+ }, specEdit ? t('spec.save') : t('spec.edit')) : null
3386
+ )
3387
+ ),
3388
+ h('div', { className: 'dkb-section-card', style: { margin: '8px 0' } },
3389
+ h('div', { className: 'dkb-section-title' },
3390
+ h('span', null, '🔀 ' + (t('pr.title') || 'Pull Request & Release Notes')),
3391
+ openTask.prDescription ? h('span', { className: 'dkb-section-badge dkb-section-badge-ok' }, 'PR Ready') : null
3392
+ ),
3393
+ openTask.prDescription ? h('pre', { className: 'dkb-consolePre', style: { maxHeight: 160 } }, openTask.prDescription) : null,
3394
+ h('div', { className: 'dkb-row', style: { marginTop: 4 } },
3395
+ h('button', { type: 'button', className: 'dkb-save', disabled: busyPr, onClick: onGenPr }, busyPr ? t('pr.generate') : t('pr.generate')),
3396
+ openTask.prDescription ? h('button', {
3397
+ type: 'button', className: 'dkb-discard',
3398
+ onClick: () => { navigator.clipboard?.writeText(openTask.prDescription); showNotice(t('pr.copied')) }
3399
+ }, t('pr.copy')) : null,
3400
+ openTask.branch ? h('button', { type: 'button', className: 'dkb-save', disabled: busyPr, onClick: onCreatePrGitea }, t('pr.create')) : null
3401
+ )
3402
+ ),
3403
+ openTask.worktree
3404
+ ? h('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
3405
+ t('panel.worktree', { path: openTask.worktree })
3406
+ + (openTask.branch ? ' (' + openTask.branch + ')' : ''))
3407
+ : null,
3408
+ openTask.report?.verification?.status === 'failed'
3409
+ ? h('div', {
3410
+ style: {
3411
+ margin: '8px 0', padding: '10px 12px',
3412
+ background: 'var(--dsw-alias-state-error-secondary, #ffebe9)',
3413
+ border: '1px solid var(--dsw-alias-state-error-primary, #cf222e)',
3414
+ borderRadius: 8, fontSize: 12, display: 'flex', flexDirection: 'column', gap: 6,
3415
+ },
3416
+ },
3417
+ h('div', { style: { fontWeight: 'bold', color: 'var(--dsw-alias-state-error-primary, #cf222e)' } },
3418
+ '✗ ' + (t('panel.verificationFailed') || 'Automated verification before Review failed') + (openTask.report.verification.command ? ' (' + openTask.report.verification.command + ')' : '')
3419
+ ),
3420
+ openTask.report.verification.log
3421
+ ? h('pre', {
3422
+ style: {
3423
+ margin: 0, padding: 6, maxHeight: 150, overflowY: 'auto',
3424
+ background: 'var(--dsw-alias-bg-layer-2)',
3425
+ fontSize: 11, fontFamily: 'monospace', borderRadius: 4, whiteSpace: 'pre-wrap',
3426
+ },
3427
+ }, openTask.report.verification.log)
3428
+ : null
3429
+ )
3430
+ : null,
3431
+ openTask.permission && !openTask.permissionConfirmedAt
3432
+ ? h('div', {
3433
+ className: 'dkb-warnBox',
3434
+ style: { margin: '4px 0', padding: 10, background: 'var(--dsw-alias-state-warn-secondary, #fff3cd)', border: '1px solid var(--dsw-alias-state-warn-secondary, #ffeeba)', borderRadius: 6 },
3435
+ },
3436
+ h('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
3437
+ t('task.permissionGateWarning', { permission: openTask.permission })),
3438
+ h('button', {
3439
+ type: 'button', className: 'dkb-save',
3440
+ onClick: async () => {
3441
+ try {
3442
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/confirm-permission', { method: 'POST' })
3443
+ if (out && out.task) setOpenTask(out.task)
3444
+ await load(board)
3445
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3446
+ },
3447
+ }, t('task.confirmPermission')),
3448
+ )
3449
+ : null,
3450
+ body === null
3451
+ ? h('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
3452
+ : body.length === 0
3453
+ ? null
3454
+ : h('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
3455
+ h('div', { className: 'dkb-edit' },
3456
+ h('div', { className: 'dkb-field' },
3457
+ h('span', { className: 'dkb-label' }, t('panel.priority')),
3458
+ h('select', {
3459
+ className: 'dkb-input', value: editPriority,
3460
+ onChange: (e) => setEditPriority(e.target.value),
3461
+ },
3462
+ h('option', { value: '' }, t('panel.priorityNone')),
3463
+ ['high', 'medium', 'low'].map((p) => h('option', { value: p, key: p }, t('priority.' + p)))),
3464
+ ),
3465
+ h('div', { className: 'dkb-field' },
3466
+ h('span', { className: 'dkb-label' }, t('panel.labels')),
3467
+ h('input', {
3468
+ className: 'dkb-input', value: editLabels,
3469
+ placeholder: t('dialog.labelsHint'),
3470
+ onChange: (e) => setEditLabels(e.target.value),
3471
+ }),
3472
+ ),
3473
+ h('div', { className: 'dkb-field' },
3474
+ h('span', { className: 'dkb-label' }, t('panel.due')),
3475
+ h('input', {
3476
+ type: 'date', className: 'dkb-input', value: editDue,
3477
+ onChange: (e) => setEditDue(e.target.value),
3478
+ }),
3479
+ ),
3480
+ h('div', { className: 'dkb-field' },
3481
+ h('span', { className: 'dkb-label' }, t('task.cron')),
3482
+ h('input', {
3483
+ className: 'dkb-input', value: editCron,
3484
+ placeholder: '* * * * *',
3485
+ onChange: (e) => setEditCron(e.target.value),
3486
+ }),
3487
+ openTask.nextRunAt
3488
+ ? h('p', { className: 'dkb-hint' },
3489
+ t('task.nextRun', { date: new Date(openTask.nextRunAt).toLocaleString() }))
3490
+ : null,
3491
+ ),
3492
+ h('button', {
3493
+ type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
3494
+ onClick: async () => {
3495
+ try {
3496
+ const labels = editLabels.split(',').map((s) => s.trim()).filter(Boolean)
3497
+ const dueAt = editDue === ''
3498
+ ? 0
3499
+ : new Date(editDue + 'T00:00:00').getTime()
3500
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
3501
+ method: 'POST',
3502
+ body: JSON.stringify({ priority: editPriority, labels, dueAt, cron: editCron }),
3503
+ })
3504
+ setOpenTask(out.task)
3505
+ await load(board)
3506
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3507
+ },
3508
+ }, t('panel.saveEdit')),
3509
+ ),
3510
+ openTask.progressDump
3511
+ ? h('div', null,
3512
+ h('div', { className: 'dkb-group' }, t('task.progressDump')),
3513
+ h('pre', {
3579
3514
  style: {
3580
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
3581
- padding: '4px 8px', background: 'var(--dsw-alias-bg-layer-2)',
3582
- borderRadius: 4, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
3515
+ background: 'var(--dsw-alias-bg-layer-1, #f6f8fa)', padding: 8, borderRadius: 6, fontSize: 12,
3516
+ whiteSpace: 'pre-wrap', maxHeight: 150, overflowY: 'auto', margin: 0,
3583
3517
  },
3584
- },
3585
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flex: 1 } },
3586
- h('span', { style: { color: st.column === 'done' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-tertiary)' } },
3587
- st.column === 'done' ? '✓' : '•'
3588
- ),
3589
- h('span', { style: { textDecoration: st.column === 'done' ? 'line-through' : 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, st.title),
3590
- st.issueNumber ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, '#' + st.issueNumber) : null,
3591
- ),
3592
- h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('column.' + st.column)),
3593
- ))
3594
- ) : h('p', { className: 'dkb-hint', style: { margin: '4px 0' } }, t('epic.noSubtasks')),
3595
- h('div', { style: { display: 'flex', gap: 6, marginTop: 6 } },
3518
+ }, openTask.progressDump),
3519
+ )
3520
+ : null,
3521
+ h('div', { style: { margin: '6px 0', padding: 10, background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 6 } },
3522
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
3523
+ h('span', { className: 'dkb-group', style: { margin: 0 } },
3524
+ '👑 ' + t('epic.subtasks') + (subtasks.length > 0 ? ` (${subtasks.filter(s => s.column === 'done').length}/${subtasks.length})` : '')
3525
+ ),
3526
+ h('button', {
3527
+ type: 'button', className: 'dkb-discard',
3528
+ style: { padding: '2px 8px', fontSize: 11 },
3529
+ onClick: () => setTriageOpen(!triageOpen),
3530
+ }, triageOpen ? t('dialog.cancel') : '⚡ ' + t('epic.triage')),
3531
+ ),
3532
+ triageOpen ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, margin: '8px 0', padding: 8, background: 'var(--dsw-alias-bg-layer-3)', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l1)' } },
3533
+ h('span', { style: { fontSize: 12, fontWeight: 600 } }, t('epic.triageTitle')),
3534
+ h('span', { className: 'dkb-hint', style: { margin: 0 } }, t('epic.triageHelp')),
3535
+ h('textarea', {
3536
+ className: 'dkb-area',
3537
+ rows: 4,
3538
+ placeholder: 'Subtask 1\nSubtask 2\nSubtask 3...',
3539
+ value: triageText,
3540
+ onChange: (e) => setTriageText(e.target.value),
3541
+ }),
3542
+ openTask.owner && openTask.repo ? h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer' } },
3596
3543
  h('input', {
3597
- className: 'dkb-input',
3598
- style: { flex: 1, fontSize: 12 },
3599
- placeholder: t('epic.addSubtask'),
3600
- value: newSubtaskTitle,
3601
- onChange: (e) => setNewSubtaskTitle(e.target.value),
3602
- onKeyDown: async (e) => {
3603
- if (e.key === 'Enter' && newSubtaskTitle.trim()) {
3604
- e.preventDefault()
3605
- const title = newSubtaskTitle.trim()
3606
- setNewSubtaskTitle('')
3607
- try {
3608
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
3609
- method: 'POST',
3610
- body: JSON.stringify({ title }),
3611
- })
3612
- if (out && out.subtask) {
3613
- setSubtasks(prev => [...prev, out.subtask])
3614
- await load(board)
3615
- }
3616
- } catch (err) { setError(t(err.key || 'error.unknown')) }
3617
- }
3618
- },
3544
+ type: 'checkbox',
3545
+ checked: triageGitea,
3546
+ onChange: (e) => setTriageGitea(e.target.checked),
3619
3547
  }),
3548
+ h('span', null, t('epic.createGiteaIssues'))
3549
+ ) : null,
3550
+ h('div', { style: { display: 'flex', gap: 6 } },
3620
3551
  h('button', {
3621
3552
  type: 'button', className: 'dkb-save',
3622
- style: { padding: '3px 10px', fontSize: 12 },
3553
+ disabled: triageSubmitting || !triageText.trim(),
3623
3554
  onClick: async () => {
3555
+ const lines = triageText.split('\n').map(s => s.trim()).filter(Boolean)
3556
+ if (lines.length === 0) return
3557
+ setTriageSubmitting(true)
3558
+ try {
3559
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/triage', {
3560
+ method: 'POST',
3561
+ body: JSON.stringify({
3562
+ subtasks: lines.map(title => ({ title })),
3563
+ createGiteaIssues: triageGitea,
3564
+ }),
3565
+ })
3566
+ if (out && out.subtasks) setSubtasks(out.subtasks)
3567
+ if (out && out.epic) setOpenTask(out.epic)
3568
+ setTriageText('')
3569
+ setTriageOpen(false)
3570
+ await load(board)
3571
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
3572
+ finally { setTriageSubmitting(false) }
3573
+ },
3574
+ }, triageSubmitting ? '...' : t('epic.triageSubmit')),
3575
+ h('button', {
3576
+ type: 'button', className: 'dkb-discard',
3577
+ onClick: () => setTriageOpen(false),
3578
+ }, t('dialog.cancel')),
3579
+ )
3580
+ ) : null,
3581
+ subtasks.length > 0 ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
3582
+ subtasks.map((st) => h('li', {
3583
+ key: st.id,
3584
+ style: {
3585
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
3586
+ padding: '4px 8px', background: 'var(--dsw-alias-bg-layer-2)',
3587
+ borderRadius: 4, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
3588
+ },
3589
+ },
3590
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flex: 1 } },
3591
+ h('span', { style: { color: st.column === 'done' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-tertiary)' } },
3592
+ st.column === 'done' ? '✓' : '•'
3593
+ ),
3594
+ h('span', { style: { textDecoration: st.column === 'done' ? 'line-through' : 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, st.title),
3595
+ st.issueNumber ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, '#' + st.issueNumber) : null,
3596
+ ),
3597
+ h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('column.' + st.column)),
3598
+ ))
3599
+ ) : h('p', { className: 'dkb-hint', style: { margin: '4px 0' } }, t('epic.noSubtasks')),
3600
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 6 } },
3601
+ h('input', {
3602
+ className: 'dkb-input',
3603
+ style: { flex: 1, fontSize: 12 },
3604
+ placeholder: t('epic.addSubtask'),
3605
+ value: newSubtaskTitle,
3606
+ onChange: (e) => setNewSubtaskTitle(e.target.value),
3607
+ onKeyDown: async (e) => {
3608
+ if (e.key === 'Enter' && newSubtaskTitle.trim()) {
3609
+ e.preventDefault()
3624
3610
  const title = newSubtaskTitle.trim()
3625
- if (!title) return
3626
3611
  setNewSubtaskTitle('')
3627
3612
  try {
3628
3613
  const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
@@ -3634,468 +3619,471 @@ window.__ModuleLoader__.load({
3634
3619
  await load(board)
3635
3620
  }
3636
3621
  } catch (err) { setError(t(err.key || 'error.unknown')) }
3637
- },
3638
- }, '+'),
3639
- )
3622
+ }
3623
+ },
3624
+ }),
3625
+ h('button', {
3626
+ type: 'button', className: 'dkb-save',
3627
+ style: { padding: '3px 10px', fontSize: 12 },
3628
+ onClick: async () => {
3629
+ const title = newSubtaskTitle.trim()
3630
+ if (!title) return
3631
+ setNewSubtaskTitle('')
3632
+ try {
3633
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
3634
+ method: 'POST',
3635
+ body: JSON.stringify({ title }),
3636
+ })
3637
+ if (out && out.subtask) {
3638
+ setSubtasks(prev => [...prev, out.subtask])
3639
+ await load(board)
3640
+ }
3641
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
3642
+ },
3643
+ }, '+'),
3644
+ )
3645
+ ),
3646
+ openTask.sessionId ? h('div', { style: { margin: '6px 0' } },
3647
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3648
+ h('span', null, '🖥️ ' + t('console.tab')),
3649
+ h('span', { className: 'dkb-tag', style: { fontSize: 10, fontFamily: 'monospace' } }, openTask.sessionId.slice(0, 16) + '...'),
3640
3650
  ),
3641
- openTask.sessionId ? h('div', { style: { margin: '6px 0' } },
3642
- h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3643
- h('span', null, '🖥️ ' + t('console.tab')),
3644
- h('span', { className: 'dkb-tag', style: { fontSize: 10, fontFamily: 'monospace' } }, openTask.sessionId.slice(0, 16) + '...'),
3645
- ),
3646
- h('pre', { className: 'dkb-consolePre' },
3647
- openTask.progressDump || (openTask.runs && openTask.runs[0] ? `[${new Date(openTask.runs[0].startedAt).toLocaleTimeString()}] ${openTask.runs[0].model || 'Agent'} active\nResult: ${openTask.runs[0].result || 'running'}` : t('console.empty'))
3648
- )
3649
- ) : null,
3650
- openTask.runs && openTask.runs.length
3651
- ? h('div', null,
3652
- h('div', { className: 'dkb-group' }, t('task.runs')),
3653
- h('ul', { className: 'dkb-log' }, openTask.runs.map((r) => h('li', {
3654
- className: 'dkb-logRow', key: r.id,
3655
- style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
3656
- },
3657
- h('span', null,
3658
- (r.result === 'succeeded' ? '✓ ' : r.result === 'failed' ? '✕ ' : '▸ ')
3659
- + new Date(r.startedAt).toLocaleTimeString()
3660
- + (r.model ? ' · ' + r.model : '')
3661
- + (r.error ? ' · ' + r.error : '')
3662
- ),
3663
- r.sessionId
3664
- ? h('button', {
3665
- type: 'button',
3666
- className: 'dkb-discard',
3667
- style: { padding: '2px 6px', fontSize: 11 },
3668
- onClick: () => {
3669
- if (!openSession(props.ctx, r.sessionId)) {
3670
- setError(t('error.sessionNotOpened', { id: r.sessionId }))
3671
- return
3672
- }
3673
- setOpenTask(null)
3674
- if (props.onClose) props.onClose()
3675
- },
3676
- }, t('panel.openChat'))
3677
- : null,
3678
- ))))
3679
- : null,
3680
- h('button', {
3651
+ h('pre', { className: 'dkb-consolePre' },
3652
+ openTask.progressDump || (openTask.runs && openTask.runs[0] ? `[${new Date(openTask.runs[0].startedAt).toLocaleTimeString()}] ${openTask.runs[0].model || 'Agent'} active\nResult: ${openTask.runs[0].result || 'running'}` : t('console.empty'))
3653
+ )
3654
+ ) : null,
3655
+ openTask.runs && openTask.runs.length
3656
+ ? h('div', null,
3657
+ h('div', { className: 'dkb-group' }, t('task.runs')),
3658
+ h('ul', { className: 'dkb-log' }, openTask.runs.map((r) => h('li', {
3659
+ className: 'dkb-logRow', key: r.id,
3660
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
3661
+ },
3662
+ h('span', null,
3663
+ (r.result === 'succeeded' ? '✓ ' : r.result === 'failed' ? '✕ ' : '▸ ')
3664
+ + new Date(r.startedAt).toLocaleTimeString()
3665
+ + (r.model ? ' · ' + r.model : '')
3666
+ + (r.error ? ' · ' + r.error : '')
3667
+ ),
3668
+ r.sessionId
3669
+ ? h('button', {
3670
+ type: 'button',
3671
+ className: 'dkb-discard',
3672
+ style: { padding: '2px 6px', fontSize: 11 },
3673
+ onClick: () => {
3674
+ if (!openSession(props.ctx, r.sessionId)) {
3675
+ setError(t('error.sessionNotOpened', { id: r.sessionId }))
3676
+ return
3677
+ }
3678
+ setOpenTask(null)
3679
+ if (props.onClose) props.onClose()
3680
+ },
3681
+ }, t('panel.openChat'))
3682
+ : null,
3683
+ ))))
3684
+ : null,
3685
+ h('button', {
3686
+ type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
3687
+ onClick: async () => {
3688
+ const mine = openTask.assignee !== '' && openTask.assignee !== undefined
3689
+ try {
3690
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/assign', {
3691
+ method: 'POST',
3692
+ body: JSON.stringify({ login: mine ? '' : 'me' }),
3693
+ })
3694
+ if (out && out.task) setOpenTask(out.task)
3695
+ await load(board)
3696
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3697
+ },
3698
+ }, openTask.assignee ? t('panel.assignDrop') : t('panel.assignMe')),
3699
+ openTask.sessionId
3700
+ ? h('button', {
3681
3701
  type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
3682
- onClick: async () => {
3683
- const mine = openTask.assignee !== '' && openTask.assignee !== undefined
3684
- try {
3685
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/assign', {
3686
- method: 'POST',
3687
- body: JSON.stringify({ login: mine ? '' : 'me' }),
3688
- })
3689
- if (out && out.task) setOpenTask(out.task)
3690
- await load(board)
3691
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3702
+ onClick: () => {
3703
+ if (!openSession(props.ctx, openTask.sessionId)) {
3704
+ setError(t('error.sessionNotOpened', { id: openTask.sessionId }))
3705
+ return
3706
+ }
3707
+ setOpenTask(null)
3708
+ if (props.onClose) props.onClose()
3692
3709
  },
3693
- }, openTask.assignee ? t('panel.assignDrop') : t('panel.assignMe')),
3694
- openTask.sessionId
3695
- ? h('button', {
3696
- type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
3697
- onClick: () => {
3698
- if (!openSession(props.ctx, openTask.sessionId)) {
3699
- setError(t('error.sessionNotOpened', { id: openTask.sessionId }))
3700
- return
3701
- }
3702
- setOpenTask(null)
3703
- if (props.onClose) props.onClose()
3704
- },
3705
- }, t('panel.openChat'))
3706
- : null,
3707
- openTask.sessionId && (openTask.column === 'in-progress' || openTask.col === 'in-progress')
3708
- ? h('button', {
3709
- type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
3710
- onClick: () => { setNudgeTask(openTask); setNudgePrompt(''); setNudgeSuccess(false) },
3711
- }, '💬 ' + t('card.nudge'))
3712
- : null,
3713
- openTask.costMetrics && openTask.costMetrics.totalTokens > 0
3714
- ? h('div', {
3715
- className: 'dkb-section-card',
3716
- style: {
3717
- margin: '8px 0', padding: 12, border: '1px solid var(--dsw-alias-border-l2)',
3718
- borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 6,
3719
- },
3710
+ }, t('panel.openChat'))
3711
+ : null,
3712
+ openTask.sessionId && (openTask.column === 'in-progress' || openTask.col === 'in-progress')
3713
+ ? h('button', {
3714
+ type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
3715
+ onClick: () => { setNudgeTask(openTask); setNudgePrompt(''); setNudgeSuccess(false) },
3716
+ }, '💬 ' + t('card.nudge'))
3717
+ : null,
3718
+ openTask.costMetrics && openTask.costMetrics.totalTokens > 0
3719
+ ? h('div', {
3720
+ className: 'dkb-section-card',
3721
+ style: {
3722
+ margin: '8px 0', padding: 12, border: '1px solid var(--dsw-alias-border-l2)',
3723
+ borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 6,
3720
3724
  },
3721
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3722
- h('div', { className: 'dkb-group', style: { margin: 0, fontWeight: 600 } },
3723
- '🪙 ' + t('telemetry.title')
3724
- ),
3725
- h('span', {
3726
- className: 'dkb-tag',
3727
- style: { background: 'var(--dsw-alias-state-business-tertiary, #ddf4ff)', color: 'var(--dsw-alias-state-business-primary, #0969da)', borderColor: 'var(--dsw-alias-state-business-tertiary, #54aeff)' }
3728
- }, openTask.costMetrics.costFormatted || '$0.00'),
3725
+ },
3726
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3727
+ h('div', { className: 'dkb-group', style: { margin: 0, fontWeight: 600 } },
3728
+ '🪙 ' + t('telemetry.title')
3729
3729
  ),
3730
- h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, fontSize: '11px', textAlign: 'center' } },
3731
- h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3732
- h('div', { style: { opacity: 0.7 } }, t('telemetry.tokens')),
3733
- h('div', { style: { fontWeight: 600 } }, openTask.costMetrics.totalTokens.toLocaleString()),
3734
- ),
3735
- h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3736
- h('div', { style: { opacity: 0.7 } }, t('telemetry.input')),
3737
- h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.uncachedInputTokens || 0).toLocaleString()),
3738
- ),
3739
- h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3740
- h('div', { style: { opacity: 0.7 } }, t('telemetry.output')),
3741
- h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.outputTokens || 0).toLocaleString()),
3742
- ),
3743
- h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3744
- h('div', { style: { opacity: 0.7 } }, t('telemetry.cache')),
3745
- h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.cacheReadTokens || 0).toLocaleString()),
3746
- ),
3730
+ h('span', {
3731
+ className: 'dkb-tag',
3732
+ style: { background: 'var(--dsw-alias-state-business-tertiary, #ddf4ff)', color: 'var(--dsw-alias-state-business-primary, #0969da)', borderColor: 'var(--dsw-alias-state-business-tertiary, #54aeff)' }
3733
+ }, openTask.costMetrics.costFormatted || '$0.00'),
3734
+ ),
3735
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, fontSize: '11px', textAlign: 'center' } },
3736
+ h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3737
+ h('div', { style: { opacity: 0.7 } }, t('telemetry.tokens')),
3738
+ h('div', { style: { fontWeight: 600 } }, openTask.costMetrics.totalTokens.toLocaleString()),
3747
3739
  ),
3748
- openTask.costLimitExceeded
3749
- ? h('div', {
3750
- style: {
3751
- fontSize: '11px',
3752
- color: 'var(--dsw-alias-state-warn-primary, #9a6700)',
3753
- background: 'var(--dsw-alias-state-warn-secondary, #fff8c5)',
3754
- padding: '4px 8px', borderRadius: '4px', marginTop: '4px'
3755
- }
3756
- }, '⚠️ ' + t('card.costLimitExceeded'))
3757
- : null
3758
- )
3759
- : null,
3760
- log.length
3761
- ? h('div', null,
3762
- h('div', { className: 'dkb-group' }, t('panel.log')),
3763
- h('ul', { className: 'dkb-log' }, log.map((row) => h('li', {
3764
- className: 'dkb-logRow', key: row.id,
3765
- }, `${t('column.' + row.toCol)} ← ${row.source}${row.detail ? ' · ' + row.detail : ''}`))))
3766
- : null,
3767
- openTask.plan && openTask.plan.items && openTask.plan.items.length
3768
- ? h('div', null,
3769
- h('div', { className: 'dkb-group' }, t('plan.title')),
3770
- h('p', { className: 'dkb-hint' }, planLine(openTask, t)),
3771
- h('ul', { className: 'dkb-planList' },
3772
- openTask.plan.items.map((item, i) => h('li', {
3773
- key: i,
3774
- className: 'dkb-planItem'
3775
- + (item.done ? ' dkb-planDone' : '')
3776
- + (item.active ? ' dkb-planActive' : ''),
3777
- },
3778
- h('span', { className: 'dkb-planMark' },
3779
- item.done ? '✓' : item.active ? '▸' : '·'),
3780
- h('span', null, item.text),
3781
- ))),
3740
+ h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3741
+ h('div', { style: { opacity: 0.7 } }, t('telemetry.input')),
3742
+ h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.uncachedInputTokens || 0).toLocaleString()),
3743
+ ),
3744
+ h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3745
+ h('div', { style: { opacity: 0.7 } }, t('telemetry.output')),
3746
+ h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.outputTokens || 0).toLocaleString()),
3747
+ ),
3748
+ h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
3749
+ h('div', { style: { opacity: 0.7 } }, t('telemetry.cache')),
3750
+ h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.cacheReadTokens || 0).toLocaleString()),
3751
+ ),
3752
+ ),
3753
+ openTask.costLimitExceeded
3754
+ ? h('div', {
3755
+ style: {
3756
+ fontSize: '11px',
3757
+ color: 'var(--dsw-alias-state-warn-primary, #9a6700)',
3758
+ background: 'var(--dsw-alias-state-warn-secondary, #fff8c5)',
3759
+ padding: '4px 8px', borderRadius: '4px', marginTop: '4px'
3760
+ }
3761
+ }, '⚠️ ' + t('card.costLimitExceeded'))
3762
+ : null
3782
3763
  )
3783
- : null,
3784
- openTask.rejectReason
3785
- ? h('div', {
3786
- className: 'dkb-warnBox',
3787
- style: {
3788
- margin: '4px 0', padding: 10,
3789
- background: 'var(--dsw-alias-state-error-secondary, #ffebe9)',
3790
- border: '1px solid var(--dsw-alias-state-error-secondary, #ffc1c0)',
3791
- borderRadius: 6,
3792
- },
3764
+ : null,
3765
+ log.length
3766
+ ? h('div', null,
3767
+ h('div', { className: 'dkb-group' }, t('panel.log')),
3768
+ h('ul', { className: 'dkb-log' }, log.map((row) => h('li', {
3769
+ className: 'dkb-logRow', key: row.id,
3770
+ }, `${t('column.' + row.toCol)} ← ${row.source}${row.detail ? ' · ' + row.detail : ''}`))))
3771
+ : null,
3772
+ openTask.plan && openTask.plan.items && openTask.plan.items.length
3773
+ ? h('div', null,
3774
+ h('div', { className: 'dkb-group' }, t('plan.title')),
3775
+ h('p', { className: 'dkb-hint' }, planLine(openTask, t)),
3776
+ h('ul', { className: 'dkb-planList' },
3777
+ openTask.plan.items.map((item, i) => h('li', {
3778
+ key: i,
3779
+ className: 'dkb-planItem'
3780
+ + (item.done ? ' dkb-planDone' : '')
3781
+ + (item.active ? ' dkb-planActive' : ''),
3793
3782
  },
3794
- h('div', {
3795
- style: { marginBottom: 4, fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-error-primary, #cf222e)' },
3796
- }, '⚠️ ' + t('acceptance.rejectedAlert', { reason: openTask.rejectReason })),
3797
- )
3798
- : null,
3799
- openTask.worktree
3800
- ? h('div', {
3801
- style: {
3802
- margin: '8px 0', padding: 12,
3803
- background: 'var(--dsw-alias-bg-layer-3)',
3804
- border: '1px solid var(--dsw-alias-border-l2)',
3805
- borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
3806
- },
3783
+ h('span', { className: 'dkb-planMark' },
3784
+ item.done ? '✓' : item.active ? '▸' : '·'),
3785
+ h('span', null, item.text),
3786
+ ))),
3787
+ )
3788
+ : null,
3789
+ openTask.rejectReason
3790
+ ? h('div', {
3791
+ className: 'dkb-warnBox',
3792
+ style: {
3793
+ margin: '4px 0', padding: 10,
3794
+ background: 'var(--dsw-alias-state-error-secondary, #ffebe9)',
3795
+ border: '1px solid var(--dsw-alias-state-error-secondary, #ffc1c0)',
3796
+ borderRadius: 6,
3807
3797
  },
3808
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3809
- h('div', { className: 'dkb-group', style: { margin: 0 } },
3810
- t('diff.title'),
3811
- commitsData.length > 0 ? ` · ${t('diff.commits', { n: commitsData.length })}` : '',
3812
- diffData?.filesChanged ? ` · ${t('diff.filesChanged', { n: diffData.filesChanged })}` : '',
3813
- ),
3814
- h('button', {
3815
- type: 'button', className: 'dkb-discard',
3816
- style: { padding: '2px 8px', fontSize: 11 },
3817
- onClick: () => {
3818
- if (!diffData && !diffExpanded) loadDiffAndCommits()
3819
- setDiffExpanded(!diffExpanded)
3820
- },
3821
- }, diffExpanded ? t('diff.hide') : t('diff.show')),
3798
+ },
3799
+ h('div', {
3800
+ style: { marginBottom: 4, fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-error-primary, #cf222e)' },
3801
+ }, '⚠️ ' + t('acceptance.rejectedAlert', { reason: openTask.rejectReason })),
3802
+ )
3803
+ : null,
3804
+ openTask.worktree
3805
+ ? h('div', {
3806
+ style: {
3807
+ margin: '8px 0', padding: 12,
3808
+ background: 'var(--dsw-alias-bg-layer-3)',
3809
+ border: '1px solid var(--dsw-alias-border-l2)',
3810
+ borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
3811
+ },
3812
+ },
3813
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3814
+ h('div', { className: 'dkb-group', style: { margin: 0 } },
3815
+ t('diff.title'),
3816
+ commitsData.length > 0 ? ` · ${t('diff.commits', { n: commitsData.length })}` : '',
3817
+ diffData?.filesChanged ? ` · ${t('diff.filesChanged', { n: diffData.filesChanged })}` : '',
3822
3818
  ),
3823
- diffExpanded ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
3824
- commitsData.length > 0
3825
- ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
3826
- commitsData.map((cm) => h('div', {
3827
- key: cm.hash,
3828
- style: { fontSize: 11, display: 'flex', gap: 6, opacity: 0.85, fontFamily: 'monospace' },
3829
- },
3830
- h('span', { style: { color: 'var(--dsw-alias-link, #3b82f6)' } }, cm.hash),
3831
- h('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
3832
- h('span', { style: { opacity: 0.6 } }, cm.date),
3833
- ))
3834
- )
3835
- : null,
3836
- diffLoading
3837
- ? h('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
3838
- : h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
3839
- diffData?.files && diffData.files.length > 0
3840
- ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, margin: '2px 0' } },
3841
- diffData.files.map((f) => h('div', {
3842
- key: f.path,
3819
+ h('button', {
3820
+ type: 'button', className: 'dkb-discard',
3821
+ style: { padding: '2px 8px', fontSize: 11 },
3822
+ onClick: () => {
3823
+ if (!diffData && !diffExpanded) loadDiffAndCommits()
3824
+ setDiffExpanded(!diffExpanded)
3825
+ },
3826
+ }, diffExpanded ? t('diff.hide') : t('diff.show')),
3827
+ ),
3828
+ diffExpanded ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
3829
+ commitsData.length > 0
3830
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
3831
+ commitsData.map((cm) => h('div', {
3832
+ key: cm.hash,
3833
+ style: { fontSize: 11, display: 'flex', gap: 6, opacity: 0.85, fontFamily: 'monospace' },
3834
+ },
3835
+ h('span', { style: { color: 'var(--dsw-alias-link, #3b82f6)' } }, cm.hash),
3836
+ h('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
3837
+ h('span', { style: { opacity: 0.6 } }, cm.date),
3838
+ ))
3839
+ )
3840
+ : null,
3841
+ diffLoading
3842
+ ? h('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
3843
+ : h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
3844
+ diffData?.files && diffData.files.length > 0
3845
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, margin: '2px 0' } },
3846
+ diffData.files.map((f) => h('div', {
3847
+ key: f.path,
3848
+ style: {
3849
+ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11,
3850
+ padding: '3px 6px', background: 'var(--dsw-alias-bg-layer-2)',
3851
+ borderRadius: 4, border: '1px solid var(--dsw-alias-border-l1)',
3852
+ },
3853
+ },
3854
+ h('span', {
3843
3855
  style: {
3844
- display: 'flex', alignItems: 'center', gap: 6, fontSize: 11,
3845
- padding: '3px 6px', background: 'var(--dsw-alias-bg-layer-2)',
3846
- borderRadius: 4, border: '1px solid var(--dsw-alias-border-l1)',
3856
+ fontWeight: 'bold', fontSize: 10, padding: '1px 4px', borderRadius: 3,
3857
+ background: f.status === 'added' ? 'var(--dsw-alias-state-success-secondary, #dafbe1)'
3858
+ : (f.status === 'deleted' ? 'var(--dsw-alias-state-error-secondary, #ffebe9)'
3859
+ : 'var(--dsw-alias-state-warn-secondary, #fff8c5)'),
3860
+ color: f.status === 'added' ? 'var(--dsw-alias-state-success-primary, #1a7f37)'
3861
+ : (f.status === 'deleted' ? 'var(--dsw-alias-state-error-primary, #cf222e)'
3862
+ : 'var(--dsw-alias-state-warn-primary, #9a6700)'),
3847
3863
  },
3848
- },
3849
- h('span', {
3850
- style: {
3851
- fontWeight: 'bold', fontSize: 10, padding: '1px 4px', borderRadius: 3,
3852
- background: f.status === 'added' ? 'var(--dsw-alias-state-success-secondary, #dafbe1)'
3853
- : (f.status === 'deleted' ? 'var(--dsw-alias-state-error-secondary, #ffebe9)'
3854
- : 'var(--dsw-alias-state-warn-secondary, #fff8c5)'),
3855
- color: f.status === 'added' ? 'var(--dsw-alias-state-success-primary, #1a7f37)'
3856
- : (f.status === 'deleted' ? 'var(--dsw-alias-state-error-primary, #cf222e)'
3857
- : 'var(--dsw-alias-state-warn-primary, #9a6700)'),
3858
- },
3859
- }, f.status === 'added' ? 'A' : (f.status === 'deleted' ? 'D' : (f.status === 'untracked' ? '?' : 'M'))),
3860
- h('span', { style: { flex: 1, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis' } }, f.path),
3861
- (f.insertions || f.deletions)
3862
- ? h('span', { style: { fontSize: 10, fontFamily: 'monospace', opacity: 0.85 } },
3863
- f.insertions ? h('span', { style: { color: 'var(--dsw-alias-state-success-primary, #1a7f37)', marginRight: 4 } }, `+${f.insertions}`) : null,
3864
- f.deletions ? h('span', { style: { color: 'var(--dsw-alias-state-error-primary, #cf222e)' } }, `-${f.deletions}`) : null,
3865
- )
3866
- : null,
3867
- h('button', {
3868
- type: 'button',
3869
- className: 'dkb-discard',
3870
- style: { padding: '1px 6px', fontSize: 10, cursor: 'pointer' },
3871
- title: t('diff.discard') || 'Discard',
3872
- onClick: async (e) => {
3873
- e.stopPropagation()
3874
- const confirmMsg = t('diff.confirmDiscard', { file: f.path }) || `Discard changes to ${f.path}?`
3875
- if (window.confirm(confirmMsg)) {
3876
- try {
3877
- await api('/task/' + encodeURIComponent(openTask.id) + '/discard', {
3878
- method: 'POST',
3879
- body: JSON.stringify({ file: f.path }),
3880
- })
3881
- await loadDiffAndCommits()
3882
- await load(board)
3883
- } catch (err) {
3884
- setError(t(err.key || err.error || 'error.unknown'))
3885
- }
3864
+ }, f.status === 'added' ? 'A' : (f.status === 'deleted' ? 'D' : (f.status === 'untracked' ? '?' : 'M'))),
3865
+ h('span', { style: { flex: 1, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis' } }, f.path),
3866
+ (f.insertions || f.deletions)
3867
+ ? h('span', { style: { fontSize: 10, fontFamily: 'monospace', opacity: 0.85 } },
3868
+ f.insertions ? h('span', { style: { color: 'var(--dsw-alias-state-success-primary, #1a7f37)', marginRight: 4 } }, `+${f.insertions}`) : null,
3869
+ f.deletions ? h('span', { style: { color: 'var(--dsw-alias-state-error-primary, #cf222e)' } }, `-${f.deletions}`) : null,
3870
+ )
3871
+ : null,
3872
+ h('button', {
3873
+ type: 'button',
3874
+ className: 'dkb-discard',
3875
+ style: { padding: '1px 6px', fontSize: 10, cursor: 'pointer' },
3876
+ title: t('diff.discard') || 'Discard',
3877
+ onClick: async (e) => {
3878
+ e.stopPropagation()
3879
+ const confirmMsg = t('diff.confirmDiscard', { file: f.path }) || `Discard changes to ${f.path}?`
3880
+ if (window.confirm(confirmMsg)) {
3881
+ try {
3882
+ await api('/task/' + encodeURIComponent(openTask.id) + '/discard', {
3883
+ method: 'POST',
3884
+ body: JSON.stringify({ file: f.path }),
3885
+ })
3886
+ await loadDiffAndCommits()
3887
+ await load(board)
3888
+ } catch (err) {
3889
+ setError(t(err.key || err.error || 'error.unknown'))
3886
3890
  }
3887
- },
3888
- }, t('diff.discard') || 'Discard')
3889
- ))
3890
- )
3891
- : null,
3892
- diffData?.diff
3893
- ? h('pre', {
3894
- style: {
3895
- margin: 0, padding: 8, maxHeight: 280, overflowY: 'auto',
3896
- background: 'var(--dsw-alias-bg-layer-2)',
3897
- color: 'var(--dsw-alias-label-primary)',
3898
- fontSize: 11, fontFamily: 'monospace', borderRadius: 4, whiteSpace: 'pre-wrap',
3899
- border: '1px solid var(--dsw-alias-border-l1)',
3900
- },
3901
- }, diffData.diff)
3902
- : (!diffData?.files || diffData.files.length === 0
3903
- ? h('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
3904
- : null)
3905
- )
3906
- ) : null
3907
- )
3908
- : null,
3909
- openTask.column === 'review'
3910
- ? h('div', {
3911
- style: {
3912
- margin: '8px 0', padding: 12,
3913
- background: 'var(--dsw-alias-bg-layer-3)',
3914
- border: '1px solid var(--dsw-alias-border-l2)',
3915
- borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
3916
- },
3891
+ }
3892
+ },
3893
+ }, t('diff.discard') || 'Discard')
3894
+ ))
3895
+ )
3896
+ : null,
3897
+ diffData?.diff
3898
+ ? h('pre', {
3899
+ style: {
3900
+ margin: 0, padding: 8, maxHeight: 280, overflowY: 'auto',
3901
+ background: 'var(--dsw-alias-bg-layer-2)',
3902
+ color: 'var(--dsw-alias-label-primary)',
3903
+ fontSize: 11, fontFamily: 'monospace', borderRadius: 4, whiteSpace: 'pre-wrap',
3904
+ border: '1px solid var(--dsw-alias-border-l1)',
3905
+ },
3906
+ }, diffData.diff)
3907
+ : (!diffData?.files || diffData.files.length === 0
3908
+ ? h('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
3909
+ : null)
3910
+ )
3911
+ ) : null
3912
+ )
3913
+ : null,
3914
+ openTask.column === 'review'
3915
+ ? h('div', {
3916
+ style: {
3917
+ margin: '8px 0', padding: 12,
3918
+ background: 'var(--dsw-alias-bg-layer-3)',
3919
+ border: '1px solid var(--dsw-alias-border-l2)',
3920
+ borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
3917
3921
  },
3918
- h('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
3919
- h('div', { style: { display: 'flex', gap: 8 } },
3920
- openTask.worktree
3921
- ? h('button', {
3922
- type: 'button', className: 'dkb-save',
3923
- disabled: merging,
3924
- style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-primary-inverted, #fff)' },
3925
- onClick: async () => {
3926
- try {
3927
- setMerging(true)
3928
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/merge', { method: 'POST', body: '{}' })
3929
- if (out && out.task) setOpenTask(out.task)
3930
- await load(board)
3931
- } catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
3932
- finally { setMerging(false) }
3933
- },
3934
- }, merging ? t('acceptance.merging') : '🔀 ' + t('acceptance.acceptAndMerge'))
3935
- : null,
3936
- h('button', {
3937
- type: 'button', className: 'dkb-save',
3938
- onClick: async () => {
3939
- try {
3940
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/accept', { method: 'POST', body: '{}' })
3941
- if (out && out.task) setOpenTask(out.task)
3942
- await load(board)
3943
- } catch (e) { setError(t(e.key || 'error.unknown')) }
3944
- },
3945
- }, '✓ ' + t('acceptance.accept')),
3946
- h('button', {
3947
- type: 'button', className: 'dkb-discard',
3948
- onClick: () => { setRejectModalOpen(true); setRejectReasonText('') },
3949
- }, '✗ ' + t('acceptance.reject')),
3950
- openTask.owner && openTask.repo
3951
- ? h('button', {
3922
+ },
3923
+ h('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
3924
+ h('div', { style: { display: 'flex', gap: 8 } },
3925
+ openTask.worktree
3926
+ ? h('button', {
3927
+ type: 'button', className: 'dkb-save',
3928
+ disabled: merging,
3929
+ style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-primary-inverted, #fff)' },
3930
+ onClick: async () => {
3931
+ try {
3932
+ setMerging(true)
3933
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/merge', { method: 'POST', body: '{}' })
3934
+ if (out && out.task) setOpenTask(out.task)
3935
+ await load(board)
3936
+ } catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
3937
+ finally { setMerging(false) }
3938
+ },
3939
+ }, merging ? t('acceptance.merging') : '🔀 ' + t('acceptance.acceptAndMerge'))
3940
+ : null,
3941
+ h('button', {
3942
+ type: 'button', className: 'dkb-save',
3943
+ onClick: async () => {
3944
+ try {
3945
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/accept', { method: 'POST', body: '{}' })
3946
+ if (out && out.task) setOpenTask(out.task)
3947
+ await load(board)
3948
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3949
+ },
3950
+ }, '✓ ' + t('acceptance.accept')),
3951
+ h('button', {
3952
+ type: 'button', className: 'dkb-discard',
3953
+ onClick: () => { setRejectModalOpen(true); setRejectReasonText('') },
3954
+ }, '✗ ' + t('acceptance.reject')),
3955
+ openTask.owner && openTask.repo
3956
+ ? h('button', {
3957
+ type: 'button', className: 'dkb-save',
3958
+ disabled: prCreating,
3959
+ style: { background: 'var(--dsw-alias-state-business-primary, #0969da)', color: 'var(--dsw-alias-label-primary-inverted, #fff)' },
3960
+ onClick: async () => {
3961
+ setPrCreating(true)
3962
+ try {
3963
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/pr', {
3964
+ method: 'POST',
3965
+ body: JSON.stringify({ base: 'main' }),
3966
+ })
3967
+ if (out && out.pr) setCreatedPr(out.pr)
3968
+ await load(board)
3969
+ } catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
3970
+ finally { setPrCreating(false) }
3971
+ },
3972
+ }, prCreating ? t('pr.creating') : '🐙 ' + t('pr.create'))
3973
+ : null,
3974
+ createdPr
3975
+ ? h('a', {
3976
+ className: 'dkb-save',
3977
+ style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-primary-inverted, #fff)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
3978
+ href: createdPr.html_url,
3979
+ target: '_blank', rel: 'noopener noreferrer',
3980
+ }, '↗ ' + t('pr.view') + ` #${createdPr.number}`)
3981
+ : null,
3982
+ ),
3983
+ rejectModalOpen
3984
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 } },
3985
+ h('span', { className: 'dkb-label' }, t('acceptance.rejectTitle')),
3986
+ h('textarea', {
3987
+ className: 'dkb-area',
3988
+ value: rejectReasonText,
3989
+ placeholder: t('acceptance.rejectReasonPlaceholder'),
3990
+ onChange: (e) => setRejectReasonText(e.target.value),
3991
+ rows: 3,
3992
+ }),
3993
+ h('div', { style: { display: 'flex', gap: 6 } },
3994
+ h('button', {
3952
3995
  type: 'button', className: 'dkb-save',
3953
- disabled: prCreating,
3954
- style: { background: 'var(--dsw-alias-state-business-primary, #0969da)', color: 'var(--dsw-alias-label-primary-inverted, #fff)' },
3955
3996
  onClick: async () => {
3956
- setPrCreating(true)
3997
+ if (!rejectReasonText.trim()) return
3957
3998
  try {
3958
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/pr', {
3999
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/reject', {
3959
4000
  method: 'POST',
3960
- body: JSON.stringify({ base: 'main' }),
4001
+ body: JSON.stringify({ reason: rejectReasonText.trim() }),
3961
4002
  })
3962
- if (out && out.pr) setCreatedPr(out.pr)
4003
+ if (out && out.task) setOpenTask(out.task)
4004
+ setRejectModalOpen(false)
3963
4005
  await load(board)
3964
- } catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
3965
- finally { setPrCreating(false) }
4006
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
3966
4007
  },
3967
- }, prCreating ? t('pr.creating') : '🐙 ' + t('pr.create'))
3968
- : null,
3969
- createdPr
3970
- ? h('a', {
3971
- className: 'dkb-save',
3972
- style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-primary-inverted, #fff)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
3973
- href: createdPr.html_url,
3974
- target: '_blank', rel: 'noopener noreferrer',
3975
- }, '↗ ' + t('pr.view') + ` #${createdPr.number}`)
3976
- : null,
3977
- ),
3978
- rejectModalOpen
3979
- ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 } },
3980
- h('span', { className: 'dkb-label' }, t('acceptance.rejectTitle')),
3981
- h('textarea', {
3982
- className: 'dkb-area',
3983
- value: rejectReasonText,
3984
- placeholder: t('acceptance.rejectReasonPlaceholder'),
3985
- onChange: (e) => setRejectReasonText(e.target.value),
3986
- rows: 3,
3987
- }),
3988
- h('div', { style: { display: 'flex', gap: 6 } },
3989
- h('button', {
3990
- type: 'button', className: 'dkb-save',
3991
- onClick: async () => {
3992
- if (!rejectReasonText.trim()) return
3993
- try {
3994
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/reject', {
3995
- method: 'POST',
3996
- body: JSON.stringify({ reason: rejectReasonText.trim() }),
3997
- })
3998
- if (out && out.task) setOpenTask(out.task)
3999
- setRejectModalOpen(false)
4000
- await load(board)
4001
- } catch (e) { setError(t(e.key || 'error.unknown')) }
4002
- },
4003
- }, t('acceptance.rejectSubmit')),
4004
- h('button', {
4005
- type: 'button', className: 'dkb-discard',
4006
- onClick: () => setRejectModalOpen(false),
4007
- }, t('dialog.cancel')),
4008
- ),
4009
- )
4010
- : null,
4011
- )
4012
- : null,
4013
- h('div', { style: { margin: '4px 0' } },
4014
- h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
4015
- h('span', null, t('checklist.title')),
4016
- Array.isArray(openTask.checklist) && openTask.checklist.length > 0
4017
- ? h('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
4018
- t('checklist.progress', {
4019
- done: openTask.checklist.filter((i) => i.completed).length,
4020
- total: openTask.checklist.length,
4021
- }))
4008
+ }, t('acceptance.rejectSubmit')),
4009
+ h('button', {
4010
+ type: 'button', className: 'dkb-discard',
4011
+ onClick: () => setRejectModalOpen(false),
4012
+ }, t('dialog.cancel')),
4013
+ ),
4014
+ )
4022
4015
  : null,
4023
- ),
4016
+ )
4017
+ : null,
4018
+ h('div', { style: { margin: '4px 0' } },
4019
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
4020
+ h('span', null, t('checklist.title')),
4024
4021
  Array.isArray(openTask.checklist) && openTask.checklist.length > 0
4025
- ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
4026
- openTask.checklist.map((item, idx) => h('li', {
4027
- key: idx,
4028
- style: {
4029
- display: 'flex', flexDirection: 'column', gap: 2, padding: '4px 8px',
4030
- background: item.completed ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-3)',
4031
- borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)',
4032
- },
4022
+ ? h('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
4023
+ t('checklist.progress', {
4024
+ done: openTask.checklist.filter((i) => i.completed).length,
4025
+ total: openTask.checklist.length,
4026
+ }))
4027
+ : null,
4028
+ ),
4029
+ Array.isArray(openTask.checklist) && openTask.checklist.length > 0
4030
+ ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
4031
+ openTask.checklist.map((item, idx) => h('li', {
4032
+ key: idx,
4033
+ style: {
4034
+ display: 'flex', flexDirection: 'column', gap: 2, padding: '4px 8px',
4035
+ background: item.completed ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-3)',
4036
+ borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)',
4033
4037
  },
4034
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
4035
- h('input', {
4036
- type: 'checkbox',
4037
- checked: Boolean(item.completed),
4038
- onChange: async (e) => {
4039
- const updated = [...openTask.checklist]
4040
- updated[idx] = { ...updated[idx], completed: e.target.checked }
4041
- try {
4042
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/checklist', {
4043
- method: 'POST',
4044
- body: JSON.stringify({ checklist: updated }),
4045
- })
4046
- if (out && out.task) setOpenTask(out.task)
4047
- await load(board)
4048
- } catch (err) { setError(t(err.key || 'error.unknown')) }
4049
- },
4050
- }),
4051
- h('span', {
4052
- style: {
4053
- textDecoration: item.completed ? 'line-through' : 'none',
4054
- color: item.completed ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-primary)',
4055
- fontSize: 13, flex: 1,
4056
- },
4057
- }, item.text),
4058
- item.required !== false
4059
- ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
4060
- : null,
4061
- ),
4062
- item.evidence
4063
- ? h('div', {
4064
- style: {
4065
- fontSize: 11, color: 'var(--dsw-alias-label-secondary)', marginLeft: 24,
4066
- fontFamily: 'var(--dsw-font-family-mono, monospace)', whiteSpace: 'pre-wrap',
4067
- },
4068
- }, t('checklist.evidence', { text: item.evidence }))
4069
- : null,
4070
- )))
4071
- : h('p', { className: 'dkb-hint' }, t('checklist.empty')),
4072
- h('div', { style: { display: 'flex', gap: 6, marginTop: 4 } },
4073
- h('input', {
4074
- className: 'dkb-input',
4075
- style: { flex: 1 },
4076
- placeholder: t('checklist.placeholder'),
4077
- value: newChecklistText,
4078
- onChange: (e) => setNewChecklistText(e.target.value),
4079
- onKeyDown: async (e) => {
4080
- if (e.key === 'Enter' && newChecklistText.trim()) {
4081
- e.preventDefault()
4082
- const updated = [...(openTask.checklist || []), { text: newChecklistText.trim(), required: true, completed: false }]
4083
- setNewChecklistText('')
4084
- try {
4085
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/checklist', {
4086
- method: 'POST',
4087
- body: JSON.stringify({ checklist: updated }),
4088
- })
4089
- if (out && out.task) setOpenTask(out.task)
4090
- await load(board)
4091
- } catch (err) { setError(t(err.key || 'error.unknown')) }
4092
- }
4093
4038
  },
4094
- }),
4095
- h('button', {
4096
- type: 'button', className: 'dkb-save',
4097
- onClick: async () => {
4098
- if (!newChecklistText.trim()) return
4039
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
4040
+ h('input', {
4041
+ type: 'checkbox',
4042
+ checked: Boolean(item.completed),
4043
+ onChange: async (e) => {
4044
+ const updated = [...openTask.checklist]
4045
+ updated[idx] = { ...updated[idx], completed: e.target.checked }
4046
+ try {
4047
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/checklist', {
4048
+ method: 'POST',
4049
+ body: JSON.stringify({ checklist: updated }),
4050
+ })
4051
+ if (out && out.task) setOpenTask(out.task)
4052
+ await load(board)
4053
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
4054
+ },
4055
+ }),
4056
+ h('span', {
4057
+ style: {
4058
+ textDecoration: item.completed ? 'line-through' : 'none',
4059
+ color: item.completed ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-primary)',
4060
+ fontSize: 13, flex: 1,
4061
+ },
4062
+ }, item.text),
4063
+ item.required !== false
4064
+ ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
4065
+ : null,
4066
+ ),
4067
+ item.evidence
4068
+ ? h('div', {
4069
+ style: {
4070
+ fontSize: 11, color: 'var(--dsw-alias-label-secondary)', marginLeft: 24,
4071
+ fontFamily: 'var(--dsw-font-family-mono, monospace)', whiteSpace: 'pre-wrap',
4072
+ },
4073
+ }, t('checklist.evidence', { text: item.evidence }))
4074
+ : null,
4075
+ )))
4076
+ : h('p', { className: 'dkb-hint' }, t('checklist.empty')),
4077
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 4 } },
4078
+ h('input', {
4079
+ className: 'dkb-input',
4080
+ style: { flex: 1 },
4081
+ placeholder: t('checklist.placeholder'),
4082
+ value: newChecklistText,
4083
+ onChange: (e) => setNewChecklistText(e.target.value),
4084
+ onKeyDown: async (e) => {
4085
+ if (e.key === 'Enter' && newChecklistText.trim()) {
4086
+ e.preventDefault()
4099
4087
  const updated = [...(openTask.checklist || []), { text: newChecklistText.trim(), required: true, completed: false }]
4100
4088
  setNewChecklistText('')
4101
4089
  try {
@@ -4106,228 +4094,411 @@ window.__ModuleLoader__.load({
4106
4094
  if (out && out.task) setOpenTask(out.task)
4107
4095
  await load(board)
4108
4096
  } catch (err) { setError(t(err.key || 'error.unknown')) }
4109
- },
4110
- }, t('checklist.add')),
4111
- ),
4097
+ }
4098
+ },
4099
+ }),
4100
+ h('button', {
4101
+ type: 'button', className: 'dkb-save',
4102
+ onClick: async () => {
4103
+ if (!newChecklistText.trim()) return
4104
+ const updated = [...(openTask.checklist || []), { text: newChecklistText.trim(), required: true, completed: false }]
4105
+ setNewChecklistText('')
4106
+ try {
4107
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/checklist', {
4108
+ method: 'POST',
4109
+ body: JSON.stringify({ checklist: updated }),
4110
+ })
4111
+ if (out && out.task) setOpenTask(out.task)
4112
+ await load(board)
4113
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
4114
+ },
4115
+ }, t('checklist.add')),
4112
4116
  ),
4113
- openTask.report && openTask.report.summary
4114
- ? h('div', {
4115
- style: {
4116
- margin: '6px 0', padding: 10,
4117
- background: 'var(--dsw-alias-bg-layer-1)',
4118
- border: '1px solid var(--dsw-alias-border-l2)',
4119
- borderRadius: 6,
4120
- },
4117
+ ),
4118
+ openTask.report && openTask.report.summary
4119
+ ? h('div', {
4120
+ style: {
4121
+ margin: '6px 0', padding: 10,
4122
+ background: 'var(--dsw-alias-bg-layer-1)',
4123
+ border: '1px solid var(--dsw-alias-border-l2)',
4124
+ borderRadius: 6,
4121
4125
  },
4122
- h('div', { className: 'dkb-group', style: { margin: '0 0 6px 0' } }, t('report.title')),
4123
- h('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)', marginBottom: 6, fontWeight: 500 } },
4124
- openTask.report.summary),
4125
- openTask.report.changedFiles && openTask.report.changedFiles.length
4126
- ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
4127
- h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.changedFiles') + ': '),
4128
- openTask.report.changedFiles.map((f) => h('span', { className: 'dkb-tag', key: f, style: { margin: '1px 2px', fontFamily: 'monospace' } }, f)),
4129
- )
4130
- : null,
4131
- openTask.report.checksRun && openTask.report.checksRun.length
4132
- ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
4133
- h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.checksRun') + ': '),
4134
- openTask.report.checksRun.map((chk, i) => h('div', { key: i, style: { fontFamily: 'monospace', fontSize: 11, marginLeft: 8 } }, '• ' + chk)),
4135
- )
4136
- : null,
4137
- openTask.report.artifacts && openTask.report.artifacts.length
4138
- ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
4139
- h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.artifacts') + ': '),
4140
- openTask.report.artifacts.map((a, i) => h('span', { className: 'dkb-tag', key: i, style: { margin: '1px 2px' } }, a)),
4141
- )
4142
- : null,
4143
- openTask.report.risks && openTask.report.risks.length
4144
- ? h('div', { style: { fontSize: 12 } },
4145
- h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.risks') + ': '),
4146
- openTask.report.risks.map((r, i) => h('div', { key: i, style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary, #b08800)', marginLeft: 8 } }, '⚠ ' + r)),
4147
- )
4148
- : null,
4149
- )
4150
- : null,
4151
- h('div', { style: { margin: '6px 0' } },
4152
- h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
4153
- h('span', null, t('comments.title')),
4154
- Array.isArray(openTask.comments) && openTask.comments.length > 0
4155
- ? h('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
4126
+ },
4127
+ h('div', { className: 'dkb-group', style: { margin: '0 0 6px 0' } }, t('report.title')),
4128
+ h('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)', marginBottom: 6, fontWeight: 500 } },
4129
+ openTask.report.summary),
4130
+ openTask.report.changedFiles && openTask.report.changedFiles.length
4131
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
4132
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.changedFiles') + ': '),
4133
+ openTask.report.changedFiles.map((f) => h('span', { className: 'dkb-tag', key: f, style: { margin: '1px 2px', fontFamily: 'monospace' } }, f)),
4134
+ )
4156
4135
  : null,
4157
- ),
4136
+ openTask.report.checksRun && openTask.report.checksRun.length
4137
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
4138
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.checksRun') + ': '),
4139
+ openTask.report.checksRun.map((chk, i) => h('div', { key: i, style: { fontFamily: 'monospace', fontSize: 11, marginLeft: 8 } }, '• ' + chk)),
4140
+ )
4141
+ : null,
4142
+ openTask.report.artifacts && openTask.report.artifacts.length
4143
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
4144
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.artifacts') + ': '),
4145
+ openTask.report.artifacts.map((a, i) => h('span', { className: 'dkb-tag', key: i, style: { margin: '1px 2px' } }, a)),
4146
+ )
4147
+ : null,
4148
+ openTask.report.risks && openTask.report.risks.length
4149
+ ? h('div', { style: { fontSize: 12 } },
4150
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.risks') + ': '),
4151
+ openTask.report.risks.map((r, i) => h('div', { key: i, style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary, #b08800)', marginLeft: 8 } }, '⚠ ' + r)),
4152
+ )
4153
+ : null,
4154
+ )
4155
+ : null,
4156
+ h('div', { style: { margin: '6px 0' } },
4157
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
4158
+ h('span', null, t('comments.title')),
4158
4159
  Array.isArray(openTask.comments) && openTask.comments.length > 0
4159
- ? h('ul', {
4160
- style: {
4161
- listStyle: 'none', padding: 0, margin: '4px 0', maxHeight: 200, overflowY: 'auto',
4162
- display: 'flex', flexDirection: 'column', gap: 6,
4163
- },
4160
+ ? h('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
4161
+ : null,
4162
+ ),
4163
+ Array.isArray(openTask.comments) && openTask.comments.length > 0
4164
+ ? h('ul', {
4165
+ style: {
4166
+ listStyle: 'none', padding: 0, margin: '4px 0', maxHeight: 200, overflowY: 'auto',
4167
+ display: 'flex', flexDirection: 'column', gap: 6,
4164
4168
  },
4165
- openTask.comments.map((c, i) => h('li', {
4166
- key: c.id || i,
4167
- style: {
4168
- padding: 8, background: 'var(--dsw-alias-bg-layer-3)',
4169
- borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
4170
- },
4169
+ },
4170
+ openTask.comments.map((c, i) => h('li', {
4171
+ key: c.id || i,
4172
+ style: {
4173
+ padding: 8, background: 'var(--dsw-alias-bg-layer-3)',
4174
+ borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
4171
4175
  },
4172
- h('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
4173
- h('span', {
4174
- className: 'dkb-tag',
4175
- style: {
4176
- fontWeight: 600,
4177
- background: c.role === 'agent' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-2)',
4178
- },
4179
- }, c.role === 'agent' ? t('comments.authorAgent') : (c.author || t('comments.authorUser'))),
4180
- h('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
4181
- new Date(c.createdAt || Date.now()).toLocaleTimeString()),
4182
- ),
4183
- h('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
4184
- )))
4185
- : h('p', { className: 'dkb-hint' }, t('comments.empty')),
4186
- h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
4187
- h('textarea', {
4188
- className: 'dkb-area',
4189
- placeholder: t('comments.placeholder'),
4190
- value: newCommentText,
4191
- rows: 2,
4192
- onChange: (e) => setNewCommentText(e.target.value),
4193
- }),
4194
- h('button', {
4195
- type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
4196
- onClick: async () => {
4197
- if (!newCommentText.trim()) return
4198
- try {
4199
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/comments', {
4200
- method: 'POST',
4201
- body: JSON.stringify({ text: newCommentText.trim(), role: 'user', author: 'user' }),
4202
- })
4203
- if (out && out.task) setOpenTask(out.task)
4204
- setNewCommentText('')
4205
- await load(board)
4206
- } catch (err) { setError(t(err.key || 'error.unknown')) }
4207
4176
  },
4208
- }, t('comments.add')),
4209
- ),
4177
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
4178
+ h('span', {
4179
+ className: 'dkb-tag',
4180
+ style: {
4181
+ fontWeight: 600,
4182
+ background: c.role === 'agent' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-2)',
4183
+ },
4184
+ }, c.role === 'agent' ? t('comments.authorAgent') : (c.author || t('comments.authorUser'))),
4185
+ h('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
4186
+ new Date(c.createdAt || Date.now()).toLocaleTimeString()),
4187
+ ),
4188
+ h('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
4189
+ )))
4190
+ : h('p', { className: 'dkb-hint' }, t('comments.empty')),
4191
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
4192
+ h('textarea', {
4193
+ className: 'dkb-area',
4194
+ placeholder: t('comments.placeholder'),
4195
+ value: newCommentText,
4196
+ rows: 2,
4197
+ onChange: (e) => setNewCommentText(e.target.value),
4198
+ }),
4199
+ h('button', {
4200
+ type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
4201
+ onClick: async () => {
4202
+ if (!newCommentText.trim()) return
4203
+ try {
4204
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/comments', {
4205
+ method: 'POST',
4206
+ body: JSON.stringify({ text: newCommentText.trim(), role: 'user', author: 'user' }),
4207
+ })
4208
+ if (out && out.task) setOpenTask(out.task)
4209
+ setNewCommentText('')
4210
+ await load(board)
4211
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
4212
+ },
4213
+ }, t('comments.add')),
4214
+ ),
4215
+ ),
4216
+ openTask.archivedAt
4217
+ ? h('button', {
4218
+ type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
4219
+ onClick: async () => {
4220
+ try {
4221
+ await api('/task/' + encodeURIComponent(openTask.id) + '/restore', { method: 'POST', body: '{}' })
4222
+ setArchive((cur) => (cur || []).filter((x) => x.id !== openTask.id))
4223
+ setOpenTask(null)
4224
+ await load(board)
4225
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
4226
+ },
4227
+ }, t('archive.restore'))
4228
+ : h('div', { className: 'dkb-start' },
4229
+ h('div', { className: 'dkb-group' },
4230
+ openTask.sessionId ? t('panel.continue') : t('panel.start')),
4231
+ h('p', { className: 'dkb-hint' },
4232
+ openTask.sessionId ? t('panel.continueHint') : t('panel.startHint')),
4233
+ !openTask.sessionId && state && !state.projectRoot.set && state.projectRoot.path !== ''
4234
+ ? h('p', { className: 'dkb-hint dkb-workdir' },
4235
+ t('panel.workDir', { path: state.projectRoot.path }),
4236
+ h('br'),
4237
+ t('panel.workDirHint'))
4238
+ : null,
4239
+ React.createElement(LaunchFields, {
4240
+ t, presets, agentPreset, permission, disabled: starting,
4241
+ onPreset: setAgentPreset, onPermission: setPermission,
4242
+ hidePreset: Boolean(openTask.sessionId),
4243
+ }),
4244
+ h('div', { className: 'dkb-field' },
4245
+ h('span', { className: 'dkb-label' }, t('panel.provider')),
4246
+ h('select', {
4247
+ className: 'dkb-input', value: provider, disabled: starting || providers.length === 0,
4248
+ onChange: (e) => { setProvider(e.target.value); setModel('') },
4249
+ },
4250
+ h('option', { value: '' },
4251
+ providers.length === 0 ? t('panel.noProviders') : t('panel.pickProvider')),
4252
+ providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
4210
4253
  ),
4211
- openTask.archivedAt
4254
+ ),
4255
+ h('div', { className: 'dkb-field' },
4256
+ h('span', { className: 'dkb-label' }, t('panel.model')),
4257
+ h('select', {
4258
+ className: 'dkb-input', value: model,
4259
+ disabled: starting || provider === '' || models.length === 0,
4260
+ onChange: (e) => setModel(e.target.value),
4261
+ },
4262
+ h('option', { value: '' },
4263
+ provider === '' ? t('panel.pickProviderFirst')
4264
+ : models.length === 0 ? t('panel.noModels') : t('panel.pickModel')),
4265
+ models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
4266
+ ),
4267
+ ),
4268
+ h('div', { className: 'dkb-field' },
4269
+ h('span', { className: 'dkb-label' }, t('panel.message')),
4270
+ h('textarea', {
4271
+ className: 'dkb-area', value: draftText, disabled: starting,
4272
+ onChange: (e) => setDraftText(e.target.value),
4273
+ }),
4274
+ h('p', { className: 'dkb-hint' }, t('panel.messageHint')),
4275
+ ),
4276
+ h('div', { className: 'dkb-foot dkb-footSplit' },
4277
+ h('div', { className: 'dkb-footLeft' },
4278
+ h('button', {
4279
+ type: 'button', className: 'dkb-danger',
4280
+ onClick: async () => {
4281
+ if (!window.confirm(t('panel.deleteConfirm', { title: openTask.title }))) return
4282
+ try {
4283
+ await api('/task/' + encodeURIComponent(openTask.id), { method: 'DELETE' })
4284
+ setOpenTask(null)
4285
+ await load(board)
4286
+ } catch (e) {
4287
+ setError(t(e.key || 'error.unknown'))
4288
+ }
4289
+ },
4290
+ }, t('panel.delete')),
4291
+ openTask.column === 'done'
4212
4292
  ? h('button', {
4213
- type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
4293
+ type: 'button', className: 'dkb-discard',
4214
4294
  onClick: async () => {
4215
4295
  try {
4216
- await api('/task/' + encodeURIComponent(openTask.id) + '/restore', { method: 'POST', body: '{}' })
4217
- setArchive((cur) => (cur || []).filter((x) => x.id !== openTask.id))
4296
+ await api('/task/' + encodeURIComponent(openTask.id) + '/archive', { method: 'POST', body: '{}' })
4218
4297
  setOpenTask(null)
4219
4298
  await load(board)
4220
4299
  } catch (e) { setError(t(e.key || 'error.unknown')) }
4221
4300
  },
4222
- }, t('archive.restore'))
4223
- : h('div', { className: 'dkb-start' },
4224
- h('div', { className: 'dkb-group' },
4225
- openTask.sessionId ? t('panel.continue') : t('panel.start')),
4226
- h('p', { className: 'dkb-hint' },
4227
- openTask.sessionId ? t('panel.continueHint') : t('panel.startHint')),
4228
- !openTask.sessionId && state && !state.projectRoot.set && state.projectRoot.path !== ''
4229
- ? h('p', { className: 'dkb-hint dkb-workdir' },
4230
- t('panel.workDir', { path: state.projectRoot.path }),
4231
- h('br'),
4232
- t('panel.workDirHint'))
4301
+ }, t('panel.archive'))
4233
4302
  : null,
4234
- React.createElement(LaunchFields, {
4235
- t, presets, agentPreset, permission, disabled: starting,
4236
- onPreset: setAgentPreset, onPermission: setPermission,
4237
- hidePreset: Boolean(openTask.sessionId),
4238
- }),
4303
+ openTask.column === 'cleanup' && openTask.worktree
4304
+ ? h('button', {
4305
+ type: 'button', className: 'dkb-discard',
4306
+ onClick: async () => {
4307
+ try {
4308
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/cleanup-worktree', {
4309
+ method: 'POST', body: '{}',
4310
+ })
4311
+ if (out && out.error === 'worktree-dirty') {
4312
+ alert(t('panel.worktreeDirty', { files: (out.files || []).join(', ') }))
4313
+ } else {
4314
+ setOpenTask(null)
4315
+ await load(board)
4316
+ }
4317
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
4318
+ },
4319
+ }, t('panel.cleanupWorktree'))
4320
+ : null,
4321
+ ),
4322
+ (openTask.worktree || openTask.rejectReason || openTask.stalled || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? h('button', {
4323
+ type: 'button',
4324
+ className: 'dkb-secondary',
4325
+ style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
4326
+ disabled: starting || model === '',
4327
+ onClick: async () => {
4328
+ setStarting(true)
4329
+ try {
4330
+ rememberLaunch(board, { provider, model, agentPreset, permission })
4331
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/resume', {
4332
+ method: 'POST',
4333
+ body: JSON.stringify({ provider, model, text: draftText, agentPreset, permission }),
4334
+ })
4335
+ setOpenTask(null)
4336
+ await load(board)
4337
+ if (!openSession(props.ctx, out && out.sessionId)) {
4338
+ setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
4339
+ return
4340
+ }
4341
+ if (props.onClose) props.onClose()
4342
+ } catch (e) {
4343
+ setError(t(e.key || 'error.unknown'))
4344
+ } finally {
4345
+ setStarting(false)
4346
+ }
4347
+ },
4348
+ }, t('action.resume')) : null,
4349
+ openTask.workflowStatus === 'running' ? h('button', {
4350
+ type: 'button',
4351
+ className: 'dkb-discard',
4352
+ style: { marginRight: '8px' },
4353
+ disabled: starting,
4354
+ onClick: async () => {
4355
+ setStarting(true)
4356
+ try {
4357
+ await api('/task/' + encodeURIComponent(openTask.id) + '/workflow-cancel', {
4358
+ method: 'POST',
4359
+ body: JSON.stringify({ reason: 'cancelled-by-user' }),
4360
+ })
4361
+ setOpenTask(null)
4362
+ await load(board)
4363
+ if (props.onClose) props.onClose()
4364
+ } catch (e) {
4365
+ setError(t(e.key || 'error.unknown'))
4366
+ } finally {
4367
+ setStarting(false)
4368
+ }
4369
+ },
4370
+ }, t('action.cancelWorkflow')) : null,
4371
+ openTask.workflowStatus !== 'running' ? h('button', {
4372
+ type: 'button',
4373
+ className: 'dkb-secondary',
4374
+ style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
4375
+ title: t('panel.startWorkflowHint'),
4376
+ disabled: starting || model === '',
4377
+ onClick: async () => {
4378
+ setStarting(true)
4379
+ try {
4380
+ rememberLaunch(board, { provider, model, agentPreset, permission })
4381
+ await api('/task/' + encodeURIComponent(openTask.id) + '/workflow-start', {
4382
+ method: 'POST',
4383
+ body: JSON.stringify({ provider, model, text: draftText, agentPreset, permission }),
4384
+ })
4385
+ setOpenTask(null)
4386
+ await load(board)
4387
+ if (props.onClose) props.onClose()
4388
+ } catch (e) {
4389
+ setError(t(e.key || 'error.unknown'))
4390
+ } finally {
4391
+ setStarting(false)
4392
+ }
4393
+ },
4394
+ }, t('panel.startWorkflow')) : null,
4395
+ h('button', {
4396
+ type: 'button', className: 'dkb-save', disabled: starting || model === '',
4397
+ onClick: async () => {
4398
+ setStarting(true)
4399
+ try {
4400
+ rememberLaunch(board, { provider, model, agentPreset, permission })
4401
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/start', {
4402
+ method: 'POST',
4403
+ body: JSON.stringify({ provider, model, text: draftText, agentPreset, permission }),
4404
+ })
4405
+ setOpenTask(null)
4406
+ await load(board)
4407
+ if (!openSession(props.ctx, out && out.sessionId)) {
4408
+ setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
4409
+ return
4410
+ }
4411
+ if (props.onClose) props.onClose()
4412
+ } catch (e) {
4413
+ setError(t(e.key || 'error.unknown'))
4414
+ } finally {
4415
+ setStarting(false)
4416
+ }
4417
+ },
4418
+ }, starting ? t('panel.starting')
4419
+ : openTask.sessionId ? t('panel.continue') : t('panel.start')),
4420
+ ),
4421
+ ),
4422
+ ))
4423
+ : null
4424
+
4425
+ const moveDialogEl = dialog && dialog.mode === 'move'
4426
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
4427
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4428
+ h('span', { className: 'dkb-title' },
4429
+ t('move.title', { column: t('column.' + dialog.column) })),
4430
+ h('p', { className: 'dkb-note' },
4431
+ cardRef(dialog.task) ? cardRef(dialog.task) + ' · ' + dialog.task.title : dialog.task.title),
4432
+ h('p', { className: 'dkb-hint' }, t('move.' + dialog.column)),
4433
+ h('div', { className: 'dkb-row' },
4434
+ h('button', {
4435
+ type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
4436
+ }, t('dialog.cancel')),
4437
+ h('button', {
4438
+ type: 'button', className: 'dkb-save',
4439
+ onClick: () => {
4440
+ const d = dialog
4441
+ setDialog(null)
4442
+ commitMove(d.task, d.column, d.where)
4443
+ },
4444
+ }, t('move.confirm')),
4445
+ ),
4446
+ ))
4447
+ : null
4448
+
4449
+ const batchEl = dialog && dialog.mode === 'batch'
4450
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
4451
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4452
+ h('span', { className: 'dkb-title' },
4453
+ t('batch.title', { n: dialog.ids.length })),
4454
+ h('p', { className: 'dkb-hint' }, t('batch.hint')),
4455
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
4456
+ h('ul', { className: 'dkb-batchList' },
4457
+ dialog.ids.map((id, at) => {
4458
+ const task = state.tasks.find((x) => x.id === id)
4459
+ return h('li', { className: 'dkb-batchRow', key: id },
4460
+ h('span', { className: 'dkb-refNum' }, String(at + 1)),
4461
+ h('span', null, task ? task.title : id))
4462
+ })),
4239
4463
  h('div', { className: 'dkb-field' },
4240
4464
  h('span', { className: 'dkb-label' }, t('panel.provider')),
4241
4465
  h('select', {
4242
- className: 'dkb-input', value: provider, disabled: starting || providers.length === 0,
4466
+ className: 'dkb-input', value: provider,
4243
4467
  onChange: (e) => { setProvider(e.target.value); setModel('') },
4244
4468
  },
4245
- h('option', { value: '' },
4246
- providers.length === 0 ? t('panel.noProviders') : t('panel.pickProvider')),
4469
+ h('option', { value: '' }, t('panel.pickProvider')),
4247
4470
  providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
4248
4471
  ),
4249
4472
  ),
4250
4473
  h('div', { className: 'dkb-field' },
4251
4474
  h('span', { className: 'dkb-label' }, t('panel.model')),
4252
4475
  h('select', {
4253
- className: 'dkb-input', value: model,
4254
- disabled: starting || provider === '' || models.length === 0,
4476
+ className: 'dkb-input', value: model, disabled: provider === '',
4255
4477
  onChange: (e) => setModel(e.target.value),
4256
4478
  },
4257
- h('option', { value: '' },
4258
- provider === '' ? t('panel.pickProviderFirst')
4259
- : models.length === 0 ? t('panel.noModels') : t('panel.pickModel')),
4479
+ h('option', { value: '' }, t('panel.pickModel')),
4260
4480
  models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
4261
4481
  ),
4262
4482
  ),
4263
- h('div', { className: 'dkb-field' },
4264
- h('span', { className: 'dkb-label' }, t('panel.message')),
4265
- h('textarea', {
4266
- className: 'dkb-area', value: draftText, disabled: starting,
4267
- onChange: (e) => setDraftText(e.target.value),
4268
- }),
4269
- h('p', { className: 'dkb-hint' }, t('panel.messageHint')),
4270
- ),
4271
- h('div', { className: 'dkb-foot dkb-footSplit' },
4272
- h('div', { className: 'dkb-footLeft' },
4483
+ React.createElement(LaunchFields, {
4484
+ t, presets, agentPreset, permission,
4485
+ onPreset: setAgentPreset, onPermission: setPermission,
4486
+ }),
4487
+ h('div', { className: 'dkb-row' },
4273
4488
  h('button', {
4274
- type: 'button', className: 'dkb-danger',
4275
- onClick: async () => {
4276
- if (!window.confirm(t('panel.deleteConfirm', { title: openTask.title }))) return
4277
- try {
4278
- await api('/task/' + encodeURIComponent(openTask.id), { method: 'DELETE' })
4279
- setOpenTask(null)
4280
- await load(board)
4281
- } catch (e) {
4282
- setError(t(e.key || 'error.unknown'))
4283
- }
4284
- },
4285
- }, t('panel.delete')),
4286
- openTask.column === 'done'
4287
- ? h('button', {
4288
- type: 'button', className: 'dkb-discard',
4289
- onClick: async () => {
4290
- try {
4291
- await api('/task/' + encodeURIComponent(openTask.id) + '/archive', { method: 'POST', body: '{}' })
4292
- setOpenTask(null)
4293
- await load(board)
4294
- } catch (e) { setError(t(e.key || 'error.unknown')) }
4295
- },
4296
- }, t('panel.archive'))
4297
- : null,
4298
- openTask.column === 'cleanup' && openTask.worktree
4299
- ? h('button', {
4300
- type: 'button', className: 'dkb-discard',
4301
- onClick: async () => {
4302
- try {
4303
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/cleanup-worktree', {
4304
- method: 'POST', body: '{}',
4305
- })
4306
- if (out && out.error === 'worktree-dirty') {
4307
- alert(t('panel.worktreeDirty', { files: (out.files || []).join(', ') }))
4308
- } else {
4309
- setOpenTask(null)
4310
- await load(board)
4311
- }
4312
- } catch (e) { setError(t(e.key || 'error.unknown')) }
4313
- },
4314
- }, t('panel.cleanupWorktree'))
4315
- : null,
4316
- ),
4317
- (openTask.worktree || openTask.rejectReason || openTask.stalled || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? h('button', {
4318
- type: 'button',
4319
- className: 'dkb-secondary',
4320
- style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
4321
- disabled: starting || model === '',
4489
+ type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
4490
+ }, t('dialog.cancel')),
4491
+ h('button', {
4492
+ type: 'button', className: 'dkb-save', disabled: model === '',
4322
4493
  onClick: async () => {
4323
- setStarting(true)
4324
4494
  try {
4325
4495
  rememberLaunch(board, { provider, model, agentPreset, permission })
4326
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/resume', {
4496
+ const out = await api('/batch', {
4327
4497
  method: 'POST',
4328
- body: JSON.stringify({ provider, model, text: draftText, agentPreset, permission }),
4498
+ body: JSON.stringify({ ids: dialog.ids, provider, model, agentPreset, permission }),
4329
4499
  })
4330
- setOpenTask(null)
4500
+ setDialog(null)
4501
+ setPicked({})
4331
4502
  await load(board)
4332
4503
  if (!openSession(props.ctx, out && out.sessionId)) {
4333
4504
  setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
@@ -4335,535 +4506,369 @@ window.__ModuleLoader__.load({
4335
4506
  }
4336
4507
  if (props.onClose) props.onClose()
4337
4508
  } catch (e) {
4338
- setError(t(e.key || 'error.unknown'))
4339
- } finally {
4340
- setStarting(false)
4341
- }
4342
- },
4343
- }, t('action.resume')) : null,
4344
- openTask.workflowStatus === 'running' ? h('button', {
4345
- type: 'button',
4346
- className: 'dkb-discard',
4347
- style: { marginRight: '8px' },
4348
- disabled: starting,
4349
- onClick: async () => {
4350
- setStarting(true)
4351
- try {
4352
- await api('/task/' + encodeURIComponent(openTask.id) + '/workflow-cancel', {
4353
- method: 'POST',
4354
- body: JSON.stringify({ reason: 'cancelled-by-user' }),
4355
- })
4356
- setOpenTask(null)
4357
- await load(board)
4358
- if (props.onClose) props.onClose()
4359
- } catch (e) {
4360
- setError(t(e.key || 'error.unknown'))
4361
- } finally {
4362
- setStarting(false)
4363
- }
4364
- },
4365
- }, t('action.cancelWorkflow')) : null,
4366
- openTask.workflowStatus !== 'running' ? h('button', {
4367
- type: 'button',
4368
- className: 'dkb-secondary',
4369
- style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
4370
- title: t('panel.startWorkflowHint'),
4371
- disabled: starting || model === '',
4372
- onClick: async () => {
4373
- setStarting(true)
4374
- try {
4375
- rememberLaunch(board, { provider, model, agentPreset, permission })
4376
- await api('/task/' + encodeURIComponent(openTask.id) + '/workflow-start', {
4377
- method: 'POST',
4378
- body: JSON.stringify({ provider, model, text: draftText, agentPreset, permission }),
4379
- })
4380
- setOpenTask(null)
4381
- await load(board)
4382
- if (props.onClose) props.onClose()
4383
- } catch (e) {
4384
- setError(t(e.key || 'error.unknown'))
4385
- } finally {
4386
- setStarting(false)
4509
+ setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4387
4510
  }
4388
4511
  },
4389
- }, t('panel.startWorkflow')) : null,
4512
+ }, t('batch.start')),
4513
+ ),
4514
+ ))
4515
+ : null
4516
+
4517
+ const queueEl = dialog && dialog.mode === 'queue'
4518
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
4519
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4520
+ h('span', { className: 'dkb-title' }, t('queue.title')),
4521
+ h('p', { className: 'dkb-hint' }, t('queue.hint')),
4522
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
4523
+ h('div', { className: 'dkb-field' },
4524
+ h('span', { className: 'dkb-label' }, t('queue.session')),
4525
+ h('select', {
4526
+ className: 'dkb-input', value: dialog.sessionId,
4527
+ onChange: (e) => setDialog(Object.assign({}, dialog, { sessionId: e.target.value })),
4528
+ }, dialog.sessions.map((one) => h('option', {
4529
+ value: one.sessionId, key: one.sessionId,
4530
+ }, t('queue.option', {
4531
+ n: one.tasks.length,
4532
+ title: (one.tasks[0] && one.tasks[0].title) || one.sessionId,
4533
+ })))),
4534
+ ),
4535
+ h('div', { className: 'dkb-row' },
4536
+ h('button', {
4537
+ type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
4538
+ }, t('dialog.cancel')),
4390
4539
  h('button', {
4391
- type: 'button', className: 'dkb-save', disabled: starting || model === '',
4540
+ type: 'button', className: 'dkb-save',
4392
4541
  onClick: async () => {
4393
- setStarting(true)
4394
4542
  try {
4395
- rememberLaunch(board, { provider, model, agentPreset, permission })
4396
- const out = await api('/task/' + encodeURIComponent(openTask.id) + '/start', {
4543
+ await api('/task/' + encodeURIComponent(dialog.task.id) + '/queue', {
4397
4544
  method: 'POST',
4398
- body: JSON.stringify({ provider, model, text: draftText, agentPreset, permission }),
4545
+ body: JSON.stringify({ sessionId: dialog.sessionId }),
4399
4546
  })
4400
- setOpenTask(null)
4547
+ setDialog(null)
4401
4548
  await load(board)
4402
- if (!openSession(props.ctx, out && out.sessionId)) {
4403
- setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
4404
- return
4405
- }
4406
- if (props.onClose) props.onClose()
4407
4549
  } catch (e) {
4408
- setError(t(e.key || 'error.unknown'))
4409
- } finally {
4410
- setStarting(false)
4550
+ setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4411
4551
  }
4412
4552
  },
4413
- }, starting ? t('panel.starting')
4414
- : openTask.sessionId ? t('panel.continue') : t('panel.start')),
4553
+ }, t('queue.add')),
4415
4554
  ),
4416
- ),
4417
4555
  ))
4418
4556
  : null
4419
4557
 
4420
- const moveDialogEl = dialog && dialog.mode === 'move'
4421
- ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
4422
- h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4423
- h('span', { className: 'dkb-title' },
4424
- t('move.title', { column: t('column.' + dialog.column) })),
4425
- h('p', { className: 'dkb-note' },
4426
- cardRef(dialog.task) ? cardRef(dialog.task) + ' · ' + dialog.task.title : dialog.task.title),
4427
- h('p', { className: 'dkb-hint' }, t('move.' + dialog.column)),
4428
- h('div', { className: 'dkb-row' },
4429
- h('button', {
4430
- type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
4431
- }, t('dialog.cancel')),
4432
- h('button', {
4433
- type: 'button', className: 'dkb-save',
4434
- onClick: () => {
4435
- const d = dialog
4436
- setDialog(null)
4437
- commitMove(d.task, d.column, d.where)
4438
- },
4439
- }, t('move.confirm')),
4440
- ),
4441
- ))
4442
- : null
4443
-
4444
- const batchEl = dialog && dialog.mode === 'batch'
4558
+ const dialogEl = dialog && dialog.mode !== 'move' && dialog.mode !== 'batch' && dialog.mode !== 'queue'
4445
4559
  ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
4446
- h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4447
- h('span', { className: 'dkb-title' },
4448
- t('batch.title', { n: dialog.ids.length })),
4449
- h('p', { className: 'dkb-hint' }, t('batch.hint')),
4450
- dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
4451
- h('ul', { className: 'dkb-batchList' },
4452
- dialog.ids.map((id, at) => {
4453
- const task = state.tasks.find((x) => x.id === id)
4454
- return h('li', { className: 'dkb-batchRow', key: id },
4455
- h('span', { className: 'dkb-refNum' }, String(at + 1)),
4456
- h('span', null, task ? task.title : id))
4457
- })),
4458
- h('div', { className: 'dkb-field' },
4459
- h('span', { className: 'dkb-label' }, t('panel.provider')),
4460
- h('select', {
4461
- className: 'dkb-input', value: provider,
4462
- onChange: (e) => { setProvider(e.target.value); setModel('') },
4560
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4561
+ h('span', { className: 'dkb-title' }, t('dialog.title')),
4562
+ h('div', { className: 'dkb-row' },
4563
+ h('button', {
4564
+ type: 'button', className: dialog.mode === 'own' ? 'dkb-save' : 'dkb-discard',
4565
+ onClick: () => setDialog(Object.assign({}, dialog, { mode: 'own', error: '' })),
4566
+ }, t('dialog.own')),
4567
+ state.kind === 'simple' ? null : h('button', {
4568
+ type: 'button', className: dialog.mode === 'gitea' ? 'dkb-save' : 'dkb-discard',
4569
+ onClick: async () => {
4570
+ setDialog(Object.assign({}, dialog, { mode: 'gitea', error: '' }))
4571
+ if (dialog.repos !== null) return
4572
+ try {
4573
+ const out = await api('/gitea/repos?q=')
4574
+ setDialog((d) => Object.assign({}, d, { mode: 'gitea', repos: out.repos || [], error: '' }))
4575
+ } catch (e) {
4576
+ setDialog((d) => Object.assign({}, d, { mode: 'gitea', repos: [], error: t(e.key || 'error.unknown') }))
4577
+ }
4463
4578
  },
4464
- h('option', { value: '' }, t('panel.pickProvider')),
4465
- providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
4579
+ }, t('dialog.fromGitea')),
4580
+ ),
4581
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
4582
+ dialog.mode === 'own'
4583
+ ? h('div', null,
4584
+ h('div', { className: 'dkb-field' },
4585
+ h('span', { className: 'dkb-label' }, t('dialog.kind')),
4586
+ h('div', { className: 'dkb-row' },
4587
+ h('button', {
4588
+ type: 'button', className: dialog.project ? 'dkb-save' : 'dkb-discard',
4589
+ onClick: () => setDialog(Object.assign({}, dialog, { project: true, error: '' })),
4590
+ }, t('dialog.kindProject')),
4591
+ h('button', {
4592
+ type: 'button', className: dialog.project ? 'dkb-discard' : 'dkb-save',
4593
+ onClick: () => setDialog(Object.assign({}, dialog, { project: false, error: '' })),
4594
+ }, t('dialog.kindPlain')),
4595
+ ),
4596
+ h('p', { className: 'dkb-hint' },
4597
+ t(dialog.project ? 'dialog.kindProjectHint' : 'dialog.kindPlainHint')),
4466
4598
  ),
4467
- ),
4468
- h('div', { className: 'dkb-field' },
4469
- h('span', { className: 'dkb-label' }, t('panel.model')),
4470
- h('select', {
4471
- className: 'dkb-input', value: model, disabled: provider === '',
4472
- onChange: (e) => setModel(e.target.value),
4473
- },
4474
- h('option', { value: '' }, t('panel.pickModel')),
4475
- models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
4599
+ dialog.project
4600
+ ? h('div', { className: 'dkb-field' },
4601
+ h('span', { className: 'dkb-label' }, t('dialog.repo')),
4602
+ h('select', {
4603
+ className: 'dkb-input', value: dialog.repo || '',
4604
+ onChange: (e) => setDialog(Object.assign({}, dialog, { repo: e.target.value })),
4605
+ },
4606
+ h('option', { value: '' }, t('dialog.repoNew')),
4607
+ (state.facets || [])
4608
+ .filter((f) => f.ns === REPO_FACET)
4609
+ .flatMap((f) => f.values)
4610
+ .map((v) => h('option', { value: v.value, key: v.value }, v.value)),
4611
+ ),
4612
+ (dialog.repo || '') === ''
4613
+ ? h('div', null,
4614
+ h('input', {
4615
+ className: 'dkb-input', value: dialog.newRepo || '',
4616
+ placeholder: t('dialog.repoNamePlaceholder'),
4617
+ onChange: (e) => setDialog(Object.assign({}, dialog, { newRepo: e.target.value })),
4618
+ }),
4619
+ h('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
4620
+ )
4621
+ : null,
4622
+ )
4623
+ : null,
4624
+ h('div', { className: 'dkb-field' },
4625
+ h('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
4626
+ h('input', {
4627
+ className: 'dkb-input', value: dialog.title, autoFocus: true,
4628
+ onChange: (e) => setDialog(Object.assign({}, dialog, { title: e.target.value })),
4629
+ }),
4476
4630
  ),
4477
- ),
4478
- React.createElement(LaunchFields, {
4479
- t, presets, agentPreset, permission,
4480
- onPreset: setAgentPreset, onPermission: setPermission,
4481
- }),
4482
- h('div', { className: 'dkb-row' },
4483
- h('button', {
4484
- type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
4485
- }, t('dialog.cancel')),
4486
- h('button', {
4487
- type: 'button', className: 'dkb-save', disabled: model === '',
4488
- onClick: async () => {
4489
- try {
4490
- rememberLaunch(board, { provider, model, agentPreset, permission })
4491
- const out = await api('/batch', {
4492
- method: 'POST',
4493
- body: JSON.stringify({ ids: dialog.ids, provider, model, agentPreset, permission }),
4494
- })
4495
- setDialog(null)
4496
- setPicked({})
4497
- await load(board)
4498
- if (!openSession(props.ctx, out && out.sessionId)) {
4499
- setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
4500
- return
4631
+ h('div', { className: 'dkb-field' },
4632
+ h('span', { className: 'dkb-label' }, t('dialog.taskBody')),
4633
+ h('textarea', {
4634
+ className: 'dkb-area', value: dialog.body || '',
4635
+ onChange: (e) => setDialog(Object.assign({}, dialog, { body: e.target.value })),
4636
+ }),
4637
+ h('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
4638
+ ),
4639
+ h('div', { className: 'dkb-field' },
4640
+ h('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
4641
+ h('input', {
4642
+ className: 'dkb-input', value: dialog.labels || '',
4643
+ placeholder: t('dialog.labelsHint'),
4644
+ onChange: (e) => setDialog(Object.assign({}, dialog, { labels: e.target.value })),
4645
+ }),
4646
+ ),
4647
+ h('div', { className: 'dkb-field' },
4648
+ h('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
4649
+ h('select', {
4650
+ className: 'dkb-input', value: dialog.column || 'backlog',
4651
+ onChange: (e) => setDialog(Object.assign({}, dialog, { column: e.target.value })),
4652
+ }, (state.columns || []).map((c) => h('option', { value: c.id, key: c.id }, t('column.' + c.id)))),
4653
+ ),
4654
+ h('div', { className: 'dkb-foot' },
4655
+ h('button', { type: 'button', className: 'dkb-discard', onClick: () => setDialog(null) }, t('dialog.cancel')),
4656
+ h('button', {
4657
+ type: 'button', className: 'dkb-save', disabled: String(dialog.title || '').trim() === '',
4658
+ onClick: async () => {
4659
+ try {
4660
+ if (dialog.project) {
4661
+ await api('/project-task', {
4662
+ method: 'POST',
4663
+ body: JSON.stringify({
4664
+ repo: dialog.repo || '',
4665
+ newRepo: dialog.newRepo || '',
4666
+ title: dialog.title,
4667
+ body: dialog.body || '',
4668
+ board: 'main',
4669
+ }),
4670
+ })
4671
+ } else {
4672
+ await api('/task', {
4673
+ method: 'POST',
4674
+ body: JSON.stringify({
4675
+ title: dialog.title,
4676
+ body: dialog.body || '',
4677
+ labels: String(dialog.labels || '').split(',').map((x) => x.trim()).filter(Boolean),
4678
+ column: dialog.column || 'backlog',
4679
+ board: 'simple',
4680
+ }),
4681
+ })
4682
+ }
4683
+ setDialog(null)
4684
+ await load(board)
4685
+ } catch (e) {
4686
+ setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4501
4687
  }
4502
- if (props.onClose) props.onClose()
4503
- } catch (e) {
4504
- setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4505
- }
4506
- },
4507
- }, t('batch.start')),
4508
- ),
4509
- ))
4510
- : null
4511
-
4512
- const queueEl = dialog && dialog.mode === 'queue'
4513
- ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
4514
- h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4515
- h('span', { className: 'dkb-title' }, t('queue.title')),
4516
- h('p', { className: 'dkb-hint' }, t('queue.hint')),
4517
- dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
4518
- h('div', { className: 'dkb-field' },
4519
- h('span', { className: 'dkb-label' }, t('queue.session')),
4520
- h('select', {
4521
- className: 'dkb-input', value: dialog.sessionId,
4522
- onChange: (e) => setDialog(Object.assign({}, dialog, { sessionId: e.target.value })),
4523
- }, dialog.sessions.map((one) => h('option', {
4524
- value: one.sessionId, key: one.sessionId,
4525
- }, t('queue.option', {
4526
- n: one.tasks.length,
4527
- title: (one.tasks[0] && one.tasks[0].title) || one.sessionId,
4528
- })))),
4529
- ),
4530
- h('div', { className: 'dkb-row' },
4531
- h('button', {
4532
- type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
4533
- }, t('dialog.cancel')),
4534
- h('button', {
4535
- type: 'button', className: 'dkb-save',
4536
- onClick: async () => {
4537
- try {
4538
- await api('/task/' + encodeURIComponent(dialog.task.id) + '/queue', {
4539
- method: 'POST',
4540
- body: JSON.stringify({ sessionId: dialog.sessionId }),
4541
- })
4542
- setDialog(null)
4543
- await load(board)
4544
- } catch (e) {
4545
- setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4546
- }
4547
- },
4548
- }, t('queue.add')),
4549
- ),
4550
- ))
4551
- : null
4552
-
4553
- const dialogEl = dialog && dialog.mode !== 'move' && dialog.mode !== 'batch' && dialog.mode !== 'queue'
4554
- ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
4555
- h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
4556
- h('span', { className: 'dkb-title' }, t('dialog.title')),
4557
- h('div', { className: 'dkb-row' },
4558
- h('button', {
4559
- type: 'button', className: dialog.mode === 'own' ? 'dkb-save' : 'dkb-discard',
4560
- onClick: () => setDialog(Object.assign({}, dialog, { mode: 'own', error: '' })),
4561
- }, t('dialog.own')),
4562
- state.kind === 'simple' ? null : h('button', {
4563
- type: 'button', className: dialog.mode === 'gitea' ? 'dkb-save' : 'dkb-discard',
4564
- onClick: async () => {
4565
- setDialog(Object.assign({}, dialog, { mode: 'gitea', error: '' }))
4566
- if (dialog.repos !== null) return
4567
- try {
4568
- const out = await api('/gitea/repos?q=')
4569
- setDialog((d) => Object.assign({}, d, { mode: 'gitea', repos: out.repos || [], error: '' }))
4570
- } catch (e) {
4571
- setDialog((d) => Object.assign({}, d, { mode: 'gitea', repos: [], error: t(e.key || 'error.unknown') }))
4572
- }
4688
+ },
4689
+ }, t('dialog.create')),
4690
+ ),
4691
+ )
4692
+ : h('div', null,
4693
+ h('div', { className: 'dkb-row' },
4694
+ h('select', {
4695
+ className: 'dkb-input', value: dialog.full,
4696
+ disabled: dialog.repos === null || dialog.repos.length === 0,
4697
+ onChange: async (e) => {
4698
+ const full = e.target.value
4699
+ setDialog((d) => Object.assign({}, d, { full, issues: null, error: '' }))
4700
+ const pair = splitFullName(full)
4701
+ if (pair === undefined) return
4702
+ try {
4703
+ const out = await api('/gitea/issues?owner=' + encodeURIComponent(pair.owner) +
4704
+ '&repo=' + encodeURIComponent(pair.repo))
4705
+ setDialog((d) => Object.assign({}, d, { issues: out.issues || [], error: '' }))
4706
+ } catch (err) {
4707
+ setDialog((d) => Object.assign({}, d, { issues: [], error: t(err.key || 'error.unknown') }))
4708
+ }
4709
+ },
4573
4710
  },
4574
- }, t('dialog.fromGitea')),
4575
- ),
4576
- dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
4577
- dialog.mode === 'own'
4578
- ? h('div', null,
4579
- h('div', { className: 'dkb-field' },
4580
- h('span', { className: 'dkb-label' }, t('dialog.kind')),
4581
- h('div', { className: 'dkb-row' },
4582
- h('button', {
4583
- type: 'button', className: dialog.project ? 'dkb-save' : 'dkb-discard',
4584
- onClick: () => setDialog(Object.assign({}, dialog, { project: true, error: '' })),
4585
- }, t('dialog.kindProject')),
4586
- h('button', {
4587
- type: 'button', className: dialog.project ? 'dkb-discard' : 'dkb-save',
4588
- onClick: () => setDialog(Object.assign({}, dialog, { project: false, error: '' })),
4589
- }, t('dialog.kindPlain')),
4590
- ),
4591
- h('p', { className: 'dkb-hint' },
4592
- t(dialog.project ? 'dialog.kindProjectHint' : 'dialog.kindPlainHint')),
4593
- ),
4594
- dialog.project
4595
- ? h('div', { className: 'dkb-field' },
4596
- h('span', { className: 'dkb-label' }, t('dialog.repo')),
4597
- h('select', {
4598
- className: 'dkb-input', value: dialog.repo || '',
4599
- onChange: (e) => setDialog(Object.assign({}, dialog, { repo: e.target.value })),
4600
- },
4601
- h('option', { value: '' }, t('dialog.repoNew')),
4602
- (state.facets || [])
4603
- .filter((f) => f.ns === REPO_FACET)
4604
- .flatMap((f) => f.values)
4605
- .map((v) => h('option', { value: v.value, key: v.value }, v.value)),
4606
- ),
4607
- (dialog.repo || '') === ''
4608
- ? h('div', null,
4609
- h('input', {
4610
- className: 'dkb-input', value: dialog.newRepo || '',
4611
- placeholder: t('dialog.repoNamePlaceholder'),
4612
- onChange: (e) => setDialog(Object.assign({}, dialog, { newRepo: e.target.value })),
4613
- }),
4614
- h('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
4615
- )
4616
- : null,
4617
- )
4618
- : null,
4619
- h('div', { className: 'dkb-field' },
4620
- h('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
4621
- h('input', {
4622
- className: 'dkb-input', value: dialog.title, autoFocus: true,
4623
- onChange: (e) => setDialog(Object.assign({}, dialog, { title: e.target.value })),
4624
- }),
4625
- ),
4626
- h('div', { className: 'dkb-field' },
4627
- h('span', { className: 'dkb-label' }, t('dialog.taskBody')),
4628
- h('textarea', {
4629
- className: 'dkb-area', value: dialog.body || '',
4630
- onChange: (e) => setDialog(Object.assign({}, dialog, { body: e.target.value })),
4711
+ h('option', { value: '' },
4712
+ dialog.repos === null ? t('dialog.loadingRepos')
4713
+ : dialog.repos.length === 0 ? t('dialog.noRepos') : t('dialog.pickRepo')),
4714
+ (dialog.repos || []).map((r) => {
4715
+ const full = r.fullName || (r.owner + '/' + r.repo)
4716
+ return h('option', { value: full, key: full }, repoOption(r, t))
4631
4717
  }),
4632
- h('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
4633
- ),
4634
- h('div', { className: 'dkb-field' },
4635
- h('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
4636
- h('input', {
4637
- className: 'dkb-input', value: dialog.labels || '',
4638
- placeholder: t('dialog.labelsHint'),
4639
- onChange: (e) => setDialog(Object.assign({}, dialog, { labels: e.target.value })),
4640
- }),
4641
- ),
4642
- h('div', { className: 'dkb-field' },
4643
- h('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
4644
- h('select', {
4645
- className: 'dkb-input', value: dialog.column || 'backlog',
4646
- onChange: (e) => setDialog(Object.assign({}, dialog, { column: e.target.value })),
4647
- }, (state.columns || []).map((c) => h('option', { value: c.id, key: c.id }, t('column.' + c.id)))),
4648
4718
  ),
4649
- h('div', { className: 'dkb-foot' },
4650
- h('button', { type: 'button', className: 'dkb-discard', onClick: () => setDialog(null) }, t('dialog.cancel')),
4651
- h('button', {
4652
- type: 'button', className: 'dkb-save', disabled: String(dialog.title || '').trim() === '',
4653
- onClick: async () => {
4654
- try {
4655
- if (dialog.project) {
4656
- await api('/project-task', {
4657
- method: 'POST',
4658
- body: JSON.stringify({
4659
- repo: dialog.repo || '',
4660
- newRepo: dialog.newRepo || '',
4661
- title: dialog.title,
4662
- body: dialog.body || '',
4663
- board: 'main',
4664
- }),
4665
- })
4666
- } else {
4667
- await api('/task', {
4719
+ ),
4720
+ dialog.issues === null ? null
4721
+ : dialog.issues.length === 0
4722
+ ? h('p', { className: 'dkb-note' }, t('dialog.noIssues'))
4723
+ : h('div', { className: 'dkb-list' }, dialog.issues.map((issue) =>
4724
+ h('button', {
4725
+ type: 'button',
4726
+ className: 'dkb-issue' + (issue.imported ? ' dkb-issueUsed' : ''),
4727
+ key: issue.number,
4728
+ onClick: async () => {
4729
+ const pair = splitFullName(dialog.full)
4730
+ if (pair === undefined) return
4731
+ try {
4732
+ await api('/import', {
4668
4733
  method: 'POST',
4669
- body: JSON.stringify({
4670
- title: dialog.title,
4671
- body: dialog.body || '',
4672
- labels: String(dialog.labels || '').split(',').map((x) => x.trim()).filter(Boolean),
4673
- column: dialog.column || 'backlog',
4674
- board: 'simple',
4675
- }),
4734
+ body: JSON.stringify({ owner: pair.owner, repo: pair.repo, issueNumber: issue.number, board }),
4676
4735
  })
4736
+ setDialog(null)
4737
+ await load(board)
4738
+ } catch (e) {
4739
+ setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4677
4740
  }
4678
- setDialog(null)
4679
- await load(board)
4680
- } catch (e) {
4681
- setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4682
- }
4683
- },
4684
- }, t('dialog.create')),
4685
- ),
4686
- )
4687
- : h('div', null,
4688
- h('div', { className: 'dkb-row' },
4689
- h('select', {
4690
- className: 'dkb-input', value: dialog.full,
4691
- disabled: dialog.repos === null || dialog.repos.length === 0,
4692
- onChange: async (e) => {
4693
- const full = e.target.value
4694
- setDialog((d) => Object.assign({}, d, { full, issues: null, error: '' }))
4695
- const pair = splitFullName(full)
4696
- if (pair === undefined) return
4697
- try {
4698
- const out = await api('/gitea/issues?owner=' + encodeURIComponent(pair.owner) +
4699
- '&repo=' + encodeURIComponent(pair.repo))
4700
- setDialog((d) => Object.assign({}, d, { issues: out.issues || [], error: '' }))
4701
- } catch (err) {
4702
- setDialog((d) => Object.assign({}, d, { issues: [], error: t(err.key || 'error.unknown') }))
4703
- }
4704
- },
4705
- },
4706
- h('option', { value: '' },
4707
- dialog.repos === null ? t('dialog.loadingRepos')
4708
- : dialog.repos.length === 0 ? t('dialog.noRepos') : t('dialog.pickRepo')),
4709
- (dialog.repos || []).map((r) => {
4710
- const full = r.fullName || (r.owner + '/' + r.repo)
4711
- return h('option', { value: full, key: full }, repoOption(r, t))
4712
- }),
4713
- ),
4714
- ),
4715
- dialog.issues === null ? null
4716
- : dialog.issues.length === 0
4717
- ? h('p', { className: 'dkb-note' }, t('dialog.noIssues'))
4718
- : h('div', { className: 'dkb-list' }, dialog.issues.map((issue) =>
4719
- h('button', {
4720
- type: 'button',
4721
- className: 'dkb-issue' + (issue.imported ? ' dkb-issueUsed' : ''),
4722
- key: issue.number,
4723
- onClick: async () => {
4724
- const pair = splitFullName(dialog.full)
4725
- if (pair === undefined) return
4726
- try {
4727
- await api('/import', {
4728
- method: 'POST',
4729
- body: JSON.stringify({ owner: pair.owner, repo: pair.repo, issueNumber: issue.number, board }),
4730
- })
4731
- setDialog(null)
4732
- await load(board)
4733
- } catch (e) {
4734
- setDialog(Object.assign({}, dialog, { error: t(e.key || 'error.unknown') }))
4735
- }
4736
- },
4737
- }, `#${issue.number} ${issue.title}${issue.imported ? ' · ' + t('dialog.imported') : ''}`))),
4738
- ),
4739
- ))
4741
+ },
4742
+ }, `#${issue.number} ${issue.title}${issue.imported ? ' · ' + t('dialog.imported') : ''}`))),
4743
+ ),
4744
+ ))
4740
4745
  : null
4741
4746
 
4742
4747
  const archiveEl = archive
4743
4748
  ? h('div', { className: 'dkb-archive' },
4744
- h('p', { className: 'dkb-hint' }, t('archive.hint')),
4745
- archive.length === 0
4746
- ? h('p', { className: 'dkb-note' }, t('archive.empty'))
4747
- : h('ul', { className: 'dkb-archiveList' },
4748
- archive.map((task) => h('li', { className: 'dkb-archiveRow', key: task.id },
4749
- h('button', {
4750
- type: 'button', className: 'dkb-archiveTitle dkb-archiveOpen',
4751
- onClick: () => setOpenTask(task),
4749
+ h('p', { className: 'dkb-hint' }, t('archive.hint')),
4750
+ archive.length === 0
4751
+ ? h('p', { className: 'dkb-note' }, t('archive.empty'))
4752
+ : h('ul', { className: 'dkb-archiveList' },
4753
+ archive.map((task) => h('li', { className: 'dkb-archiveRow', key: task.id },
4754
+ h('button', {
4755
+ type: 'button', className: 'dkb-archiveTitle dkb-archiveOpen',
4756
+ onClick: () => setOpenTask(task),
4757
+ },
4758
+ [cardRef(task), task.title].filter(Boolean).join(' · ')),
4759
+ h('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
4760
+ t('archive.since', { ago: agoText(state.now, task.archivedAt, t) })),
4761
+ h('button', {
4762
+ type: 'button', className: 'dkb-discard',
4763
+ onClick: async () => {
4764
+ try {
4765
+ await api('/task/' + encodeURIComponent(task.id) + '/restore', { method: 'POST', body: '{}' })
4766
+ setArchive((cur) => (cur || []).filter((x) => x.id !== task.id))
4767
+ await load(board)
4768
+ } catch (e) { setError(t(e.key || 'error.unknown')) }
4752
4769
  },
4753
- [cardRef(task), task.title].filter(Boolean).join(' · ')),
4754
- h('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
4755
- t('archive.since', { ago: agoText(state.now, task.archivedAt, t) })),
4756
- h('button', {
4757
- type: 'button', className: 'dkb-discard',
4758
- onClick: async () => {
4759
- try {
4760
- await api('/task/' + encodeURIComponent(task.id) + '/restore', { method: 'POST', body: '{}' })
4761
- setArchive((cur) => (cur || []).filter((x) => x.id !== task.id))
4762
- await load(board)
4763
- } catch (e) { setError(t(e.key || 'error.unknown')) }
4764
- },
4765
- }, t('archive.restore')),
4766
- ))),
4770
+ }, t('archive.restore')),
4771
+ ))),
4767
4772
  )
4768
4773
  : null
4769
4774
 
4770
4775
  const metricsEl = metrics
4771
4776
  ? h('div', { className: 'dkb-archive' },
4772
- h('p', { className: 'dkb-hint' },
4773
- t('metrics.hint', { n: metrics.tasks, week: metrics.done.week, month: metrics.done.month })),
4774
- h('div', { className: 'dkb-group' }, t('metrics.columns')),
4775
- metrics.columns.length === 0
4776
- ? h('p', { className: 'dkb-note' }, t('metrics.empty'))
4777
- : h('ul', { className: 'dkb-archiveList' },
4778
- metrics.columns.map((row) => h('li', {
4779
- className: 'dkb-archiveRow', key: row.column,
4780
- },
4781
- h('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
4782
- h('span', { className: 'dkb-taskRef' },
4783
- t('metrics.median', { time: agoText(0, -row.median, t) })),
4784
- h('span', { className: 'dkb-taskRef' },
4785
- t('metrics.mean', { time: agoText(0, -row.mean, t) })),
4786
- h('span', { className: 'dkb-count' }, String(row.tasks)),
4787
- ))),
4788
- h('div', { className: 'dkb-group' },
4789
- t('metrics.stale', { days: metrics.staleDays })),
4790
- metrics.stale.length === 0
4791
- ? h('p', { className: 'dkb-note' }, t('metrics.noStale'))
4792
- : h('ul', { className: 'dkb-archiveList' },
4793
- metrics.stale.map((row) => h('li', {
4794
- className: 'dkb-archiveRow', key: row.id,
4795
- },
4796
- h('span', { className: 'dkb-archiveTitle' }, row.title),
4797
- h('span', { className: 'dkb-taskRef' }, columnTitle({ id: row.column }, t)),
4798
- h('span', { className: 'dkb-taskRef', title: exactAt(row.since) },
4799
- agoText(state.now, row.since, t)),
4800
- ))),
4777
+ h('p', { className: 'dkb-hint' },
4778
+ t('metrics.hint', { n: metrics.tasks, week: metrics.done.week, month: metrics.done.month })),
4779
+ h('div', { className: 'dkb-group' }, t('metrics.columns')),
4780
+ metrics.columns.length === 0
4781
+ ? h('p', { className: 'dkb-note' }, t('metrics.empty'))
4782
+ : h('ul', { className: 'dkb-archiveList' },
4783
+ metrics.columns.map((row) => h('li', {
4784
+ className: 'dkb-archiveRow', key: row.column,
4785
+ },
4786
+ h('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
4787
+ h('span', { className: 'dkb-taskRef' },
4788
+ t('metrics.median', { time: agoText(0, -row.median, t) })),
4789
+ h('span', { className: 'dkb-taskRef' },
4790
+ t('metrics.mean', { time: agoText(0, -row.mean, t) })),
4791
+ h('span', { className: 'dkb-count' }, String(row.tasks)),
4792
+ ))),
4793
+ h('div', { className: 'dkb-group' },
4794
+ t('metrics.stale', { days: metrics.staleDays })),
4795
+ metrics.stale.length === 0
4796
+ ? h('p', { className: 'dkb-note' }, t('metrics.noStale'))
4797
+ : h('ul', { className: 'dkb-archiveList' },
4798
+ metrics.stale.map((row) => h('li', {
4799
+ className: 'dkb-archiveRow', key: row.id,
4800
+ },
4801
+ h('span', { className: 'dkb-archiveTitle' }, row.title),
4802
+ h('span', { className: 'dkb-taskRef' }, columnTitle({ id: row.column }, t)),
4803
+ h('span', { className: 'dkb-taskRef', title: exactAt(row.since) },
4804
+ agoText(state.now, row.since, t)),
4805
+ ))),
4801
4806
  )
4802
4807
  : null
4803
4808
 
4804
4809
  const nudgeDialogEl = nudgeTask
4805
4810
  ? h('div', {
4806
- className: 'dkb-modal',
4807
- onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
4811
+ className: 'dkb-modal',
4812
+ onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
4813
+ },
4814
+ h('div', {
4815
+ className: 'dkb-dialog',
4816
+ style: { maxWidth: '440px', width: '90%', padding: '16px' },
4817
+ onClick: (e) => e.stopPropagation(),
4808
4818
  },
4809
- h('div', {
4810
- className: 'dkb-dialog',
4811
- style: { maxWidth: '440px', width: '90%', padding: '16px' },
4812
- onClick: (e) => e.stopPropagation(),
4813
- },
4814
- h('div', { className: 'dkb-dialogHead', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' } },
4815
- h('h3', { style: { margin: 0, fontSize: '15px' } }, t('nudge.title') + ': ' + (nudgeTask.title || '')),
4816
- h('button', {
4817
- type: 'button',
4818
- className: 'dkb-discard',
4819
- style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '16px', padding: '0 4px' },
4820
- onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
4821
- }, '✕'),
4822
- ),
4823
- nudgeSuccess
4824
- ? h('div', {
4825
- style: {
4826
- padding: '12px',
4827
- borderRadius: '6px',
4828
- background: 'var(--dsw-alias-state-success-secondary, #dafbe1)',
4829
- color: 'var(--dsw-alias-state-success-primary, #1a7f37)',
4830
- marginBottom: '12px',
4831
- textAlign: 'center',
4832
- fontWeight: '600',
4819
+ h('div', { className: 'dkb-dialogHead', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' } },
4820
+ h('h3', { style: { margin: 0, fontSize: '15px' } }, t('nudge.title') + ': ' + (nudgeTask.title || '')),
4821
+ h('button', {
4822
+ type: 'button',
4823
+ className: 'dkb-discard',
4824
+ style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '16px', padding: '0 4px' },
4825
+ onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
4826
+ }, '✕'),
4827
+ ),
4828
+ nudgeSuccess
4829
+ ? h('div', {
4830
+ style: {
4831
+ padding: '12px',
4832
+ borderRadius: '6px',
4833
+ background: 'var(--dsw-alias-state-success-secondary, #dafbe1)',
4834
+ color: 'var(--dsw-alias-state-success-primary, #1a7f37)',
4835
+ marginBottom: '12px',
4836
+ textAlign: 'center',
4837
+ fontWeight: '600',
4838
+ },
4839
+ }, '✓ ' + t('nudge.success'))
4840
+ : h('div', null,
4841
+ h('textarea', {
4842
+ className: 'dkb-area',
4843
+ style: { width: '100%', minHeight: '90px', marginBottom: '12px', boxSizing: 'border-box' },
4844
+ placeholder: t('nudge.placeholder'),
4845
+ value: nudgePrompt,
4846
+ autoFocus: true,
4847
+ onChange: (e) => setNudgePrompt(e.target.value),
4848
+ onKeyDown: (e) => {
4849
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
4850
+ e.preventDefault();
4851
+ handleNudgeSubmit();
4852
+ }
4833
4853
  },
4834
- }, '✓ ' + t('nudge.success'))
4835
- : h('div', null,
4836
- h('textarea', {
4837
- className: 'dkb-area',
4838
- style: { width: '100%', minHeight: '90px', marginBottom: '12px', boxSizing: 'border-box' },
4839
- placeholder: t('nudge.placeholder'),
4840
- value: nudgePrompt,
4841
- autoFocus: true,
4842
- onChange: (e) => setNudgePrompt(e.target.value),
4843
- onKeyDown: (e) => {
4844
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
4845
- e.preventDefault();
4846
- handleNudgeSubmit();
4847
- }
4848
- },
4849
- }),
4850
- h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px' } },
4851
- h('button', {
4852
- type: 'button',
4853
- className: 'dkb-discard',
4854
- disabled: nudgeSending,
4855
- onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
4856
- }, t('panel.close')),
4857
- h('button', {
4858
- type: 'button',
4859
- className: 'dkb-save',
4860
- disabled: !nudgePrompt.trim() || nudgeSending,
4861
- onClick: handleNudgeSubmit,
4862
- }, nudgeSending ? t('nudge.sending') : t('nudge.send')),
4863
- ),
4854
+ }),
4855
+ h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px' } },
4856
+ h('button', {
4857
+ type: 'button',
4858
+ className: 'dkb-discard',
4859
+ disabled: nudgeSending,
4860
+ onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
4861
+ }, t('panel.close')),
4862
+ h('button', {
4863
+ type: 'button',
4864
+ className: 'dkb-save',
4865
+ disabled: !nudgePrompt.trim() || nudgeSending,
4866
+ onClick: handleNudgeSubmit,
4867
+ }, nudgeSending ? t('nudge.sending') : t('nudge.send')),
4864
4868
  ),
4865
- ),
4866
- )
4869
+ ),
4870
+ ),
4871
+ )
4867
4872
  : null;
4868
4873
 
4869
4874
  return h('div', { className: 'dkb-screen' },
@@ -4875,7 +4880,20 @@ window.__ModuleLoader__.load({
4875
4880
  }
4876
4881
 
4877
4882
  function TaskChip(props) {
4883
+ const ctx = props.ctx
4878
4884
  const t = props.t || fallbackT
4885
+ const scope = React.useMemo(() => {
4886
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.configForms
4887
+ return s ? s.get(NS) : undefined
4888
+ }, [ctx])
4889
+ const snapshot = React.useSyncExternalStore(
4890
+ React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
4891
+ React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
4892
+ React.useCallback(() => ({ status: 'loading' }), []),
4893
+ )
4894
+ const stored = (snapshot && snapshot.value) || {}
4895
+ const visible = stored.showHeaderChip !== false && stored.showHeaderChip !== 'false'
4896
+
4879
4897
  const [task, setTask] = React.useState(null)
4880
4898
  const [columns, setColumns] = React.useState(COLUMN_ORDER)
4881
4899
  const [note, setNote] = React.useState('')
@@ -4888,71 +4906,71 @@ window.__ModuleLoader__.load({
4888
4906
  let alive = true
4889
4907
  if (!sessionId) { setTask(null); return () => {} }
4890
4908
  api('/session/' + encodeURIComponent(sessionId) + '/task')
4891
- .then((out) => {
4892
- if (!alive) return
4893
- setTask((out && out.task) || null)
4894
- setColumns((out && out.columns) || COLUMN_ORDER)
4895
- })
4896
- .catch(() => { if (alive) setTask(null) })
4909
+ .then((out) => {
4910
+ if (!alive) return
4911
+ setTask((out && out.task) || null)
4912
+ setColumns((out && out.columns) || COLUMN_ORDER)
4913
+ })
4914
+ .catch(() => { if (alive) setTask(null) })
4897
4915
  return () => { alive = false }
4898
4916
  }, [sessionId])
4899
4917
 
4900
4918
  const move = React.useCallback(async (column) => {
4901
4919
  if (!task) return
4902
4920
  try {
4903
- const out = await api('/task/' + encodeURIComponent(task.id) + '/move', {
4904
- method: 'POST', body: JSON.stringify({ column }),
4905
- })
4906
- setTask(out.task)
4907
- setOpen(false)
4921
+ const out = await api('/task/' + encodeURIComponent(task.id) + '/move', {
4922
+ method: 'POST', body: JSON.stringify({ column }),
4923
+ })
4924
+ setTask(out.task)
4925
+ setOpen(false)
4908
4926
  } catch { }
4909
4927
  }, [task])
4910
4928
 
4911
- if (!task) return null
4929
+ if (!visible || !task) return null
4912
4930
 
4913
4931
  const label = [cardRef(task) || task.title, t('column.' + task.column)].filter(Boolean).join(' · ')
4914
4932
  ensureCss()
4915
4933
  return h('span', { className: 'dkb-chipWrap' },
4916
4934
  h('button', {
4917
- type: 'button', className: 'dkb-chip', title: task.title,
4918
- onClick: () => setOpen((v) => !v),
4935
+ type: 'button', className: 'dkb-chip', title: task.title,
4936
+ onClick: () => setOpen((v) => !v),
4919
4937
  }, '▦ ' + label),
4920
4938
  open ? h('div', { className: 'dkb-chipPanel' },
4921
- h('span', { className: 'dkb-hint' }, t('chip.moveTo')),
4922
- columns.map((id) => h('button', {
4923
- type: 'button', key: id,
4924
- className: id === task.column ? 'dkb-save' : 'dkb-discard',
4925
- onClick: () => move(id),
4926
- }, t('column.' + id))),
4927
- props.toggle
4928
- ? h('button', {
4929
- type: 'button', className: 'dkb-discard',
4930
- onClick: () => { setOpen(false); props.toggle.show(task.id, task.board) },
4931
- }, t('chip.openCard'))
4932
- : null,
4933
- h('span', { className: 'dkb-hint' }, t('chip.note')),
4934
- h('textarea', {
4935
- className: 'dkb-area', value: note, disabled: saving,
4936
- onChange: (e) => setNote(e.target.value),
4937
- }),
4938
- h('span', { className: 'dkb-hint' }, t('chip.noteHint')),
4939
- h('button', {
4940
- type: 'button', className: 'dkb-save',
4941
- disabled: saving || note.trim() === '',
4942
- onClick: async () => {
4943
- setSaving(true)
4944
- try {
4945
- const out = await api('/task/' + encodeURIComponent(task.id) + '/note', {
4946
- method: 'POST', body: JSON.stringify({ text: note }),
4947
- })
4948
- setTask(out.task)
4949
- setNote('')
4950
- setOpen(false)
4951
- } catch { } finally {
4952
- setSaving(false)
4953
- }
4954
- },
4955
- }, t('chip.noteSave')),
4939
+ h('span', { className: 'dkb-hint' }, t('chip.moveTo')),
4940
+ columns.map((id) => h('button', {
4941
+ type: 'button', key: id,
4942
+ className: id === task.column ? 'dkb-save' : 'dkb-discard',
4943
+ onClick: () => move(id),
4944
+ }, t('column.' + id))),
4945
+ props.toggle
4946
+ ? h('button', {
4947
+ type: 'button', className: 'dkb-discard',
4948
+ onClick: () => { setOpen(false); props.toggle.show(task.id, task.board) },
4949
+ }, t('chip.openCard'))
4950
+ : null,
4951
+ h('span', { className: 'dkb-hint' }, t('chip.note')),
4952
+ h('textarea', {
4953
+ className: 'dkb-area', value: note, disabled: saving,
4954
+ onChange: (e) => setNote(e.target.value),
4955
+ }),
4956
+ h('span', { className: 'dkb-hint' }, t('chip.noteHint')),
4957
+ h('button', {
4958
+ type: 'button', className: 'dkb-save',
4959
+ disabled: saving || note.trim() === '',
4960
+ onClick: async () => {
4961
+ setSaving(true)
4962
+ try {
4963
+ const out = await api('/task/' + encodeURIComponent(task.id) + '/note', {
4964
+ method: 'POST', body: JSON.stringify({ text: note }),
4965
+ })
4966
+ setTask(out.task)
4967
+ setNote('')
4968
+ setOpen(false)
4969
+ } catch { } finally {
4970
+ setSaving(false)
4971
+ }
4972
+ },
4973
+ }, t('chip.noteSave')),
4956
4974
  ) : null,
4957
4975
  )
4958
4976
  }
@@ -4976,19 +4994,19 @@ window.__ModuleLoader__.load({
4976
4994
  return {
4977
4995
  isOpen: () => open,
4978
4996
  set(next) {
4979
- if (open === next) return
4980
- open = next
4981
- for (const fn of listeners) fn()
4997
+ if (open === next) return
4998
+ open = next
4999
+ for (const fn of listeners) fn()
4982
5000
  },
4983
5001
  toggle() { this.set(!open) },
4984
5002
  show(taskId, boardId) {
4985
- wanted = { id: String(taskId || ''), board: String(boardId || 'main') }
4986
- if (open) { for (const fn of listeners) fn() } else this.set(true)
5003
+ wanted = { id: String(taskId || ''), board: String(boardId || 'main') }
5004
+ if (open) { for (const fn of listeners) fn() } else this.set(true)
4987
5005
  },
4988
5006
  takeWanted() {
4989
- const asked = wanted
4990
- wanted = undefined
4991
- return asked
5007
+ const asked = wanted
5008
+ wanted = undefined
5009
+ return asked
4992
5010
  },
4993
5011
  subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) },
4994
5012
  }
@@ -5094,10 +5112,10 @@ window.__ModuleLoader__.load({
5094
5112
 
5095
5113
  const applyActive = () => {
5096
5114
  if (toggle.isOpen()) {
5097
- document.documentElement.setAttribute(ACTIVE_ATTR, '')
5098
- document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL }))
5115
+ document.documentElement.setAttribute(ACTIVE_ATTR, '')
5116
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL }))
5099
5117
  } else {
5100
- document.documentElement.removeAttribute(ACTIVE_ATTR)
5118
+ document.documentElement.removeAttribute(ACTIVE_ATTR)
5101
5119
  }
5102
5120
  }
5103
5121
 
@@ -5134,17 +5152,17 @@ window.__ModuleLoader__.load({
5134
5152
  function registerFirst(ctx, candidates, component) {
5135
5153
  for (const entry of candidates) {
5136
5154
  try {
5137
- if (typeof ctx.slots?.has === 'function' && !ctx.slots.has(entry.name)) {
5138
- continue
5139
- }
5140
- let registeredInSlot = false
5141
- const res = ctx.slots.inject(entry.name, () => {
5142
- ctx.slots.register(entry, component)
5143
- registeredInSlot = true
5144
- })
5145
- if (registeredInSlot || (res !== null && res !== false && res !== undefined)) {
5146
- return entry.name
5147
- }
5155
+ if (typeof ctx.slots?.has === 'function' && !ctx.slots.has(entry.name)) {
5156
+ continue
5157
+ }
5158
+ let registeredInSlot = false
5159
+ const res = ctx.slots.inject(entry.name, () => {
5160
+ ctx.slots.register(entry, component)
5161
+ registeredInSlot = true
5162
+ })
5163
+ if (registeredInSlot || (res !== null && res !== false && res !== undefined)) {
5164
+ return entry.name
5165
+ }
5148
5166
  } catch { }
5149
5167
  }
5150
5168
  return undefined
@@ -5152,6 +5170,7 @@ window.__ModuleLoader__.load({
5152
5170
 
5153
5171
  helpers.createToggle = createToggle
5154
5172
  helpers.BoardScreen = BoardScreen
5173
+ helpers.TaskChip = TaskChip
5155
5174
 
5156
5175
  function apply(ctx) {
5157
5176
  ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-kanban: locales')