@goodandready/dsh-kanban 0.2.2 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -13,12 +13,12 @@ window.__ModuleLoader__.load({
13
13
  var module = { exports: {} }
14
14
  var exports = module.exports
15
15
  const React = require('react')
16
+ const h = React.createElement
16
17
  const ReactDOM = require('react-dom/client')
17
18
 
18
19
  const NS = 'dsh-kanban'
19
20
  const COLUMN_ORDER = ['backlog', 'in-progress', 'review', 'deploy', 'cleanup', 'done']
20
21
 
21
- // ---------------------------------------------------------- чистая часть
22
22
 
23
23
  /**
24
24
  * Соседи для переноса карточки. Хранилище работает не с числовым индексом,
@@ -115,7 +115,7 @@ window.__ModuleLoader__.load({
115
115
  function rememberLaunch(board, choice) {
116
116
  try {
117
117
  window.localStorage.setItem(RECALL_KEY + (board || 'main'), JSON.stringify(choice))
118
- } catch { /* нечем запомнить — не повод ломать запуск */ }
118
+ } catch { /* storage unavailable */ }
119
119
  }
120
120
 
121
121
  /**
@@ -141,7 +141,6 @@ window.__ModuleLoader__.load({
141
141
  }
142
142
  const out = {}
143
143
  for (const group of bySession.values()) {
144
- // Одна задача в сессии — не пачка: отметка «1 из 1» ничего не сообщает.
145
144
  if (group.length < 2) continue
146
145
  const order = group.slice().sort((a, b) => (a.queuedAt || 0) - (b.queuedAt || 0))
147
146
  order.forEach((task, at) => { out[task.id] = { at: at + 1, total: order.length } })
@@ -191,17 +190,15 @@ window.__ModuleLoader__.load({
191
190
  const href = typeof task.issueUrl === 'string' && /^https?:\/\//i.test(task.issueUrl)
192
191
  ? task.issueUrl
193
192
  : ''
194
- return React.createElement('span', { className: 'dkb-ref' },
195
- React.createElement('span', { className: 'dkb-refRepo', title: task.repo }, task.repo),
193
+ return h('span', { className: 'dkb-ref' },
194
+ h('span', { className: 'dkb-refRepo', title: task.repo }, task.repo),
196
195
  number === ''
197
196
  ? null
198
197
  : href === ''
199
- ? React.createElement('span', { className: 'dkb-refNum' }, number)
200
- : React.createElement('a', {
198
+ ? h('span', { className: 'dkb-refNum' }, number)
199
+ : h('a', {
201
200
  className: 'dkb-refNum dkb-refLink', href, target: '_blank', rel: 'noreferrer noopener',
202
201
  title: t ? t('panel.issue') : undefined,
203
- // Щелчок по номеру — «открыть issue», а не «открыть карточку».
204
- // Два разных намерения, и путать их нельзя.
205
202
  onClick: (e) => e.stopPropagation(),
206
203
  }, number),
207
204
  )
@@ -224,9 +221,6 @@ window.__ModuleLoader__.load({
224
221
  function planLine(task, t) {
225
222
  const progress = task && task.plan && task.plan.progress
226
223
  if (!progress || !progress.total) return ''
227
- // На идущем пункте показываем ЕГО номер, а не число сделанных: «3 из 7 ·
228
- // пишу тесты» читается как «идёт третий», и так оно и есть. Когда
229
- // текущего пункта нет — план не начат или закончен, — показываем сделанные.
230
224
  const shown = progress.text ? progress.current : progress.done
231
225
  const counter = t('plan.counter', { done: shown, total: progress.total })
232
226
  return progress.text ? counter + ' · ' + progress.text : counter
@@ -312,8 +306,6 @@ window.__ModuleLoader__.load({
312
306
  if (state === 'failed') {
313
307
  const where = (sync.problem && sync.problem.where) || ''
314
308
  const why = (sync.problem && sync.problem.message) || ''
315
- // Время ПОСЛЕДНЕГО УСПЕХА, а не последней попытки: вопрос человека —
316
- // когда доска в последний раз видела правду.
317
309
  const seen = sync.okAt
318
310
  ? t('sync.lastSeen', { ago: agoText(now, sync.okAt, t) })
319
311
  : t('sync.neverSeen')
@@ -345,7 +337,6 @@ window.__ModuleLoader__.load({
345
337
  const r = (n >> 16) & 255
346
338
  const g = (n >> 8) & 255
347
339
  const b = n & 255
348
- // Коэффициенты воспринимаемой яркости: глаз чувствительнее к зелёному.
349
340
  return (r * 299 + g * 587 + b * 114) / 1000 > 140 ? '#111' : '#fff'
350
341
  }
351
342
 
@@ -383,11 +374,11 @@ window.__ModuleLoader__.load({
383
374
  function renderSpans(spans, keyBase) {
384
375
  return (spans || []).map((span, i) => {
385
376
  const key = keyBase + '-' + i
386
- if (span.kind === 'code') return React.createElement('code', { className: 'dkb-code', key }, span.text)
387
- if (span.kind === 'strong') return React.createElement('strong', { key }, span.text)
388
- if (span.kind === 'em') return React.createElement('em', { key }, span.text)
377
+ if (span.kind === 'code') return h('code', { className: 'dkb-code', key }, span.text)
378
+ if (span.kind === 'strong') return h('strong', { key }, span.text)
379
+ if (span.kind === 'em') return h('em', { key }, span.text)
389
380
  if (span.kind === 'link') {
390
- return React.createElement('a', {
381
+ return h('a', {
391
382
  key, href: span.href, target: '_blank', rel: 'noreferrer noopener',
392
383
  }, span.text)
393
384
  }
@@ -399,26 +390,24 @@ window.__ModuleLoader__.load({
399
390
  return (blocks || []).map((block, i) => {
400
391
  const key = 'b' + i
401
392
  if (block.kind === 'heading') {
402
- // Заголовки тела не спорят с заголовком окна: рисуем их своим
403
- // классом, а не h1..h6, у которых свой размер в оболочке.
404
- return React.createElement('div', {
393
+ return h('div', {
405
394
  key, className: 'dkb-mdHead dkb-mdHead' + Math.min(block.level, 3),
406
395
  }, renderSpans(block.spans, key))
407
396
  }
408
397
  if (block.kind === 'code') {
409
- return React.createElement('pre', { key, className: 'dkb-mdCode' }, block.text)
398
+ return h('pre', { key, className: 'dkb-mdCode' }, block.text)
410
399
  }
411
- if (block.kind === 'rule') return React.createElement('hr', { key, className: 'dkb-mdRule' })
400
+ if (block.kind === 'rule') return h('hr', { key, className: 'dkb-mdRule' })
412
401
  if (block.kind === 'quote') {
413
- return React.createElement('blockquote', { key, className: 'dkb-mdQuote' },
402
+ return h('blockquote', { key, className: 'dkb-mdQuote' },
414
403
  renderSpans(block.spans, key))
415
404
  }
416
405
  if (block.kind === 'list') {
417
- return React.createElement(block.ordered ? 'ol' : 'ul', { key, className: 'dkb-mdList' },
418
- (block.items || []).map((item, j) => React.createElement('li', { key: key + '-' + j },
406
+ return h(block.ordered ? 'ol' : 'ul', { key, className: 'dkb-mdList' },
407
+ (block.items || []).map((item, j) => h('li', { key: key + '-' + j },
419
408
  renderSpans(item, key + '-' + j))))
420
409
  }
421
- return React.createElement('p', { key, className: 'dkb-mdPara' }, renderSpans(block.spans, key))
410
+ return h('p', { key, className: 'dkb-mdPara' }, renderSpans(block.spans, key))
422
411
  })
423
412
  }
424
413
 
@@ -467,17 +456,12 @@ window.__ModuleLoader__.load({
467
456
  return {
468
457
  board: (payload && payload.board) || 'main',
469
458
  kind: (payload && payload.kind) || 'project',
470
- // Своё «сейчас» на случай ответа без него: часы браузера и сервера
471
- // расходятся, и лучше считать по одному из них, чем по нулю.
472
459
  now: (payload && typeof payload.now === 'number') ? payload.now : Date.now(),
473
460
  boards: payload && Array.isArray(payload.boards) ? payload.boards : [],
474
461
  facets: payload && Array.isArray(payload.facets) ? payload.facets : [],
475
462
  sync: (payload && payload.sync) || { state: 'never' },
476
463
  columns,
477
464
  tasks: payload && Array.isArray(payload.tasks) ? payload.tasks : [],
478
- // Фактический корень проектов: {path, set} — путь и признак «задан
479
- // настройкой». Сервер знает и настройку, и рабочую папку харнесса,
480
- // браузеру ни то ни другое не доступно.
481
465
  projectRoot: payload && typeof payload.projectRoot === 'object' && payload.projectRoot !== null
482
466
  ? { path: String(payload.projectRoot.path || ''), set: payload.projectRoot.set === true }
483
467
  : { path: '', set: false },
@@ -625,11 +609,8 @@ window.__ModuleLoader__.load({
625
609
  columnTitle, planLine, stateLabel, isCollapsed,
626
610
  agoText, exactAt, shortDate, syncLine, readableOn, tagStyle, priorityColor, renderBlocks, renderSpans, matchesFilters, anySelected, toggleValue, normalizeBoard, errorKey,
627
611
  chipSessionId, matchesQuery, splitFullName, repoOption, openSession, COLUMN_ORDER,
628
- // createToggle подставляется ниже, когда объявлен: он нужен тестам, а
629
- // объявление живёт рядом со встраиванием в оболочку.
630
612
  }
631
613
 
632
- // ------------------------------------------------------------------ сеть
633
614
 
634
615
  async function api(path, options) {
635
616
  const res = await fetch('/dsh-kanban' + path, Object.assign({
@@ -645,16 +626,102 @@ window.__ModuleLoader__.load({
645
626
  return payload
646
627
  }
647
628
 
648
- // ----------------------------------------------------------------- стили
629
+
630
+ function ensureCss() {
631
+ if (typeof document === 'undefined') return
632
+ if (document.getElementById('dsh-kanban-full-css')) return
633
+ const style = document.createElement('style')
634
+ style.id = 'dsh-kanban-full-css'
635
+ style.dataset.dshPlugin = NS
636
+ style.textContent = css
637
+ document.head.appendChild(style)
638
+ }
639
+
640
+ function createErrorBoundary() {
641
+ if (!React || typeof React.Component !== 'function') {
642
+ return function NoopBoundary(props) { return props?.children || null }
643
+ }
644
+ return class ErrorBoundary extends React.Component {
645
+ constructor(props) {
646
+ super(props)
647
+ this.state = { hasError: false, error: null }
648
+ }
649
+ static getDerivedStateFromError(error) {
650
+ return { hasError: true, error }
651
+ }
652
+ componentDidCatch(error, errorInfo) {
653
+ console.error('[dsh-kanban] React Error:', error, errorInfo)
654
+ }
655
+ render() {
656
+ if (this.state.hasError) {
657
+ return h(
658
+ 'div',
659
+ {
660
+ style: {
661
+ margin: '12px 0', padding: '14px', borderRadius: '8px',
662
+ background: 'var(--dsw-alias-state-danger-bg, #ffebe9)',
663
+ border: '1px solid var(--dsw-alias-state-danger-border, #ffc1c0)',
664
+ },
665
+ },
666
+ h('div', {
667
+ style: { fontWeight: 600, marginBottom: '6px', color: 'var(--dsw-alias-state-danger-primary, #cf222e)' },
668
+ }, '⚠️ Kanban UI Error:'),
669
+ h('div', {
670
+ style: { fontSize: '12px', wordBreak: 'break-all', color: 'var(--dsw-alias-label-primary)' },
671
+ }, String(this.state.error?.message || this.state.error)),
672
+ h(
673
+ 'button',
674
+ {
675
+ type: 'button',
676
+ className: 'dkb-discard',
677
+ style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
678
+ onClick: () => this.setState({ hasError: false, error: null }),
679
+ },
680
+ 'Retry'
681
+ )
682
+ )
683
+ }
684
+ return this.props?.children || null
685
+ }
686
+ }
687
+ }
688
+
689
+ function refreshMirrorUntilVisible(ctx) {
690
+ const setTimer = typeof setInterval === 'function' ? setInterval : (typeof globalThis !== 'undefined' && typeof globalThis.setInterval === 'function' ? globalThis.setInterval : null)
691
+ const clearTimer = typeof clearInterval === 'function' ? clearInterval : (typeof globalThis !== 'undefined' && typeof globalThis.clearInterval === 'function' ? globalThis.clearInterval : null)
692
+ if (!setTimer || !clearTimer) return () => {}
693
+ const visible = () => {
694
+ try {
695
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
696
+ const view = s?.describe?.()?.getSnapshot?.()?.view
697
+ return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
698
+ } catch (_) {
699
+ return false
700
+ }
701
+ }
702
+ if (visible()) return () => {}
703
+ let tries = 0
704
+ const timer = setTimer(() => {
705
+ if (visible() || tries >= 15) { clearTimer(timer); return }
706
+ tries += 1
707
+ try {
708
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
709
+ s?.describe?.()?.load?.()
710
+ } catch (_) { /* clipboard read best-effort */ }
711
+ }, 1000)
712
+ return () => clearTimer(timer)
713
+ }
714
+
649
715
 
650
716
  const css =
717
+ '.dkb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:8px;margin-bottom:12px}' +'.dkb-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px;padding-bottom:4px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +'.dkb-section-badge{font-size:11px;font-weight:500;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}' +'.dkb-section-badge-ok{border-color:var(--dsw-alias-state-success-border,#34d399);color:var(--dsw-alias-state-success-primary,#059669);background:var(--dsw-alias-state-success-bg,rgba(16,185,129,0.1))}' +
651
718
  '.dkb-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
652
719
  '.dkb-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
653
720
  '.dkb-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
654
721
  '.dkb-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
655
722
  '.dkb-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
656
723
  '.dkb-sub{color:var(--dsw-alias-label-secondary);font-size:13px}' +
657
- '.dkb-chevron{color:var(--dsw-alias-label-secondary);flex:none;transition:transform .16s}' +
724
+ '.dkb-chevron{color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;flex:none;transition:transform .16s}' +
658
725
  '.dkb-chevronOpen{transform:rotate(180deg)}' +
659
726
  '.dkb-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}' +
660
727
  '.dkb-group{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:14px 0 2px}' +
@@ -672,27 +739,18 @@ window.__ModuleLoader__.load({
672
739
  '.dkb-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
673
740
  '.dkb-discard{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 14px;font-size:13px;color:var(--dsw-alias-label-secondary);background:transparent}' +
674
741
  '.dkb-discard:disabled,.dkb-save:disabled{opacity:.4;cursor:default}' +
675
- // Строка в боковой панели. Метрики и переменные взяты у оболочки, чтобы
676
- // строка не выделялась среди родных: та же высота, те же состояния.
677
- // Кнопка — клон родной, поэтому вид берётся у оболочки. Своего здесь
678
- // ровно столько, чтобы разложить значок и подпись внутри клона.
679
742
  '.dkb-entry-clone{align-items:center;gap:8px;display:flex}' +
680
743
  '.dkb-entry-clone[data-active]{font-weight:600}' +
681
744
  '.dkb-entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}' +
682
745
  '.dkb-entryLabel{text-overflow:ellipsis;overflow:hidden}' +
683
746
  '[data-dsh-frame][data-sidebar-collapsed] .dkb-entryLabel{display:none}' +
684
- // Экран поверх колонки разговора. Показывается атрибутом на <html>, а
685
- // соседи колонки прячутся — иначе доска легла бы поверх живого чата.
686
747
  '[data-pane=conversation],[class*=centerCol]{position:relative}' +
687
748
  '[data-dsh-kanban-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}' +
688
749
  'html[data-dsh-kanban-active]:not([data-dsh-ssh-active]) [data-dsh-kanban-view]{display:block}' +
689
750
  'html[data-dsh-kanban-active]:not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-kanban-view]),' +
690
751
  'html[data-dsh-kanban-active]:not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-kanban-view]){display:none!important}' +
691
- // Сама доска
692
752
  '.dkb-screen{box-sizing:border-box;background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:12px;padding:14px 16px 56px;display:flex}' +
693
753
  '.dkb-bar{flex:none;align-items:center;gap:8px;display:flex;flex-wrap:wrap}' +
694
- // Всё управление в шапке одной высоты: пять разных высот подряд читаются
695
- // как ряд, собранный из чужих деталей.
696
754
  '.dkb-bar .dkb-input,.dkb-bar .dkb-search,.dkb-bar .dkb-save,.dkb-bar .dkb-discard,.dkb-bar .dkb-facetHead,.dkb-bar .dkb-back{height:30px;box-sizing:border-box;padding-top:0;padding-bottom:0;border-radius:8px;font-size:13px;line-height:28px;display:inline-flex;align-items:center}' +
697
755
  '.dkb-bar .dkb-facet{display:inline-flex}' +
698
756
  '.dkb-back{appearance:none;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;align-items:center;gap:6px;padding:4px 10px;font-size:13px;display:flex;flex:none}' +
@@ -733,7 +791,7 @@ window.__ModuleLoader__.load({
733
791
  '.dkb-cols{overscroll-behavior-inline:contain;scrollbar-color:var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);scrollbar-width:thin;flex:1;gap:12px;min-height:0;padding-bottom:6px;display:flex;overflow:auto hidden}' +
734
792
  '.dkb-cols::-webkit-scrollbar{height:10px}' +
735
793
  '.dkb-cols::-webkit-scrollbar-track{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px}' +
736
- '.dkb-cols::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid #0000;border-radius:999px}' +
794
+ '.dkb-cols::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid transparent;border-radius:999px}' +
737
795
  '.dkb-col{flex:1 1 220px;min-width:220px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;min-height:0;display:flex;overflow:hidden}' +
738
796
  '.dkb-colOver{border-color:var(--dsw-alias-border-l3)}' +
739
797
  '.dkb-colHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;flex:none;align-items:center;gap:6px;padding:10px 12px;display:flex}' +
@@ -755,7 +813,7 @@ window.__ModuleLoader__.load({
755
813
  '.dkb-count[data-col=review]{background:var(--dsw-alias-state-business-primary)}' +
756
814
  '.dkb-count[data-col=deploy]{background:var(--dsw-alias-state-business-primary)}' +
757
815
  '.dkb-count[data-col=done]{background:var(--dsw-alias-state-success-primary)}' +
758
- '.dkb-countOver{background:var(--dsw-alias-state-error-primary);color:#fff}' +
816
+ '.dkb-countOver{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-inverse,#fff)}' +
759
817
  '.dkb-overdue{border-color:var(--dsw-alias-state-error-primary)}' +
760
818
  '.dkb-dueOver{color:var(--dsw-alias-state-error-primary);font-weight:600}' +
761
819
  '.dkb-list{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}' +
@@ -797,14 +855,10 @@ window.__ModuleLoader__.load({
797
855
  '.dkb-planItem.dkb-planActive{color:var(--dsw-alias-label-primary);font-weight:600;text-decoration:none}' +
798
856
  '.dkb-planMark{flex:none;width:14px;text-align:center}' +
799
857
  '.dkb-tags{display:flex;flex-wrap:wrap;gap:4px}' +
800
- // Ожидание — не колонка, а состояние карточки: агент стоит и ждёт человека.
801
858
  '.dkb-waiting{color:var(--dsw-alias-state-warn-primary);font-weight:600}' +
802
859
  '.dkb-waitDot{border-radius:50%;flex:none;width:8px;height:8px;background:var(--dsw-alias-state-warn-primary);display:inline-block}' +
803
860
  '.dkb-tag{white-space:nowrap;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border:1px solid transparent;border-radius:999px;padding:1px 7px;font-size:11px;line-height:1.5;font-weight:500}' +
804
- // Снизу оставлен запас: поверх интерфейса висят плавающие виджеты соседних
805
- // плагинов (например счётчик расходов), и без запаса последняя кнопка
806
- // оказывается под ними — нажать нельзя, а выглядит как «ничего не работает».
807
- '.dkb-modal{position:absolute;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;padding:24px 24px 84px;box-sizing:border-box;z-index:70}' +
861
+ '.dkb-modal{position:absolute;inset:0;background:var(--dsw-alias-mask-bg,rgba(0,0,0,.4));display:flex;align-items:center;justify-content:center;padding:24px 24px 84px;box-sizing:border-box;z-index:70}' +
808
862
  '.dkb-panel{width:min(720px,100%);max-height:100%;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;box-sizing:border-box;overflow-y:auto;display:flex;flex-direction:column;gap:12px}' +
809
863
  '@media (min-width: 960px){.dkb-panelWide{width:min(1100px,96%)}.dkb-drawerGrid{display:grid;grid-template-columns:300px 1fr;gap:20px;align-items:start}}' +
810
864
  '.dkb-drawerSidebar{display:flex;flex-direction:column;gap:12px;border-right:1px solid var(--dsw-alias-border-l2);padding-right:16px}' +
@@ -812,31 +866,40 @@ window.__ModuleLoader__.load({
812
866
  '.dkb-panelBody{white-space:pre-wrap;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;margin:0}' +
813
867
  '.dkb-log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}' +
814
868
  '.dkb-logRow{color:var(--dsw-alias-label-secondary);font-size:12px}' +
815
- '.dkb-dialog{position:absolute;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:80}' +
869
+ '.dkb-dialog{position:absolute;inset:0;background:var(--dsw-alias-mask-bg,rgba(0,0,0,.4));display:flex;align-items:center;justify-content:center;z-index:80}' +
816
870
  '.dkb-dialogBox{width:min(560px,94%);max-height:86%;overflow-y:auto;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:10px}' +
817
871
  '.dkb-issue{appearance:none;font:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;color:var(--dsw-alias-label-primary);font-size:13px}' +
818
872
  '.dkb-issueUsed{opacity:.5}' +
819
873
  '.dkb-row{display:flex;gap:8px;align-items:center}' +
820
874
  '.dkb-search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 240px;padding:6px 10px;font-size:13px}' +
821
875
  '.dkb-search::placeholder{color:var(--dsw-alias-label-tertiary)}' +
822
- // Указатель места вставки: без него карточку отпускают вслепую.
823
876
  '.dkb-slot{height:2px;border-radius:2px;background:transparent;flex:none}' +
824
877
  '.dkb-slotOn{background:var(--dsw-alias-state-business-primary)}' +
825
878
  '.dkb-danger{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:5px 14px;font-size:13px;color:var(--dsw-alias-state-error-primary);background:transparent}' +
826
- '.dkb-danger:hover{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-bg-base)}'
879
+ '.dkb-danger:hover{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-bg-base)}' +
880
+ '.dkb-quickFilterBar{display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 0}' +
881
+ '.dkb-filterChip{appearance:none;font:inherit;cursor:pointer;background:var(--dsw-alias-interactive-bg-hover);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--dsw-alias-label-secondary);transition:all .15s}' +
882
+ '.dkb-filterChip:hover{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}' +
883
+ '.dkb-filterChipActive{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-inverse,#fff);border-color:transparent;font-weight:600}' +
884
+ '.dkb-epicTag{background:var(--dsw-alias-state-info-bg,#ddf4ff);color:var(--dsw-alias-state-info-primary,#0969da);font-weight:600}' +
885
+ '.dkb-progressBar{height:4px;background:var(--dsw-alias-border-l2);border-radius:2px;overflow:hidden;margin:4px 0}' +
886
+ '.dkb-progressFill{height:100%;background:var(--dsw-alias-state-success-primary,#16a34a);transition:width .2s}' +
887
+ '.dkb-consolePre{background:var(--dsw-alias-bg-layer-1,#0d1117);color:var(--dsw-alias-state-info-primary,#58a6ff);border:1px solid var(--dsw-alias-border-l2);padding:12px;border-radius:8px;font-family:monospace;font-size:11px;max-height:260px;overflow-y:auto;white-space:pre-wrap}'
827
888
 
828
889
 
829
- // ---------------------------------------------------------------- строки
830
890
 
831
- // Правило регистра, чтобы подписи не расползались:
832
- //
833
- // подпись УПРАВЛЕНИЯ — с прописной: «Архив», «Поиск», «Обновить». Это
834
- // имя кнопки, а не продолжение фразы;
835
- // встроенный текст и состояние — со строчной: «ждёт ответа», «здесь
836
- // 3 дн», «показано 12 из 81». Это часть предложения, а не ярлык.
837
- //
838
- // Смешение того и другого в одном ряду и выглядит халтурой.
839
891
  const en = {
892
+ 'updater.title': 'Plugin Updates',
893
+ 'updater.check': 'Check updates',
894
+ 'updater.checking': 'Checking…',
895
+ 'updater.current': 'Current version',
896
+ 'updater.latest': 'Latest version',
897
+ 'updater.upToDate': 'Up to date',
898
+ 'updater.updateNow': 'Update to v{version}',
899
+ 'updater.updating': 'Updating…',
900
+ 'updater.success': 'Plugin updated successfully! Please restart DSH service.',
901
+ 'updater.failed': 'Update check failed: ',
902
+
840
903
  'title': 'Kanban',
841
904
  'subtitle': 'Task board: Gitea issues and a session per task',
842
905
  'section.label': 'Kanban',
@@ -1176,16 +1239,45 @@ window.__ModuleLoader__.load({
1176
1239
  'comments.placeholder': 'Add a note or feedback...',
1177
1240
  'comments.authorUser': 'User',
1178
1241
  'comments.authorAgent': 'Agent',
1242
+ 'epic.badge': 'Epic',
1243
+ 'epic.progress': '{done}/{total} subtasks',
1244
+ 'epic.triage': 'Triage / Decompose',
1245
+ 'epic.triageTitle': 'Decompose Task into Subtasks',
1246
+ 'epic.triageHelp': 'Enter one subtask title per line. Child tasks will be linked to this Epic.',
1247
+ 'epic.triageSubmit': 'Create Subtasks',
1248
+ 'epic.createGiteaIssues': 'Also create linked Gitea issues',
1249
+ 'epic.subtasks': 'Subtasks',
1250
+ 'epic.addSubtask': 'Add subtask',
1251
+ 'epic.noSubtasks': 'No subtasks yet.',
1252
+ 'console.tab': 'Live Console',
1253
+ 'console.empty': 'No agent output recorded yet.',
1254
+ 'filter.all': 'All',
1255
+ 'filter.active': 'Active',
1256
+ 'filter.review': 'Review',
1257
+ 'filter.epics': 'Epics',
1258
+ 'filter.stale': 'Silent',
1259
+ 'pr.create': 'Create Gitea PR',
1260
+ 'pr.creating': 'Creating PR...',
1261
+ 'pr.created': 'PR #{n} created',
1262
+ 'pr.view': 'View PR',
1179
1263
  'error.issueNotFound': 'Issue not found.',
1180
1264
  'error.titleRequired': 'A title is required.',
1181
1265
  'error.crossSite': 'Request rejected as cross-site.',
1182
1266
  'error.unknown': 'Request failed.',
1183
1267
  }
1184
1268
 
1185
- // Китайский — вторая встроенная локаль ядра. Русского здесь нет:
1186
- // он приезжает языковым пакетом `dsh-russian-lang`, который
1187
- // регистрирует `ru` для этого пространства извне.
1188
1269
  const zh = {
1270
+ 'updater.title': '插件更新',
1271
+ 'updater.check': '检查更新',
1272
+ 'updater.checking': '正在检查…',
1273
+ 'updater.current': '当前版本',
1274
+ 'updater.latest': '最新版本',
1275
+ 'updater.upToDate': '已是最新',
1276
+ 'updater.updateNow': '一键更新至 v{version}',
1277
+ 'updater.updating': '正在更新…',
1278
+ 'updater.success': '插件更新成功!请重启 DSH 服务以生效。',
1279
+ 'updater.failed': '更新检查失败:',
1280
+
1189
1281
  'title': '看板',
1190
1282
  'subtitle': '任务看板:Gitea issue,每个任务一个会话',
1191
1283
  'section.label': '看板',
@@ -1525,6 +1617,27 @@ window.__ModuleLoader__.load({
1525
1617
  'comments.placeholder': '添加说明或反馈...',
1526
1618
  'comments.authorUser': '用户',
1527
1619
  'comments.authorAgent': 'Agent',
1620
+ 'epic.badge': '史诗',
1621
+ 'epic.progress': '{done}/{total} 子任务',
1622
+ 'epic.triage': '分解子任务',
1623
+ 'epic.triageTitle': '将任务分解为子任务',
1624
+ 'epic.triageHelp': '每行输入一个子任务标题。子任务将关联到此史诗。',
1625
+ 'epic.triageSubmit': '创建子任务',
1626
+ 'epic.createGiteaIssues': '同时在 Gitea 中创建关联 issue',
1627
+ 'epic.subtasks': '子任务',
1628
+ 'epic.addSubtask': '添加子任务',
1629
+ 'epic.noSubtasks': '暂无子任务。',
1630
+ 'console.tab': '实时终端',
1631
+ 'console.empty': '暂无 Agent 活动输出。',
1632
+ 'filter.all': '全部',
1633
+ 'filter.active': '进行中',
1634
+ 'filter.review': '待复核',
1635
+ 'filter.epics': '史诗',
1636
+ 'filter.stale': '沉默',
1637
+ 'pr.create': '创建 Gitea PR',
1638
+ 'pr.creating': '正在创建 PR...',
1639
+ 'pr.created': '已创建 PR #{n}',
1640
+ 'pr.view': '查看 PR',
1528
1641
  'error.issueNotFound': '没有找到 issue。',
1529
1642
  'error.titleRequired': '标题不能为空。',
1530
1643
  'error.crossSite': '请求被判定为跨站而拒绝。',
@@ -1532,8 +1645,6 @@ window.__ModuleLoader__.load({
1532
1645
  }
1533
1646
 
1534
1647
  function fallbackT(key, vars) {
1535
- // Запасной перевод — английский: русский приезжает языковым пакетом,
1536
- // а до службы локализации доска обязана говорить хоть на чём-то.
1537
1648
  let text = en[key] !== undefined ? en[key] : key
1538
1649
  if (vars) {
1539
1650
  for (const name of Object.keys(vars)) {
@@ -1543,7 +1654,6 @@ window.__ModuleLoader__.load({
1543
1654
  return text
1544
1655
  }
1545
1656
 
1546
- // ------------------------------------------------------- карточка настроек
1547
1657
 
1548
1658
  const FIELDS = [
1549
1659
  { field: 'giteaUrl', kind: 'text', group: 'gitea' },
@@ -1583,19 +1693,127 @@ window.__ModuleLoader__.load({
1583
1693
  return 'hint.' + spec.field
1584
1694
  }
1585
1695
 
1696
+ let ChevronIcon = null
1697
+ try {
1698
+ const primitives = typeof require === 'function' ? require('@deepseek-ai/dsh-client-ui-primitives') : null
1699
+ ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDown)
1700
+ } catch (_) {
1701
+ ChevronIcon = null
1702
+ }
1703
+
1704
+ function FallbackChevron(props) {
1705
+ return h('svg', {
1706
+ width: 14,
1707
+ height: 14,
1708
+ viewBox: '0 0 14 14',
1709
+ fill: 'none',
1710
+ 'aria-hidden': 'true',
1711
+ }, h('path', {
1712
+ d: 'M3.5 5.25L7 8.75L10.5 5.25',
1713
+ stroke: 'currentColor',
1714
+ strokeWidth: '1.5',
1715
+ strokeLinecap: 'round',
1716
+ strokeLinejoin: 'round',
1717
+ }))
1718
+ }
1719
+
1586
1720
  function Chevron(props) {
1587
- return React.createElement('svg', {
1721
+ const Icon = ChevronIcon || FallbackChevron
1722
+ return h('span', {
1588
1723
  className: 'dkb-chevron' + (props.open ? ' dkb-chevronOpen' : ''),
1589
- width: 16, height: 16, viewBox: '0 0 16 16', 'aria-hidden': 'true',
1590
- }, React.createElement('path', {
1591
- d: 'M4 6l4 4 4-4', fill: 'none', stroke: 'currentColor', strokeWidth: '1.5',
1592
- strokeLinecap: 'round', strokeLinejoin: 'round',
1593
- }))
1724
+ 'aria-hidden': 'true',
1725
+ }, h(Icon))
1726
+ }
1727
+
1728
+
1729
+ function PluginUpdaterSection(props) {
1730
+ const t = props.t || fallbackT
1731
+ const [status, setStatus] = React.useState(null)
1732
+ const [loading, setLoading] = React.useState(false)
1733
+ const [updating, setUpdating] = React.useState(false)
1734
+ const [msg, setMsg] = React.useState(null)
1735
+
1736
+ const checkUpdate = React.useCallback(async () => {
1737
+ setLoading(true)
1738
+ setMsg(null)
1739
+ try {
1740
+ const res = await fetch('/api/dsh-kanban/update', {
1741
+ headers: { accept: 'application/json' },
1742
+ cache: 'no-store',
1743
+ })
1744
+ if (!res.ok) throw new Error('HTTP ' + res.status)
1745
+ const data = await res.json()
1746
+ setStatus(data)
1747
+ } catch (e) {
1748
+ setMsg({ ok: false, text: (t('updater.failed') || 'Update check failed: ') + (e?.message || String(e)) })
1749
+ } finally {
1750
+ setLoading(false)
1751
+ }
1752
+ }, [t])
1753
+
1754
+ const onUpdateNow = async () => {
1755
+ setUpdating(true)
1756
+ setMsg(null)
1757
+ try {
1758
+ const res = await fetch('/api/dsh-kanban/update', {
1759
+ method: 'POST',
1760
+ headers: {
1761
+ 'x-dsh-plugin-update': '1',
1762
+ 'content-type': 'application/json',
1763
+ },
1764
+ })
1765
+ const data = await res.json()
1766
+ if (!res.ok) throw new Error(data?.error || 'HTTP ' + res.status)
1767
+ setStatus(data)
1768
+ setMsg({ ok: true, text: t('updater.success') || 'Plugin updated successfully! Please restart DSH service.' })
1769
+ } catch (e) {
1770
+ setMsg({ ok: false, text: e?.message || String(e) })
1771
+ } finally {
1772
+ setUpdating(false)
1773
+ }
1774
+ }
1775
+
1776
+ React.useEffect(() => {
1777
+ checkUpdate()
1778
+ }, [checkUpdate])
1779
+
1780
+ const currentVer = status?.currentVersion || '0.2.7'
1781
+ const latestVer = status?.latestVersion
1782
+ const updateAvailable = Boolean(status?.updateAvailable && latestVer && latestVer !== currentVer)
1783
+
1784
+ return h('div', { className: 'dkb-section-card', key: 'g-updater' },
1785
+ h('div', { className: 'dkb-section-title' },
1786
+ h('span', null, '🔄 ' + t('updater.title')),
1787
+ h('span', { className: 'dkb-section-badge' + (!updateAvailable ? ' dkb-section-badge-ok' : '') },
1788
+ updateAvailable ? 'v' + latestVer + ' available' : 'v' + currentVer + ' (' + t('updater.upToDate') + ')'
1789
+ ),
1790
+ ),
1791
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px', marginTop: '6px' } },
1792
+ h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } },
1793
+ updateAvailable ? (t('updater.latest') + ': v' + latestVer) : (t('updater.current') + ': v' + currentVer)
1794
+ ),
1795
+ h('div', { style: { display: 'flex', gap: '8px' } },
1796
+ updateAvailable ? h('button', {
1797
+ type: 'button',
1798
+ className: 'dkb-save',
1799
+ style: { padding: '4px 10px', fontSize: '12px' },
1800
+ onClick: onUpdateNow,
1801
+ disabled: updating || loading,
1802
+ }, updating ? (t('updater.updating') || 'Updating…') : (t('updater.updateNow') || 'Update now').replace('{version}', latestVer)) : null,
1803
+ h('button', {
1804
+ type: 'button',
1805
+ className: 'dkb-discard',
1806
+ style: { padding: '4px 10px', fontSize: '12px' },
1807
+ onClick: checkUpdate,
1808
+ disabled: loading || updating,
1809
+ }, loading ? (t('updater.checking') || 'Checking…') : (t('updater.check') || 'Check updates')),
1810
+ ),
1811
+ ),
1812
+ msg ? h('p', { className: msg.ok ? 'dkb-note' : 'dkb-failed', style: { margin: '4px 0 0 0', fontSize: '12px' } }, msg.text) : null,
1813
+ )
1594
1814
  }
1595
1815
 
1596
1816
  function KanbanSettingsCard(props) {
1597
- // Все хуки объявлены выше любого возврата: ранний возврат над хуком даёт
1598
- // React error 310 при первом же изменении состояния.
1599
1817
  const ctx = props.ctx
1600
1818
  const t = props.t || fallbackT
1601
1819
  const [open, setOpen] = React.useState(false)
@@ -1605,7 +1823,10 @@ window.__ModuleLoader__.load({
1605
1823
  const [saved, setSaved] = React.useState(false)
1606
1824
 
1607
1825
  const scope = React.useMemo(
1608
- () => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
1826
+ () => {
1827
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1828
+ return s ? s.bind({ namespace: NS }) : undefined
1829
+ },
1609
1830
  [ctx],
1610
1831
  )
1611
1832
  const snapshot = React.useSyncExternalStore(
@@ -1633,9 +1854,6 @@ window.__ModuleLoader__.load({
1633
1854
  const save = React.useCallback(async () => {
1634
1855
  if (!scope || !draft) return
1635
1856
  setSaving(true); setFailed(''); setSaved(false)
1636
- // Пишем ВСЕ поля, а не до первой ошибки: обрыв на первой неудаче
1637
- // оставил бы остальные поля незаписанными, а снаружи это выглядит как
1638
- // «кнопка не работает».
1639
1857
  const broken = []
1640
1858
  for (const spec of FIELDS) {
1641
1859
  if (!draft || !Object.prototype.hasOwnProperty.call(draft, spec.field)) continue
@@ -1657,82 +1875,109 @@ window.__ModuleLoader__.load({
1657
1875
  setDraft(null); setSaved(true)
1658
1876
  }, [scope, draft, t])
1659
1877
 
1660
- const head = React.createElement('button', {
1878
+ const head = h('button', {
1661
1879
  type: 'button', className: 'dkb-head', onClick: () => setOpen((v) => !v),
1662
1880
  },
1663
- React.createElement('span', { className: 'dkb-headText' },
1664
- React.createElement('span', { className: 'dkb-title' }, t('title')),
1665
- React.createElement('span', { className: 'dkb-sub' }, t('subtitle')),
1881
+ h('span', { className: 'dkb-headText' },
1882
+ h('span', { className: 'dkb-title' }, t('title')),
1883
+ h('span', { className: 'dkb-sub' }, t('subtitle')),
1666
1884
  ),
1667
- React.createElement(Chevron, { open }),
1885
+ h(Chevron, { open }),
1668
1886
  )
1669
- const style = React.createElement('style', null, css)
1887
+ ensureCss()
1888
+ const style = null
1670
1889
 
1671
- if (!open) return React.createElement('li', { className: 'dkb-card' }, style, head)
1890
+ if (!open) return h('li', { className: 'dkb-card' }, head)
1672
1891
 
1673
1892
  let body
1674
1893
  if (status === 'loading') {
1675
- body = React.createElement('p', { className: 'dkb-note', role: 'status' }, t('settings.loading'))
1894
+ body = h('p', { className: 'dkb-note', role: 'status' }, t('settings.loading'))
1676
1895
  } else if (status !== 'ready') {
1677
- // Проверяем СТАТУС, а не значение: при `unavailable` признак writable
1678
- // остаётся истинным, и карточка нарисовала бы пустую, но с виду
1679
- // рабочую форму.
1680
- body = React.createElement('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
1896
+ body = h('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
1681
1897
  } else {
1682
1898
  const writable = !snapshot || snapshot.writable !== false
1683
- const rows = []
1684
- let lastGroup
1899
+ const groups = []
1900
+ const groupMap = {}
1685
1901
  for (const spec of FIELDS) {
1686
- if (spec.group !== lastGroup) {
1687
- lastGroup = spec.group
1688
- rows.push(React.createElement('div', { className: 'dkb-group', key: 'g-' + spec.group }, t('group.' + spec.group)))
1902
+ if (!groupMap[spec.group]) {
1903
+ groupMap[spec.group] = []
1904
+ groups.push(spec.group)
1689
1905
  }
1690
- if (spec.kind === 'bool') {
1691
- // Булево поле — флажок, а не строка «true»: набирать слово руками
1692
- // человека заставлять незачем.
1693
- rows.push(React.createElement('div', { className: 'dkb-field', key: spec.field },
1694
- React.createElement('label', { className: 'dkb-row' },
1695
- React.createElement('input', {
1696
- type: 'checkbox',
1697
- checked: String(valueOf(spec.field)) === 'true',
1698
- disabled: !writable || saving,
1699
- onChange: (e) => edit(spec.field, e.target.checked ? 'true' : 'false'),
1700
- }),
1701
- React.createElement('span', { className: 'dkb-label' }, t('field.' + spec.field)),
1702
- ),
1703
- React.createElement('p', { className: 'dkb-hint' }, t(hintKey(spec))),
1906
+ groupMap[spec.group].push(spec)
1907
+ }
1908
+
1909
+ const rows = []
1910
+ for (const grp of groups) {
1911
+ const fieldsInGroup = groupMap[grp]
1912
+ const fieldElements = []
1913
+ for (const spec of fieldsInGroup) {
1914
+ if (spec.kind === 'bool') {
1915
+ fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
1916
+ h('label', { className: 'dkb-row' },
1917
+ h('input', {
1918
+ type: 'checkbox',
1919
+ checked: String(valueOf(spec.field)) === 'true',
1920
+ disabled: !writable || saving,
1921
+ onChange: (e) => edit(spec.field, e.target.checked ? 'true' : 'false'),
1922
+ }),
1923
+ h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
1924
+ ),
1925
+ h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
1926
+ ))
1927
+ continue
1928
+ }
1929
+ const common = {
1930
+ className: spec.kind === 'area' ? 'dkb-area' : 'dkb-input',
1931
+ value: valueOf(spec.field),
1932
+ disabled: !writable || saving,
1933
+ onChange: (e) => edit(spec.field, e.target.value),
1934
+ }
1935
+ if (spec.kind === 'number') common.type = 'number'
1936
+ fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
1937
+ h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
1938
+ h(spec.kind === 'area' ? 'textarea' : 'input', common),
1939
+ h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
1704
1940
  ))
1705
- continue
1706
1941
  }
1707
- const common = {
1708
- className: spec.kind === 'area' ? 'dkb-area' : 'dkb-input',
1709
- value: valueOf(spec.field),
1710
- disabled: !writable || saving,
1711
- onChange: (e) => edit(spec.field, e.target.value),
1942
+
1943
+ let badge = null
1944
+ if (grp === 'gitea') {
1945
+ const hasUrl = Boolean(valueOf('giteaUrl'))
1946
+ badge = h('span', {
1947
+ className: 'dkb-section-badge' + (hasUrl ? ' dkb-section-badge-ok' : ''),
1948
+ }, hasUrl ? 'Gitea Configured' : 'No URL')
1949
+ } else if (grp === 'sync') {
1950
+ const interval = Number(valueOf('syncIntervalSec')) || 0
1951
+ badge = h('span', {
1952
+ className: 'dkb-section-badge' + (interval > 0 ? ' dkb-section-badge-ok' : ''),
1953
+ }, interval > 0 ? `${interval}s` : 'Manual')
1712
1954
  }
1713
- if (spec.kind === 'number') common.type = 'number'
1714
- rows.push(React.createElement('div', { className: 'dkb-field', key: spec.field },
1715
- React.createElement('span', { className: 'dkb-label' }, t('field.' + spec.field)),
1716
- React.createElement(spec.kind === 'area' ? 'textarea' : 'input', common),
1717
- React.createElement('p', { className: 'dkb-hint' }, t(hintKey(spec))),
1955
+
1956
+ rows.push(h('div', { className: 'dkb-section-card', key: 'g-' + grp },
1957
+ h('div', { className: 'dkb-section-title' },
1958
+ h('span', null, t('group.' + grp)),
1959
+ badge,
1960
+ ),
1961
+ ...fieldElements,
1718
1962
  ))
1719
1963
  }
1720
1964
  const dirty = draft !== null && Object.keys(draft).length > 0
1721
1965
  let note
1722
- if (failed) note = React.createElement('p', { className: 'dkb-failed', role: 'status' }, failed)
1723
- else if (!writable) note = React.createElement('p', { className: 'dkb-note', role: 'status' }, t('settings.readOnly'))
1724
- else if (saved) note = React.createElement('p', { className: 'dkb-note', role: 'status' }, t('settings.saved'))
1725
- else note = React.createElement('p', { className: 'dkb-note' })
1726
- rows.push(React.createElement('div', { className: 'dkb-foot', key: 'foot' },
1966
+ if (failed) note = h('p', { className: 'dkb-failed', role: 'status' }, failed)
1967
+ else if (!writable) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.readOnly'))
1968
+ else if (saved) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.saved'))
1969
+ else note = h('p', { className: 'dkb-note' })
1970
+ rows.push(h(PluginUpdaterSection, { t, key: 'updater' }))
1971
+ rows.push(h('div', { className: 'dkb-foot', key: 'foot' },
1727
1972
  note,
1728
- React.createElement('button', { type: 'button', className: 'dkb-discard', disabled: !dirty || saving, onClick: discard }, t('discard')),
1729
- React.createElement('button', { type: 'button', className: 'dkb-save', disabled: !dirty || saving || !writable, onClick: save }, t('save')),
1973
+ h('button', { type: 'button', className: 'dkb-discard', disabled: !dirty || saving, onClick: discard }, t('discard')),
1974
+ h('button', { type: 'button', className: 'dkb-save', disabled: !dirty || saving || !writable, onClick: save }, t('save')),
1730
1975
  ))
1731
1976
  body = rows
1732
1977
  }
1733
1978
 
1734
- return React.createElement('li', { className: 'dkb-card dkb-cardOpen' },
1735
- style, head, React.createElement('div', { className: 'dkb-body' }, body))
1979
+ return h('li', { className: 'dkb-card dkb-cardOpen' },
1980
+ head, h('div', { className: 'dkb-body' }, body))
1736
1981
  }
1737
1982
 
1738
1983
  /** Стрелка влево для кнопки возврата: 14x14, обводка цветом текста. */
@@ -1740,9 +1985,8 @@ window.__ModuleLoader__.load({
1740
1985
  + ' stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
1741
1986
  + '<path d="M10 3.5 5.5 8l4.5 4.5"/></svg>'
1742
1987
 
1743
- // ------------------------------------------------------------ экран доски
1744
1988
 
1745
- function TaskCard(props) {
1989
+ const TaskCard = React.memo(function TaskCard(props) {
1746
1990
  const { task, t, now } = props
1747
1991
  const ref = cardRef(task)
1748
1992
  const status = cardStatus(task)
@@ -1752,38 +1996,29 @@ window.__ModuleLoader__.load({
1752
1996
  ? t('state.queued', { n: task.queuePos || 1 })
1753
1997
  : stateLabel(task, t)
1754
1998
  const pack = props.pack
1755
- // Дата заведения, а не время в колонке. «Здесь 34 минуты» у задачи,
1756
- // приехавшей подхватом полчаса назад, не значит ничего: это возраст
1757
- // карточки на доске, а спрашивают про возраст самой задачи.
1758
1999
  const born = shortDate(task.createdAt)
1759
2000
  const touched = task.sessionId ? agoText(now, task.updatedAt, t) : ''
1760
2001
  const menu = props.menuOpen
1761
2002
  const urgent = priorityColor(task)
1762
2003
  const overdue = task.overdue === true
1763
- return React.createElement('div', {
2004
+ return h('div', {
1764
2005
  className: 'dkb-taskCard' + (urgent ? ' dkb-urgent' : '')
1765
2006
  + (overdue ? ' dkb-overdue' : '')
1766
2007
  + (props.sameSession ? ' dkb-sameSession' : ''),
1767
2008
  onMouseEnter: () => { if (props.onHover) props.onHover(task.sessionId || '') },
1768
2009
  onMouseLeave: () => { if (props.onHover) props.onHover('') },
1769
2010
  style: urgent ? { borderLeftColor: urgent } : undefined,
1770
- // Полный заголовок в подсказке: на карточке он обрезан двумя строками,
1771
- // и прочитать его иначе можно было только открыв её.
1772
2011
  title: task.title,
1773
- // Карточка становится точкой табуляции: доска была мышиной целиком, а
1774
- // перенос теперь ещё и команда агенту — действие с последствиями.
1775
2012
  tabIndex: 0,
1776
2013
  draggable: true,
1777
2014
  onDragStart: (e) => {
1778
- try { e.dataTransfer.setData('text/plain', task.id) } catch { /* не всякий носитель умеет */ }
2015
+ try { e.dataTransfer.setData('text/plain', task.id) } catch { /* dataTransfer unsupported */ }
1779
2016
  props.onDragStart(task)
1780
2017
  },
1781
2018
  onClick: () => props.onOpen(task),
1782
2019
  },
1783
- // Шапка карточки: чей это проект и что за работа. Оба ответа нужны
1784
- // раньше заголовка — по ним карточку и находят глазами в колонке.
1785
- React.createElement('div', { className: 'dkb-taskHead' },
1786
- React.createElement('input', {
2020
+ h('div', { className: 'dkb-taskHead' },
2021
+ h('input', {
1787
2022
  type: 'checkbox',
1788
2023
  className: 'dkb-pick' + (props.picked ? ' dkb-pickOn' : ''),
1789
2024
  checked: props.picked === true,
@@ -1791,113 +2026,114 @@ window.__ModuleLoader__.load({
1791
2026
  onClick: (e) => e.stopPropagation(),
1792
2027
  onChange: () => props.onPick(task),
1793
2028
  }),
1794
- waiting ? React.createElement('span', { className: 'dkb-waitDot' }) : null,
1795
- waiting ? React.createElement('span', { className: 'dkb-waiting' }, t('board.waiting')) : null,
2029
+ waiting ? h('span', { className: 'dkb-waitDot' }) : null,
2030
+ waiting ? h('span', { className: 'dkb-waiting' }, t('board.waiting')) : null,
1796
2031
  taskRef(task, t),
1797
- task.cron ? React.createElement('span', { className: 'dkb-tag', title: task.cron }, '⏰ cron') : null,
1798
- task.progressDump ? React.createElement('span', { className: 'dkb-tag', title: 'PROGRESSDUMP' }, '📋 dump') : null,
1799
- task.rejectReason ? React.createElement('span', { className: 'dkb-tag', style: { background: 'var(--dsw-alias-state-danger-bg, #ffebe9)', color: 'var(--dsw-alias-state-danger-primary, #cf222e)', borderColor: 'var(--dsw-alias-state-danger-border, #ffc1c0)' }, title: task.rejectReason }, '↩ ' + t('acceptance.rejectedBadge')) : null,
2032
+ task.isEpic ? h('span', { className: 'dkb-tag dkb-epicTag', title: t('epic.badge') }, '👑 ' + t('epic.badge')) : null,
2033
+ task.parentId ? h('span', { className: 'dkb-tag', style: { opacity: 0.75 } }, '↳') : null,
2034
+ task.subtasksCount && task.subtasksCount.total > 0
2035
+ ? h('span', { className: 'dkb-tag', title: 'Subtasks' }, t('epic.progress', { done: task.subtasksCount.completed, total: task.subtasksCount.total }))
2036
+ : null,
2037
+ task.cron ? h('span', { className: 'dkb-tag', title: task.cron }, '⏰ cron') : null,
2038
+ task.progressDump ? h('span', { className: 'dkb-tag', title: 'PROGRESSDUMP' }, '📋 dump') : null,
2039
+ task.rejectReason ? h('span', { className: 'dkb-tag', style: { background: 'var(--dsw-alias-state-danger-bg, #ffebe9)', color: 'var(--dsw-alias-state-danger-primary, #cf222e)', borderColor: 'var(--dsw-alias-state-danger-border, #ffc1c0)' }, title: task.rejectReason }, '↩ ' + t('acceptance.rejectedBadge')) : null,
1800
2040
  Array.isArray(task.checklist) && task.checklist.length > 0
1801
- ? React.createElement('span', { className: 'dkb-tag', title: 'DoD Checklist' }, `☑ ${task.checklist.filter((i) => i.completed).length}/${task.checklist.length}`)
2041
+ ? h('span', { className: 'dkb-tag', title: 'DoD Checklist' }, `☑ ${task.checklist.filter((i) => i.completed).length}/${task.checklist.length}`)
1802
2042
  : null,
1803
2043
  task.report && task.report.summary
1804
- ? React.createElement('span', { className: 'dkb-tag', title: 'Execution Report' }, '📄 report')
2044
+ ? h('span', { className: 'dkb-tag', title: 'Execution Report' }, '📄 report')
1805
2045
  : null,
1806
2046
  Array.isArray(task.comments) && task.comments.length > 0
1807
- ? React.createElement('span', { className: 'dkb-tag', title: 'Comments' }, `💬 ${task.comments.length}`)
2047
+ ? h('span', { className: 'dkb-tag', title: 'Comments' }, `💬 ${task.comments.length}`)
1808
2048
  : null,
1809
- (task.labels || []).map((l) => React.createElement('span', {
2049
+ (task.labels || []).map((l) => h('span', {
1810
2050
  className: 'dkb-tag', key: l, style: tagStyle(task, l),
1811
2051
  }, l))),
1812
- React.createElement('div', { className: 'dkb-taskTitle' }, task.title),
1813
- plan ? React.createElement('div', { className: 'dkb-planLine' }, plan) : null,
1814
- // Второй путь к переносу, не замена перетаскиванию: мышью удобнее,
1815
- // когда мышь под рукой. Подтверждение и последствия у обоих те же —
1816
- // разойтись им нельзя.
1817
- React.createElement('div', { className: 'dkb-cardMenu' },
1818
- React.createElement('button', {
2052
+ h('div', { className: 'dkb-taskTitle' }, task.title),
2053
+ task.subtasksCount && task.subtasksCount.total > 0
2054
+ ? h('div', { className: 'dkb-progressBar', title: `${task.subtasksCount.completed}/${task.subtasksCount.total}` },
2055
+ h('div', {
2056
+ className: 'dkb-progressFill',
2057
+ style: { width: Math.round((task.subtasksCount.completed / task.subtasksCount.total) * 100) + '%' },
2058
+ }))
2059
+ : null,
2060
+ plan ? h('div', { className: 'dkb-planLine' }, plan) : null,
2061
+ h('div', { className: 'dkb-cardMenu' },
2062
+ h('button', {
1819
2063
  type: 'button', className: 'dkb-moveBtn',
1820
- // Скрыто до наведения — но появляется и при фокусе с клавиатуры:
1821
- // прятать ради красоты то, что чинили ради доступности, нельзя.
1822
2064
  'aria-expanded': menu === task.id,
1823
2065
  onClick: (e) => { e.stopPropagation(); props.onMenu(menu === task.id ? '' : task.id) },
1824
2066
  }, t('card.move')),
1825
2067
  menu === task.id
1826
- ? React.createElement('div', { className: 'dkb-facetList' },
1827
- (props.columns || []).filter((c) => c !== task.column).map((c) => React.createElement('button', {
2068
+ ? h('div', { className: 'dkb-facetList' },
2069
+ (props.columns || []).filter((c) => c !== task.column).map((c) => h('button', {
1828
2070
  type: 'button', className: 'dkb-facetRow', key: c,
1829
2071
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onMove(task, c) },
1830
2072
  }, t('column.' + c))),
1831
- // Очередь стоит рядом с переносом: и то и другое — «отдать
1832
- // работу», разница лишь в том, кому и когда.
1833
- // Останавливать нечего у того, кто не идёт: пункт, который
1834
- // ничего не делает, хуже отсутствующего.
1835
2073
  task.state === 'stopped'
1836
- ? React.createElement('button', {
2074
+ ? h('button', {
1837
2075
  type: 'button', className: 'dkb-facetRow', key: 'revive',
1838
2076
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onRevive(task) },
1839
2077
  }, t('card.revive'))
1840
2078
  : null,
1841
2079
  task.state === 'running' || task.state === 'waiting'
1842
- ? React.createElement('button', {
2080
+ ? h('button', {
1843
2081
  type: 'button', className: 'dkb-facetRow', key: 'stop',
1844
2082
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onStop(task) },
1845
2083
  }, t('card.stop'))
1846
2084
  : null,
1847
2085
  task.state === 'queued'
1848
- ? React.createElement('button', {
2086
+ ? h('button', {
1849
2087
  type: 'button', className: 'dkb-facetRow', key: 'unqueue',
1850
2088
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onUnqueue(task) },
1851
2089
  }, t('card.unqueue'))
1852
- : React.createElement('button', {
2090
+ : h('button', {
1853
2091
  type: 'button', className: 'dkb-facetRow', key: 'queue',
1854
2092
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onQueue(task) },
1855
2093
  }, t('card.queue')))
1856
2094
  : null,
1857
2095
  ),
1858
2096
  status || state || born || touched
1859
- ? React.createElement('div', { className: 'dkb-taskRef' },
2097
+ ? h('div', { className: 'dkb-taskRef' },
1860
2098
  state
1861
- ? React.createElement('span', {
2099
+ ? h('span', {
1862
2100
  className: 'dkb-state' + (task.state === 'running' ? ' dkb-stateLive' : ''),
1863
2101
  }, state)
1864
2102
  : null,
1865
- // Ссылка в чат стоит рядом с отметкой работы: «идёт» без пути к
1866
- // тому, что идёт, — сведение, которым нельзя воспользоваться.
1867
2103
  task.sessionId
1868
- ? React.createElement('button', {
2104
+ ? h('button', {
1869
2105
  type: 'button', className: 'dkb-toChat',
1870
2106
  onClick: (e) => { e.stopPropagation(); props.onOpenChat(task) },
1871
2107
  }, t('card.toChat'))
1872
2108
  : null,
1873
2109
  touched
1874
- ? React.createElement('span', {
2110
+ ? h('span', {
1875
2111
  className: task.stale ? 'dkb-countOver' : '',
1876
2112
  title: exactAt(task.updatedAt),
1877
2113
  }, touched)
1878
2114
  : null,
1879
2115
  born
1880
- ? React.createElement('span', { title: exactAt(task.createdAt) }, born)
2116
+ ? h('span', { title: exactAt(task.createdAt) }, born)
1881
2117
  : null,
1882
2118
  task.dueAt > 0
1883
- ? React.createElement('span', {
2119
+ ? h('span', {
1884
2120
  className: overdue ? 'dkb-dueOver' : '',
1885
2121
  title: exactAt(task.dueAt),
1886
2122
  }, t('card.due', { date: shortDate(task.dueAt) }))
1887
2123
  : null,
1888
2124
  task.assignee
1889
- ? React.createElement('span', { className: 'dkb-who', title: t('card.assigneeHint') },
2125
+ ? h('span', { className: 'dkb-who', title: t('card.assigneeHint') },
1890
2126
  '@' + task.assignee)
1891
2127
  : null,
1892
2128
  pack
1893
- ? React.createElement('span', {
2129
+ ? h('span', {
1894
2130
  className: 'dkb-pack', title: t('card.packHint'),
1895
2131
  }, t('card.pack', { at: pack.at, total: pack.total }))
1896
2132
  : null,
1897
- status ? React.createElement('span', null, status) : null)
2133
+ status ? h('span', null, status) : null)
1898
2134
  : null,
1899
2135
  )
1900
- }
2136
+ })
1901
2137
 
1902
2138
  /**
1903
2139
  * Чем запускать: профиль агента и уровень доступа.
@@ -1913,36 +2149,34 @@ window.__ModuleLoader__.load({
1913
2149
  const t = props.t
1914
2150
  const rows = []
1915
2151
  if (!props.hidePreset && (props.presets.agentPresets || []).length > 0) {
1916
- rows.push(React.createElement('div', { className: 'dkb-field', key: 'preset' },
1917
- React.createElement('span', { className: 'dkb-label' }, t('panel.agentPreset')),
1918
- React.createElement('select', {
2152
+ rows.push(h('div', { className: 'dkb-field', key: 'preset' },
2153
+ h('span', { className: 'dkb-label' }, t('panel.agentPreset')),
2154
+ h('select', {
1919
2155
  className: 'dkb-input', value: props.agentPreset, disabled: props.disabled,
1920
2156
  onChange: (e) => props.onPreset(e.target.value),
1921
2157
  },
1922
- React.createElement('option', { value: '' }, t('panel.presetDefault')),
1923
- props.presets.agentPresets.map((one) => React.createElement('option', {
2158
+ h('option', { value: '' }, t('panel.presetDefault')),
2159
+ props.presets.agentPresets.map((one) => h('option', {
1924
2160
  value: one.id, key: one.id, title: one.description || '',
1925
2161
  }, one.name || one.id))),
1926
2162
  ))
1927
2163
  }
1928
2164
  if ((props.presets.access || []).length > 0) {
1929
2165
  const picked = (props.presets.access || []).find((one) => one.value === props.permission)
1930
- rows.push(React.createElement('div', { className: 'dkb-field', key: 'access' },
1931
- React.createElement('span', { className: 'dkb-label' }, t('panel.permission')),
1932
- React.createElement('select', {
2166
+ rows.push(h('div', { className: 'dkb-field', key: 'access' },
2167
+ h('span', { className: 'dkb-label' }, t('panel.permission')),
2168
+ h('select', {
1933
2169
  className: 'dkb-input', value: props.permission, disabled: props.disabled,
1934
2170
  onChange: (e) => props.onPermission(e.target.value),
1935
- }, props.presets.access.map((one) => React.createElement('option', {
2171
+ }, props.presets.access.map((one) => h('option', {
1936
2172
  value: one.value, key: one.value,
1937
2173
  }, one.name || one.value))),
1938
- // Пояснение к уровню доступа пишет ядро: свои слова разошлись бы с
1939
- // тем, что этот уровень значит на самом деле.
1940
2174
  picked && picked.description
1941
- ? React.createElement('p', { className: 'dkb-hint' }, picked.description)
2175
+ ? h('p', { className: 'dkb-hint' }, picked.description)
1942
2176
  : null,
1943
2177
  ))
1944
2178
  }
1945
- return rows.length === 0 ? null : React.createElement('div', { className: 'dkb-start' }, rows)
2179
+ return rows.length === 0 ? null : h('div', { className: 'dkb-start' }, rows)
1946
2180
  }
1947
2181
 
1948
2182
  function BoardScreen(props) {
@@ -1954,11 +2188,17 @@ window.__ModuleLoader__.load({
1954
2188
  const [rejectReasonText, setRejectReasonText] = React.useState('')
1955
2189
  const [newChecklistText, setNewChecklistText] = React.useState('')
1956
2190
  const [newCommentText, setNewCommentText] = React.useState('')
2191
+ const [quickFilter, setQuickFilter] = React.useState('all')
2192
+ const [subtasks, setSubtasks] = React.useState([])
2193
+ const [triageOpen, setTriageOpen] = React.useState(false)
2194
+ const [triageText, setTriageText] = React.useState('')
2195
+ const [triageGitea, setTriageGitea] = React.useState(false)
2196
+ const [triageSubmitting, setTriageSubmitting] = React.useState(false)
2197
+ const [newSubtaskTitle, setNewSubtaskTitle] = React.useState('')
2198
+ const [prCreating, setPrCreating] = React.useState(false)
2199
+ const [createdPr, setCreatedPr] = React.useState(null)
1957
2200
  const [log, setLog] = React.useState([])
1958
2201
  const [dialog, setDialog] = React.useState(null)
1959
- // Черновик правки приоритета и меток в окне задачи. Живёт отдельно от
1960
- // карточки: пока человек не нажал «Сохранить», доска не должна видеть
1961
- // полувведённое.
1962
2202
  const [editPriority, setEditPriority] = React.useState('')
1963
2203
  const [editLabels, setEditLabels] = React.useState('')
1964
2204
  const [editDue, setEditDue] = React.useState('')
@@ -1971,41 +2211,30 @@ window.__ModuleLoader__.load({
1971
2211
  const [starting, setStarting] = React.useState(false)
1972
2212
  const [draftText, setDraftText] = React.useState('')
1973
2213
  const [query, setQuery] = React.useState('')
2214
+ const [debouncedQuery, setDebouncedQuery] = React.useState('')
2215
+ React.useEffect(() => {
2216
+ const timer = setTimeout(() => setDebouncedQuery(query), 150)
2217
+ return () => clearTimeout(timer)
2218
+ }, [query])
1974
2219
  const [dropAt, setDropAt] = React.useState(null)
1975
2220
  const [board, setBoard] = React.useState('main')
1976
- // Решения о сворачивании держим в состоянии экрана, а не в хранилище:
1977
- // это взгляд одного человека на одну доску прямо сейчас, а не свойство
1978
- // задач. Обновление доски их переживает, перезагрузка страницы — нет.
1979
2221
  const [collapsed, setCollapsed] = React.useState({})
1980
- // Отборы живут в состоянии экрана: он смонтирован всё время работы
1981
- // харнесса, поэтому выбор переживает и закрытие доски, и обновление.
1982
2222
  const [filters, setFilters] = React.useState({})
1983
2223
  const [openFacet, setOpenFacet] = React.useState('')
1984
2224
  const [cardMenu, setCardMenu] = React.useState('')
1985
2225
  const [body, setBody] = React.useState(null)
1986
2226
  const [groups, setGroups] = React.useState({})
1987
2227
  const [syncing, setSyncing] = React.useState(false)
1988
- // Отмеченные для группового запуска. Живут отдельно от открытой карточки:
1989
- // выбор переживает и открытие окна, и обновление доски.
1990
2228
  const [picked, setPicked] = React.useState({})
1991
2229
  const [archive, setArchive] = React.useState(null)
1992
2230
  const [metrics, setMetrics] = React.useState(null)
1993
- // «Только ждущие» держим отдельно от отборов: это не значение измерения,
1994
- // а взгляд «покажи, где стоит работа», и складывается он с любым отбором.
1995
2231
  const [onlyWaiting, setOnlyWaiting] = React.useState(false)
1996
2232
  const [onlyOverdue, setOnlyOverdue] = React.useState(false)
1997
2233
  const [hoverSession, setHoverSession] = React.useState('')
1998
2234
  const [order, setOrder] = React.useState('manual')
1999
- // «По проектам» — как приходит с сервера; «по ответственному» — своя
2000
- // раскладка поверх того же порядка.
2001
2235
  const [grouping, setGrouping] = React.useState('repo')
2002
- // Кто «я»: спрашивается один раз у сервера. Пусто — Gitea не ответил, и
2003
- // кнопку «Мои» рисовать нельзя: соврать числом хуже, чем не показать.
2004
2236
  const [me, setMe] = React.useState('')
2005
2237
  const [onlyMine, setOnlyMine] = React.useState(false)
2006
- // Чем запускать: профиль агента и уровень доступа. Обе службы у ядра
2007
- // необязательны, поэтому пустой список — не поломка, а «выбирать не из
2008
- // чего»: поле тогда не рисуется вовсе.
2009
2238
  const [presets, setPresets] = React.useState({ agentPresets: [], access: [] })
2010
2239
  const [agentPreset, setAgentPreset] = React.useState('')
2011
2240
  const [permission, setPermission] = React.useState('')
@@ -2013,9 +2242,6 @@ window.__ModuleLoader__.load({
2013
2242
 
2014
2243
  const load = React.useCallback(async (nextBoard) => {
2015
2244
  try {
2016
- // Отбор по репозиторию делает браузер вместе с остальными отборами:
2017
- // серверный отбор спорил бы с ним и прятал бы часть значений из
2018
- // списка, потому что их не было бы в ответе.
2019
2245
  const query = '?board=' + encodeURIComponent(nextBoard || 'main')
2020
2246
  setState(normalizeBoard(await api('/board' + query)))
2021
2247
  setError('')
@@ -2026,7 +2252,6 @@ window.__ModuleLoader__.load({
2026
2252
 
2027
2253
  React.useEffect(() => { load(board) }, [load, board])
2028
2254
 
2029
- // Подписка на Server-Sent Events (SSE) для мгновенного обновления доски (#222).
2030
2255
  React.useEffect(() => {
2031
2256
  if (typeof window === 'undefined' || typeof window.EventSource !== 'function') return undefined
2032
2257
  let es = null
@@ -2035,15 +2260,19 @@ window.__ModuleLoader__.load({
2035
2260
  const connect = () => {
2036
2261
  try {
2037
2262
  es = new window.EventSource('/dsh-kanban/events')
2038
- es.addEventListener('board:change', () => {
2039
- load(board)
2040
- })
2041
- es.addEventListener('task:change', () => {
2042
- load(board)
2043
- })
2263
+ let reloadTimer = null
2264
+ const scheduleReload = () => {
2265
+ if (reloadTimer) return
2266
+ reloadTimer = setTimeout(() => {
2267
+ reloadTimer = null
2268
+ load(board)
2269
+ }, 60)
2270
+ }
2271
+ es.addEventListener('board:change', scheduleReload)
2272
+ es.addEventListener('task:change', scheduleReload)
2044
2273
  es.onerror = () => {
2045
2274
  if (es) {
2046
- try { es.close() } catch {}
2275
+ try { es.close() } catch { /* eventsource close best-effort */ }
2047
2276
  es = null
2048
2277
  }
2049
2278
  if (!reconnectTimer) {
@@ -2053,7 +2282,7 @@ window.__ModuleLoader__.load({
2053
2282
  }, 5000)
2054
2283
  }
2055
2284
  }
2056
- } catch {}
2285
+ } catch { /* eventsource setup best-effort */ }
2057
2286
  }
2058
2287
 
2059
2288
  connect()
@@ -2061,14 +2290,12 @@ window.__ModuleLoader__.load({
2061
2290
  return () => {
2062
2291
  if (reconnectTimer) clearTimeout(reconnectTimer)
2063
2292
  if (es) {
2064
- try { es.close() } catch {}
2293
+ try { es.close() } catch { /* eventsource close best-effort */ }
2065
2294
  es = null
2066
2295
  }
2067
2296
  }
2068
2297
  }, [load, board])
2069
2298
 
2070
- // Просьба чипа «покажи вот эту». Забираем её один раз: повтор открывал
2071
- // бы окно снова после каждого закрытия.
2072
2299
  const [wanted, setWanted] = React.useState('')
2073
2300
  React.useEffect(() => {
2074
2301
  const toggle = props.toggle
@@ -2083,8 +2310,6 @@ window.__ModuleLoader__.load({
2083
2310
  return toggle.subscribe(pull)
2084
2311
  }, [props.toggle])
2085
2312
 
2086
- // Открываем, когда задача приехала: доска могла переключаться, и в
2087
- // момент просьбы её ещё не было в списке.
2088
2313
  React.useEffect(() => {
2089
2314
  if (wanted === '' || state === null) return
2090
2315
  const task = state.tasks.find((x) => x.id === wanted)
@@ -2093,9 +2318,6 @@ window.__ModuleLoader__.load({
2093
2318
  openPanelRef.current(task)
2094
2319
  }, [wanted, state])
2095
2320
 
2096
- // Escape снимает по одному слою за нажатие: диалог, затем панель, затем
2097
- // саму доску. Закрывать всё разом — терять контекст, которого человек не
2098
- // просил лишаться.
2099
2321
  React.useEffect(() => {
2100
2322
  const onKey = (e) => {
2101
2323
  if (e.key !== 'Escape') return
@@ -2107,7 +2329,6 @@ window.__ModuleLoader__.load({
2107
2329
  return () => document.removeEventListener('keydown', onKey)
2108
2330
  }, [dialog, openTask, props])
2109
2331
 
2110
- // Сначала провайдеры и текущий выбор, затем модели выбранного провайдера.
2111
2332
  React.useEffect(() => {
2112
2333
  let alive = true
2113
2334
  api('/models')
@@ -2119,8 +2340,6 @@ window.__ModuleLoader__.load({
2119
2340
  const provider = stillOffered(saved.provider, list.map((p) => p.id), '')
2120
2341
  if (provider !== '') {
2121
2342
  setProvider(provider)
2122
- // Модель проверяется своим списком, когда он приедет: здесь её
2123
- // ещё не с чем сверить, а подставить чужую нельзя.
2124
2343
  setModel(typeof saved.model === 'string' ? saved.model : '')
2125
2344
  } else if (out.current) {
2126
2345
  setProvider(out.current.provider || '')
@@ -2142,8 +2361,6 @@ window.__ModuleLoader__.load({
2142
2361
  const rows = out.agentPresets || []
2143
2362
  const access = out.access || []
2144
2363
  setPresets({ agentPresets: rows, access })
2145
- // Пустое значение — «как у харнесса по умолчанию»; подставляем его
2146
- // же явно, чтобы человек видел, с чем задача пойдёт.
2147
2364
  const saved = recallLaunch(board)
2148
2365
  setAgentPreset(stillOffered(saved.agentPreset, rows.map((x) => x.id), out.agentPresetDefault || ''))
2149
2366
  setPermission(stillOffered(saved.permission, access.map((x) => x.value), out.accessDefault || ''))
@@ -2160,8 +2377,6 @@ window.__ModuleLoader__.load({
2160
2377
  if (!alive || !out) return
2161
2378
  const list = out.models || []
2162
2379
  setModels(list)
2163
- // Модель могла исчезнуть у провайдера: подставленная из памяти, она
2164
- // осталась бы в поле, а запуск отказал бы уже после нажатия.
2165
2380
  setModel((cur) => stillOffered(cur, list.map((m) => m.id), ''))
2166
2381
  })
2167
2382
  .catch(() => { if (alive) setModels([]) })
@@ -2169,8 +2384,6 @@ window.__ModuleLoader__.load({
2169
2384
  }, [provider])
2170
2385
 
2171
2386
  const commitMove = React.useCallback(async (task, column, where) => {
2172
- // Карточку не перерисовываем заранее: до подтверждения она обязана
2173
- // оставаться там, где лежала, иначе отмена выглядит откатом.
2174
2387
  try {
2175
2388
  await api('/task/' + encodeURIComponent(task.id) + '/move', {
2176
2389
  method: 'POST',
@@ -2192,7 +2405,6 @@ window.__ModuleLoader__.load({
2192
2405
  const askMove = React.useCallback((task, column, where) => {
2193
2406
  if (!task) return
2194
2407
  const place = where ?? neighboursFor(tasksOf((state && state.tasks) || [], column), task.id, Infinity)
2195
- // Перенос внутри колонки — это порядок, а не команда: спрашивать не о чем.
2196
2408
  if (task.column === column) { commitMove(task, column, place); return }
2197
2409
  setDialog({ mode: 'move', task, column, where: place })
2198
2410
  }, [state, commitMove])
@@ -2203,7 +2415,6 @@ window.__ModuleLoader__.load({
2203
2415
  setOverColumn('')
2204
2416
  setDropAt(null)
2205
2417
  if (!task || !state) return
2206
- // Мышь слишком легко задевает карточку, чтобы перенос уходил без спроса.
2207
2418
  askMove(task, column, neighboursFor(tasksOf(state.tasks, column), task.id, index))
2208
2419
  }, [state, askMove])
2209
2420
 
@@ -2218,6 +2429,15 @@ window.__ModuleLoader__.load({
2218
2429
  ? new Date(task.dueAt).toISOString().slice(0, 10)
2219
2430
  : '')
2220
2431
  setEditCron(typeof task?.cron === 'string' ? task.cron : '')
2432
+ setTriageOpen(false)
2433
+ setTriageText('')
2434
+ setTriageGitea(false)
2435
+ setCreatedPr(null)
2436
+ setNewSubtaskTitle('')
2437
+ setSubtasks([])
2438
+ api('/task/' + encodeURIComponent(task.id) + '/subtasks')
2439
+ .then((out) => setSubtasks((out && out.subtasks) || []))
2440
+ .catch(() => setSubtasks([]))
2221
2441
  api('/task/' + encodeURIComponent(task.id) + '/body')
2222
2442
  .then((out) => setBody((out && out.blocks) || []))
2223
2443
  .catch(() => setBody([]))
@@ -2231,75 +2451,65 @@ window.__ModuleLoader__.load({
2231
2451
  }, [])
2232
2452
  openPanelRef.current = openPanel
2233
2453
 
2234
- const style = React.createElement('style', null, css)
2454
+ ensureCss()
2455
+ const style = null
2235
2456
 
2236
2457
  if (state === null) {
2237
- return React.createElement('div', { className: 'dkb-screen' }, style,
2238
- React.createElement('p', { className: 'dkb-note' }, error || t('board.loading')))
2458
+ return h('div', { className: 'dkb-screen' },
2459
+ h('p', { className: 'dkb-note' }, error || t('board.loading')))
2239
2460
  }
2240
2461
 
2241
2462
  const boards = (state && state.boards) || []
2242
- const bar = React.createElement('div', { className: 'dkb-bar' },
2243
- // Первой в шапке и слева от заголовка — там она читается как «назад».
2244
- // Своего механизма закрытия не заводит: их уже три (Escape, повторный
2245
- // щелчок по боковой строке, переход в чужую сессию), и четвёртый
2246
- // разошёлся бы с ними при первой же правке.
2247
- React.createElement('button', {
2463
+ const bar = h('div', { className: 'dkb-bar' },
2464
+ h('button', {
2248
2465
  type: 'button', className: 'dkb-back', title: t('board.backHint'),
2249
2466
  onClick: () => { if (props.onClose) props.onClose() },
2250
2467
  },
2251
- React.createElement('span', {
2468
+ h('span', {
2252
2469
  className: 'dkb-backIcon',
2253
2470
  dangerouslySetInnerHTML: { __html: BACK_ICON },
2254
2471
  }),
2255
- React.createElement('span', null, t('board.back')),
2472
+ h('span', null, t('board.back')),
2256
2473
  ),
2257
- React.createElement('span', { className: 'dkb-barTitle' }, t('section.label')),
2474
+ h('span', { className: 'dkb-barTitle' }, t('section.label')),
2258
2475
  boards.length > 1
2259
- ? React.createElement('select', {
2476
+ ? h('select', {
2260
2477
  className: 'dkb-input', style: { width: 'auto' }, value: board,
2261
2478
  onChange: (e) => {
2262
- // Отборы собраны из меток ЭТОЙ доски: на другой те же значения
2263
- // могут не встречаться вовсе, и выбор превратился бы в пустоту.
2264
2479
  setOpenTask(null); setCollapsed({}); setFilters({}); setBoard(e.target.value)
2265
2480
  },
2266
- }, boards.map((b) => React.createElement('option', { value: b.id, key: b.id }, boardTitle(b, t))))
2481
+ }, boards.map((b) => h('option', { value: b.id, key: b.id }, boardTitle(b, t))))
2267
2482
  : null,
2268
2483
  (state.facets || []).map((facet) => {
2269
2484
  const picked = filters[facet.ns] || []
2270
- // Имена пространств приходят из Gitea сырыми. Известные переводим,
2271
- // незнакомое оставляем как есть: выдумывать перевод метке, которую
2272
- // завели вчера, доска не вправе.
2273
2485
  const label = facetLabel(facet.ns, t)
2274
- return React.createElement('div', { className: 'dkb-facet', key: facet.ns },
2275
- React.createElement('button', {
2486
+ return h('div', { className: 'dkb-facet', key: facet.ns },
2487
+ h('button', {
2276
2488
  type: 'button',
2277
2489
  className: 'dkb-facetHead' + (picked.length ? ' dkb-facetOn' : ''),
2278
2490
  'aria-expanded': openFacet === facet.ns,
2279
2491
  onClick: () => setOpenFacet((cur) => (cur === facet.ns ? '' : facet.ns)),
2280
2492
  }, picked.length ? label + ' · ' + picked.length : label),
2281
2493
  openFacet === facet.ns
2282
- ? React.createElement('div', { className: 'dkb-facetList' },
2283
- facet.values.map((v) => React.createElement('label', {
2494
+ ? h('div', { className: 'dkb-facetList' },
2495
+ facet.values.map((v) => h('label', {
2284
2496
  className: 'dkb-facetRow', key: v.value,
2285
2497
  },
2286
- React.createElement('input', {
2498
+ h('input', {
2287
2499
  type: 'checkbox',
2288
2500
  checked: picked.indexOf(v.value) >= 0,
2289
2501
  onChange: () => setFilters((cur) => toggleValue(cur, facet.ns, v.value)),
2290
2502
  }),
2291
- React.createElement('span', { className: 'dkb-facetName' }, v.value),
2292
- React.createElement('span', { className: 'dkb-count' }, String(v.count)),
2503
+ h('span', { className: 'dkb-facetName' }, v.value),
2504
+ h('span', { className: 'dkb-count' }, String(v.count)),
2293
2505
  )))
2294
2506
  : null,
2295
2507
  )
2296
2508
  }),
2297
2509
  (() => {
2298
- // Ноль в шапке — мебель, за которой перестают следить, поэтому
2299
- // счётчик появляется только когда кого-то действительно ждут.
2300
2510
  const waiting = state.tasks.filter((x) => x.state === 'waiting').length
2301
2511
  if (waiting === 0) return null
2302
- return React.createElement('button', {
2512
+ return h('button', {
2303
2513
  type: 'button',
2304
2514
  className: 'dkb-waitCount' + (onlyWaiting ? ' dkb-save' : ''),
2305
2515
  title: t('board.waitCountHint'),
@@ -2307,11 +2517,9 @@ window.__ModuleLoader__.load({
2307
2517
  }, t('board.waitCount', { n: waiting }))
2308
2518
  })(),
2309
2519
  (() => {
2310
- // Просроченные — те, чей дедлайн уже прошёл. Счётчик появляется
2311
- // только когда такие есть: ноль в шапке — мебель.
2312
2520
  const overdue = state.tasks.filter((x) => x.overdue === true).length
2313
2521
  if (overdue === 0) return null
2314
- return React.createElement('button', {
2522
+ return h('button', {
2315
2523
  type: 'button',
2316
2524
  className: 'dkb-waitCount' + (onlyOverdue ? ' dkb-save' : ''),
2317
2525
  title: t('board.overdueHint'),
@@ -2319,34 +2527,32 @@ window.__ModuleLoader__.load({
2319
2527
  }, t('board.overdueCount', { n: overdue }))
2320
2528
  })(),
2321
2529
  (() => {
2322
- // Ноль своих задач — кнопки нет: пустой счётчик в шапке перестают
2323
- // замечать вместе с непустым.
2324
2530
  if (me === '') return null
2325
2531
  const mine = state.tasks.filter((x) => x.assignee === me).length
2326
2532
  if (mine === 0) return null
2327
- return React.createElement('button', {
2533
+ return h('button', {
2328
2534
  type: 'button',
2329
2535
  className: 'dkb-waitCount' + (onlyMine ? ' dkb-save' : ''),
2330
2536
  title: t('board.mineHint', { who: me }),
2331
2537
  onClick: () => setOnlyMine((cur) => !cur),
2332
2538
  }, t('board.mine', { n: mine }))
2333
2539
  })(),
2334
- React.createElement('select', {
2540
+ h('select', {
2335
2541
  className: 'dkb-input', style: { width: 'auto' }, value: grouping,
2336
2542
  onChange: (e) => setGrouping(e.target.value),
2337
2543
  },
2338
- React.createElement('option', { value: 'repo' }, t('group.repo')),
2339
- React.createElement('option', { value: 'assignee' }, t('group.assignee')),
2544
+ h('option', { value: 'repo' }, t('group.repo')),
2545
+ h('option', { value: 'assignee' }, t('group.assignee')),
2340
2546
  ),
2341
- React.createElement('select', {
2547
+ h('select', {
2342
2548
  className: 'dkb-input', style: { width: 'auto' }, value: order,
2343
2549
  onChange: (e) => setOrder(e.target.value),
2344
2550
  },
2345
- React.createElement('option', { value: 'manual' }, t('order.manual')),
2346
- React.createElement('option', { value: 'old' }, t('order.old')),
2347
- React.createElement('option', { value: 'new' }, t('order.new')),
2551
+ h('option', { value: 'manual' }, t('order.manual')),
2552
+ h('option', { value: 'old' }, t('order.old')),
2553
+ h('option', { value: 'new' }, t('order.new')),
2348
2554
  ),
2349
- React.createElement('button', {
2555
+ h('button', {
2350
2556
  type: 'button', className: metrics ? 'dkb-save' : 'dkb-discard',
2351
2557
  onClick: async () => {
2352
2558
  if (metrics) { setMetrics(null); return }
@@ -2356,7 +2562,7 @@ window.__ModuleLoader__.load({
2356
2562
  } catch (e) { setError(t(e.key || 'error.unknown')) }
2357
2563
  },
2358
2564
  }, t('board.metrics')),
2359
- React.createElement('button', {
2565
+ h('button', {
2360
2566
  type: 'button', className: archive ? 'dkb-save' : 'dkb-discard',
2361
2567
  onClick: async () => {
2362
2568
  if (archive) { setArchive(null); return }
@@ -2367,8 +2573,7 @@ window.__ModuleLoader__.load({
2367
2573
  } catch (e) { setError(t(e.key || 'error.unknown')) }
2368
2574
  },
2369
2575
  }, t('board.archive')),
2370
- // Экспорт скачивает полный снимок доски; импорт восстанавливает его.
2371
- React.createElement('button', {
2576
+ h('button', {
2372
2577
  type: 'button', className: 'dkb-discard',
2373
2578
  title: t('board.exportHint'),
2374
2579
  onClick: async () => {
@@ -2384,9 +2589,9 @@ window.__ModuleLoader__.load({
2384
2589
  } catch (e) { setError(t(e.key || 'error.unknown')) }
2385
2590
  },
2386
2591
  }, t('board.export')),
2387
- React.createElement('label', { className: 'dkb-discard dkb-import' },
2592
+ h('label', { className: 'dkb-discard dkb-import' },
2388
2593
  t('board.import'),
2389
- React.createElement('input', {
2594
+ h('input', {
2390
2595
  type: 'file', accept: 'application/json,.json', style: { display: 'none' },
2391
2596
  onChange: async (e) => {
2392
2597
  const file = e.target.files && e.target.files[0]
@@ -2400,42 +2605,34 @@ window.__ModuleLoader__.load({
2400
2605
  } catch { setError(t('error.badImport')) }
2401
2606
  },
2402
2607
  })),
2403
- // «Показано 12 из 81» появляется, только когда что-то отобрано или
2404
- // введён поиск: на нетронутой доске эта строка была бы шумом.
2405
2608
  anySelected(filters) || query.trim() !== ''
2406
- ? React.createElement('span', { className: 'dkb-shown' }, t('board.shown', {
2609
+ ? h('span', { className: 'dkb-shown' }, t('board.shown', {
2407
2610
  shown: state.tasks.filter((x) => matchesFilters(x, filters) && matchesQuery(x, query)).length,
2408
2611
  total: state.tasks.length,
2409
2612
  }))
2410
2613
  : null,
2411
2614
  anySelected(filters)
2412
- ? React.createElement('button', {
2615
+ ? h('button', {
2413
2616
  type: 'button', className: 'dkb-discard',
2414
2617
  onClick: () => { setFilters({}); setOpenFacet('') },
2415
2618
  }, t('board.clearFilters'))
2416
2619
  : null,
2417
- React.createElement('input', {
2620
+ h('input', {
2418
2621
  className: 'dkb-search', value: query, placeholder: t('board.search'),
2419
2622
  onChange: (e) => setQuery(e.target.value),
2420
2623
  }),
2421
2624
  (() => {
2422
- // Пока запрос в пути, состояние на доске ещё вчерашнее: оно приедет
2423
- // только со следующим ответом. Поэтому ход показываем сами, а не
2424
- // ждём, когда сервер расскажет о нём задним числом.
2425
2625
  const line = syncing
2426
2626
  ? { tone: 'idle', text: t('sync.running'), title: '' }
2427
2627
  : syncLine(state.sync, state.now, t)
2428
- return React.createElement('span', {
2628
+ return h('span', {
2429
2629
  className: 'dkb-sync' + (line.tone === 'bad' ? ' dkb-syncBad' : ''),
2430
2630
  title: line.title,
2431
2631
  }, line.text)
2432
2632
  })(),
2433
- React.createElement('button', {
2633
+ h('button', {
2434
2634
  type: 'button', className: 'dkb-discard', disabled: syncing,
2435
2635
  onClick: async () => {
2436
- // Кнопка гаснет на время прохода: нажать её второй раз значило бы
2437
- // попросить сверку, которая и так идёт, а сервер ответил бы
2438
- // «пропущено» — и человек решил бы, что ничего не случилось.
2439
2636
  setSyncing(true)
2440
2637
  try {
2441
2638
  await api('/sync', { method: 'POST', body: '{}' })
@@ -2447,21 +2644,32 @@ window.__ModuleLoader__.load({
2447
2644
  }
2448
2645
  },
2449
2646
  }, syncing ? t('board.syncing') : t('board.sync')),
2450
- React.createElement('button', { type: 'button', className: 'dkb-discard', onClick: () => load(board) }, t('board.refresh')),
2451
- // Полоса выбора появляется, только когда что-то отмечено: на пустом
2452
- // выборе она была бы мебелью.
2647
+ h('button', { type: 'button', className: 'dkb-discard', onClick: () => load(board) }, t('board.refresh')),
2648
+ h('div', { className: 'dkb-quickFilterBar' },
2649
+ [
2650
+ { id: 'all', label: t('filter.all') },
2651
+ { id: 'active', label: t('filter.active') },
2652
+ { id: 'review', label: t('filter.review') },
2653
+ { id: 'epics', label: t('filter.epics') },
2654
+ { id: 'stale', label: t('filter.stale') },
2655
+ ].map((chip) => h('button', {
2656
+ type: 'button',
2657
+ key: chip.id,
2658
+ className: 'dkb-filterChip' + (quickFilter === chip.id ? ' dkb-filterChipActive' : ''),
2659
+ onClick: () => setQuickFilter(chip.id),
2660
+ }, chip.label))),
2453
2661
  Object.keys(picked).length > 0
2454
- ? React.createElement('span', { className: 'dkb-shown' },
2662
+ ? h('span', { className: 'dkb-shown' },
2455
2663
  t('board.pickedCount', { n: Object.keys(picked).length }))
2456
2664
  : null,
2457
2665
  Object.keys(picked).length > 0
2458
- ? React.createElement('button', {
2666
+ ? h('button', {
2459
2667
  type: 'button', className: 'dkb-discard',
2460
2668
  onClick: () => setPicked({}),
2461
2669
  }, t('board.pickNone'))
2462
2670
  : null,
2463
2671
  Object.keys(picked).length > 0
2464
- ? React.createElement('button', {
2672
+ ? h('button', {
2465
2673
  type: 'button', className: 'dkb-save',
2466
2674
  onClick: () => setDialog({
2467
2675
  mode: 'batch',
@@ -2471,15 +2679,13 @@ window.__ModuleLoader__.load({
2471
2679
  }, t('board.pickStart'))
2472
2680
  : null,
2473
2681
  Object.keys(picked).length > 0
2474
- ? React.createElement('button', {
2682
+ ? h('button', {
2475
2683
  type: 'button', className: 'dkb-discard',
2476
2684
  onClick: async () => {
2477
2685
  const ids = Object.keys(picked)
2478
2686
  const out = await applyToEach(ids, (id) => api(
2479
2687
  '/task/' + encodeURIComponent(id) + '/archive', { method: 'POST', body: '{}' },
2480
2688
  ))
2481
- // Часть задач архивировать нельзя (архив только из «Выполнено»),
2482
- // и число отказов называется вслух.
2483
2689
  if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2484
2690
  else setPicked({})
2485
2691
  await load(board)
@@ -2487,7 +2693,7 @@ window.__ModuleLoader__.load({
2487
2693
  }, t('board.pickArchive'))
2488
2694
  : null,
2489
2695
  Object.keys(picked).length > 0
2490
- ? React.createElement('button', {
2696
+ ? h('button', {
2491
2697
  type: 'button', className: 'dkb-discard',
2492
2698
  onClick: async () => {
2493
2699
  const ids = Object.keys(picked)
@@ -2501,14 +2707,12 @@ window.__ModuleLoader__.load({
2501
2707
  }, t('board.pickUnqueue'))
2502
2708
  : null,
2503
2709
  Object.keys(picked).length > 0
2504
- ? React.createElement('select', {
2505
- className: 'dkb-input', style: { width: 'auto' }, value: '',
2710
+ ? h('select', {
2711
+ className: 'dkb-input',
2506
2712
  onChange: async (e) => {
2507
2713
  const column = e.target.value
2508
- if (column === '') return
2714
+ if (!column) return
2509
2715
  const ids = Object.keys(picked)
2510
- // Групповой перенос — та же команда агенту, что и одиночный:
2511
- // маршрут один, и последствия у него те же.
2512
2716
  const out = await applyToEach(ids, (id) => api(
2513
2717
  '/task/' + encodeURIComponent(id) + '/move',
2514
2718
  { method: 'POST', body: JSON.stringify({ column }) },
@@ -2518,36 +2722,33 @@ window.__ModuleLoader__.load({
2518
2722
  await load(board)
2519
2723
  },
2520
2724
  },
2521
- React.createElement('option', { value: '' }, t('board.pickMove')),
2522
- (state.columns || []).map((c) => React.createElement('option', {
2725
+ h('option', { value: '' }, t('board.pickMove')),
2726
+ (state.columns || []).map((c) => h('option', {
2523
2727
  value: c.id, key: c.id,
2524
2728
  }, columnTitle(c, t))),
2525
2729
  )
2526
2730
  : null,
2527
- React.createElement('button', { type: 'button', className: 'dkb-save', onClick: () => setDialog({ mode: 'own', project: false, repo: '', newRepo: '', title: '', full: '', repos: null, issues: null, error: '' }) }, t('board.newTask')),
2731
+ 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')),
2528
2732
  )
2529
2733
 
2530
- // Счётчик колонки считает ВСЕ карточки, а не найденные: иначе поиск
2531
- // сделал бы вид, что предел колонки соблюдён.
2532
2734
  const packs = packInfo(state.tasks)
2533
- const columns = React.createElement('div', { className: 'dkb-cols' },
2735
+ const columns = h('div', { className: 'dkb-cols' },
2534
2736
  state.columns.map((col) => {
2535
- // Счётчик колонки считает ОТОБРАННОЕ, а поиск — нет.
2536
- //
2537
- // Разница осмысленная, а не недосмотр: поиск временный, и предел
2538
- // «3 из 3» при найденной одной карточке врал бы. Отбор постоянный, и
2539
- // на отборе по одному проекту «в работе 3 из 3» должно значить «по
2540
- // этому проекту» — иначе предел ни о чём.
2541
2737
  const all = tasksOf(state.tasks, col.id).filter((task) => matchesFilters(task, filters))
2542
2738
  const items = sortTasks(
2543
- all.filter((task) => matchesQuery(task, query))
2739
+ all.filter((task) => matchesQuery(task, debouncedQuery))
2544
2740
  .filter((task) => !onlyWaiting || task.state === 'waiting')
2545
2741
  .filter((task) => !onlyOverdue || task.overdue === true)
2546
- .filter((task) => !onlyMine || task.assignee === me),
2742
+ .filter((task) => !onlyMine || task.assignee === me)
2743
+ .filter((task) => {
2744
+ if (quickFilter === 'active') return task.column === 'in-progress'
2745
+ if (quickFilter === 'review') return task.column === 'review'
2746
+ if (quickFilter === 'epics') return task.isEpic === true
2747
+ if (quickFilter === 'stale') return task.stale === true
2748
+ return true
2749
+ }),
2547
2750
  order,
2548
2751
  )
2549
- // Ключ группы решает переключатель: заголовки, сворачивание и
2550
- // счётчики остаются те же, меняется только по чему делим.
2551
2752
  const groupKeyOf = (task) => (grouping === 'assignee'
2552
2753
  ? (task.assignee || '')
2553
2754
  : (task.repo || ''))
@@ -2556,7 +2757,7 @@ window.__ModuleLoader__.load({
2556
2757
  : items
2557
2758
  const limit = col.limit
2558
2759
  const overLimit = limit !== undefined && limit !== null && all.length > limit
2559
- const slot = (index) => React.createElement('div', {
2760
+ const slot = (index) => h('div', {
2560
2761
  className: 'dkb-slot' + (dropAt && dropAt.column === col.id && dropAt.index === index ? ' dkb-slotOn' : ''),
2561
2762
  key: 's' + index,
2562
2763
  onDragOver: (e) => {
@@ -2569,8 +2770,6 @@ window.__ModuleLoader__.load({
2569
2770
 
2570
2771
  const rows = []
2571
2772
  rows.push(slot(0))
2572
- // Заголовок рисуем при смене проекта. Задачи приходят уже
2573
- // разложенными, поэтому группы идут подряд и разрываться не могут.
2574
2773
  let group = null
2575
2774
  const sizes = new Map()
2576
2775
  for (const x of ordered) sizes.set(groupKeyOf(x), (sizes.get(groupKeyOf(x)) || 0) + 1)
@@ -2581,20 +2780,18 @@ window.__ModuleLoader__.load({
2581
2780
  if (manyGroups && repo !== group) {
2582
2781
  group = repo
2583
2782
  const shut = groups[col.id + '/' + repo] === true
2584
- rows.push(React.createElement('button', {
2783
+ rows.push(h('button', {
2585
2784
  type: 'button', className: 'dkb-groupHead', key: 'g' + repo,
2586
2785
  'aria-expanded': !shut,
2587
2786
  onClick: () => setGroups((cur) => Object.assign({}, cur, { [col.id + '/' + repo]: !shut })),
2588
2787
  },
2589
- React.createElement('span', null,
2788
+ h('span', null,
2590
2789
  repo || t(grouping === 'assignee' ? 'group.nobody' : 'board.noRepo')),
2591
- // Счётчик виден и у свёрнутой: группа с работой, выглядящая
2592
- // пустой, — то же зло, что и свёрнутая колонка без числа.
2593
- React.createElement('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
2790
+ h('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
2594
2791
  ))
2595
2792
  }
2596
2793
  if (manyGroups && groups[col.id + '/' + (task.repo || '')] === true) return
2597
- rows.push(React.createElement(TaskCard, {
2794
+ rows.push(h(TaskCard, {
2598
2795
  key: task.id, task, t, now: state.now,
2599
2796
  columns: (state.columns || []).map((c) => c.id),
2600
2797
  menuOpen: cardMenu,
@@ -2610,9 +2807,6 @@ window.__ModuleLoader__.load({
2610
2807
  sameSession: hoverSession !== '' && task.sessionId === hoverSession,
2611
2808
  onHover: setHoverSession,
2612
2809
  onRevive: async (x) => {
2613
- // Переиспользуем обычный запуск: он сам пробует возобновить
2614
- // прежнюю сессию и только потом заводит новую. Здесь важно
2615
- // сказать, что именно вышло.
2616
2810
  try {
2617
2811
  const out = await api('/task/' + encodeURIComponent(x.id) + '/start', {
2618
2812
  method: 'POST', body: JSON.stringify({ revive: true }),
@@ -2624,8 +2818,6 @@ window.__ModuleLoader__.load({
2624
2818
  onStop: async (x) => {
2625
2819
  try {
2626
2820
  const out = await api('/task/' + encodeURIComponent(x.id) + '/stop', { method: 'POST', body: '{}' })
2627
- // «Остановлено» и «он и так стоял» — разные ответы, и второй
2628
- // нельзя выдавать за первый.
2629
2821
  if (out && out.acted !== 'stopped') setError(t('stop.' + out.acted))
2630
2822
  await load(board)
2631
2823
  } catch (e) { setError(t(e.key || 'error.unknown')) }
@@ -2635,8 +2827,6 @@ window.__ModuleLoader__.load({
2635
2827
  try {
2636
2828
  const out = await api('/sessions')
2637
2829
  const live = (out && out.sessions) || []
2638
- // Живых сессий нет — ставить некуда, и сказать об этом надо
2639
- // сразу, а не показывать пустой список.
2640
2830
  if (live.length === 0) { setError(t('error.no-live-sessions')); return }
2641
2831
  setDialog({ mode: 'queue', task: x, sessions: live, sessionId: live[0].sessionId, error: '' })
2642
2832
  } catch (e) { setError(t(e.key || 'error.unknown')) }
@@ -2666,37 +2856,33 @@ window.__ModuleLoader__.load({
2666
2856
  ? all.length + ' / ' + t('board.limit', { n: limit })
2667
2857
  : String(all.length)
2668
2858
 
2669
- return React.createElement('div', {
2859
+ return h('div', {
2670
2860
  className: 'dkb-col'
2671
2861
  + (overColumn === col.id ? ' dkb-colOver' : '')
2672
2862
  + (shut ? ' dkb-colShut' : ''),
2673
2863
  key: col.id,
2674
- // Свёрнутая колонка остаётся целью переноса: иначе карточку было бы
2675
- // некуда положить, не развернув её сперва.
2676
2864
  onDragOver: (e) => { e.preventDefault(); setOverColumn(col.id) },
2677
2865
  onDragLeave: () => { setOverColumn(''); setDropAt(null) },
2678
2866
  onDrop: (e) => { e.preventDefault(); drop(col.id, items.length) },
2679
2867
  },
2680
- React.createElement('button', {
2868
+ h('button', {
2681
2869
  type: 'button', className: 'dkb-colHead',
2682
2870
  'aria-expanded': !shut,
2683
2871
  title: t(shut ? 'board.expand' : 'board.collapse'),
2684
2872
  onClick: toggle,
2685
2873
  },
2686
- React.createElement('span', { className: 'dkb-dot', 'data-col': col.id }),
2687
- React.createElement('span', { className: 'dkb-colName' }, columnTitle(col, t)),
2688
- // Число карточек видно и в свёрнутом виде: колонка с работой,
2689
- // выглядящая пустой, — худшее, что может сделать сворачивание.
2690
- React.createElement('span', {
2874
+ h('span', { className: 'dkb-dot', 'data-col': col.id }),
2875
+ h('span', { className: 'dkb-colName' }, columnTitle(col, t)),
2876
+ h('span', {
2691
2877
  className: 'dkb-count' + (overLimit ? ' dkb-countOver' : ''),
2692
2878
  'data-col': col.id,
2693
2879
  }, count),
2694
2880
  ),
2695
- shut ? null : React.createElement('div', { className: 'dkb-list' },
2881
+ shut ? null : h('div', { className: 'dkb-list' },
2696
2882
  all.length === 0
2697
- ? React.createElement('div', { className: 'dkb-empty' }, t('board.empty'))
2883
+ ? h('div', { className: 'dkb-empty' }, t('board.empty'))
2698
2884
  : items.length === 0
2699
- ? React.createElement('div', { className: 'dkb-empty' }, t('board.noMatch'))
2885
+ ? h('div', { className: 'dkb-empty' }, t('board.noMatch'))
2700
2886
  : null,
2701
2887
  rows,
2702
2888
  ),
@@ -2704,24 +2890,18 @@ window.__ModuleLoader__.load({
2704
2890
  }),
2705
2891
  )
2706
2892
 
2707
- // Окно, а не панель справа: в 420 пикселей помещались описание и журнал,
2708
- // но не план с отметками. Живёт внутри экрана доски — сама доска лежит в
2709
- // колонке разговора, и выход за её пределы наложился бы на чужие части
2710
- // оболочки.
2711
2893
  const panel = openTask
2712
- ? React.createElement('div', {
2894
+ ? h('div', {
2713
2895
  className: 'dkb-modal',
2714
2896
  onClick: () => setOpenTask(null),
2715
- }, React.createElement('div', {
2897
+ }, h('div', {
2716
2898
  className: 'dkb-panel dkb-panelWide',
2717
2899
  role: 'dialog', 'aria-modal': 'true',
2718
2900
  onClick: (e) => e.stopPropagation(),
2719
2901
  },
2720
- // Шапка: чьё это, что за работа, и лишь потом название. Тот же
2721
- // порядок, что на карточке, — глаз не должен переучиваться.
2722
- React.createElement('div', { className: 'dkb-row' },
2723
- React.createElement('label', { className: 'dkb-label' }, t('reasoning.label')),
2724
- React.createElement('select', {
2902
+ h('div', { className: 'dkb-row' },
2903
+ h('label', { className: 'dkb-label' }, t('reasoning.label')),
2904
+ h('select', {
2725
2905
  className: 'dkb-select',
2726
2906
  value: openTask.reasoningEffort || '',
2727
2907
  onChange: async (e) => {
@@ -2732,53 +2912,51 @@ window.__ModuleLoader__.load({
2732
2912
  method: 'POST',
2733
2913
  body: JSON.stringify({ reasoningEffort: val }),
2734
2914
  })
2735
- } catch {}
2915
+ } catch { /* update reasoning effort best-effort */ }
2736
2916
  },
2737
2917
  },
2738
- React.createElement('option', { value: '' }, t('reasoning.default')),
2739
- React.createElement('option', { value: 'low' }, t('reasoning.low')),
2740
- React.createElement('option', { value: 'medium' }, t('reasoning.medium')),
2741
- React.createElement('option', { value: 'high' }, t('reasoning.high')),
2918
+ h('option', { value: '' }, t('reasoning.default')),
2919
+ h('option', { value: 'low' }, t('reasoning.low')),
2920
+ h('option', { value: 'medium' }, t('reasoning.medium')),
2921
+ h('option', { value: 'high' }, t('reasoning.high')),
2742
2922
  ),
2743
2923
  ),
2744
- React.createElement('div', { className: 'dkb-row' },
2745
- React.createElement('div', { className: 'dkb-taskHead', style: { flex: 1 } },
2924
+ h('div', { className: 'dkb-row' },
2925
+ h('div', { className: 'dkb-taskHead', style: { flex: 1 } },
2746
2926
  taskRef(openTask, t),
2747
- (openTask.labels || []).map((l) => React.createElement('span', {
2927
+ (openTask.labels || []).map((l) => h('span', {
2748
2928
  className: 'dkb-tag', key: l, style: tagStyle(openTask, l),
2749
2929
  }, l))),
2750
- React.createElement('button', {
2930
+ h('button', {
2751
2931
  type: 'button', className: 'dkb-discard', onClick: () => setOpenTask(null),
2752
2932
  }, t('panel.close')),
2753
2933
  ),
2754
- React.createElement('div', { className: 'dkb-title' }, openTask.title),
2755
- React.createElement('div', { className: 'dkb-note' },
2934
+ h('div', { className: 'dkb-title' }, openTask.title),
2935
+ h('div', { className: 'dkb-note' },
2756
2936
  t('panel.created', { ago: agoText(state.now, openTask.createdAt, t) })
2757
2937
  + (openTask.assignee ? ' · ' + t('panel.assignee', { who: openTask.assignee }) : '')
2758
- // Автор стоит рядом с датой заведения: это одно событие — кто и
2759
- // когда завёл, — и разносить его по разным углам незачем.
2760
2938
  + (openTask.author ? ' · ' + t('panel.author', { who: openTask.author }) : ''),
2761
2939
  ' · ',
2762
2940
  t('panel.updated', { ago: agoText(state.now, openTask.updatedAt, t) }),
2763
2941
  openTask.issueUrl
2764
- ? React.createElement('a', {
2942
+ ? h('a', {
2765
2943
  className: 'dkb-issueLink', href: openTask.issueUrl,
2766
2944
  target: '_blank', rel: 'noreferrer noopener',
2767
2945
  }, ' · ' + t('panel.issue') + ' ↗')
2768
2946
  : null),
2769
2947
  openTask.worktree
2770
- ? React.createElement('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
2948
+ ? h('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
2771
2949
  t('panel.worktree', { path: openTask.worktree })
2772
2950
  + (openTask.branch ? ' (' + openTask.branch + ')' : ''))
2773
2951
  : null,
2774
2952
  openTask.permission && !openTask.permissionConfirmedAt
2775
- ? React.createElement('div', {
2953
+ ? h('div', {
2776
2954
  className: 'dkb-warnBox',
2777
2955
  style: { margin: '4px 0', padding: 10, background: 'var(--dsw-alias-state-warn-bg, #fff3cd)', border: '1px solid var(--dsw-alias-state-warn-border, #ffeeba)', borderRadius: 6 },
2778
2956
  },
2779
- React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
2957
+ h('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
2780
2958
  t('task.permissionGateWarning', { permission: openTask.permission })),
2781
- React.createElement('button', {
2959
+ h('button', {
2782
2960
  type: 'button', className: 'dkb-save',
2783
2961
  onClick: async () => {
2784
2962
  try {
@@ -2790,59 +2968,53 @@ window.__ModuleLoader__.load({
2790
2968
  }, t('task.confirmPermission')),
2791
2969
  )
2792
2970
  : null,
2793
- // Пустое тело — отсутствие раздела, а не пустой раздел с заголовком.
2794
2971
  body === null
2795
- ? React.createElement('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
2972
+ ? h('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
2796
2973
  : body.length === 0
2797
2974
  ? null
2798
- : React.createElement('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
2799
- // Приоритет и метки — локальное свойство доски: правятся здесь, а не
2800
- // в Gitea. У задач из issue метки приезжают сверкой, но приоритет
2801
- // всегда свой.
2802
- React.createElement('div', { className: 'dkb-edit' },
2803
- React.createElement('div', { className: 'dkb-field' },
2804
- React.createElement('span', { className: 'dkb-label' }, t('panel.priority')),
2805
- React.createElement('select', {
2975
+ : h('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
2976
+ h('div', { className: 'dkb-edit' },
2977
+ h('div', { className: 'dkb-field' },
2978
+ h('span', { className: 'dkb-label' }, t('panel.priority')),
2979
+ h('select', {
2806
2980
  className: 'dkb-input', value: editPriority,
2807
2981
  onChange: (e) => setEditPriority(e.target.value),
2808
2982
  },
2809
- React.createElement('option', { value: '' }, t('panel.priorityNone')),
2810
- ['high', 'medium', 'low'].map((p) => React.createElement('option', { value: p, key: p }, t('priority.' + p)))),
2983
+ h('option', { value: '' }, t('panel.priorityNone')),
2984
+ ['high', 'medium', 'low'].map((p) => h('option', { value: p, key: p }, t('priority.' + p)))),
2811
2985
  ),
2812
- React.createElement('div', { className: 'dkb-field' },
2813
- React.createElement('span', { className: 'dkb-label' }, t('panel.labels')),
2814
- React.createElement('input', {
2986
+ h('div', { className: 'dkb-field' },
2987
+ h('span', { className: 'dkb-label' }, t('panel.labels')),
2988
+ h('input', {
2815
2989
  className: 'dkb-input', value: editLabels,
2816
2990
  placeholder: t('dialog.labelsHint'),
2817
2991
  onChange: (e) => setEditLabels(e.target.value),
2818
2992
  }),
2819
2993
  ),
2820
- React.createElement('div', { className: 'dkb-field' },
2821
- React.createElement('span', { className: 'dkb-label' }, t('panel.due')),
2822
- React.createElement('input', {
2994
+ h('div', { className: 'dkb-field' },
2995
+ h('span', { className: 'dkb-label' }, t('panel.due')),
2996
+ h('input', {
2823
2997
  type: 'date', className: 'dkb-input', value: editDue,
2824
2998
  onChange: (e) => setEditDue(e.target.value),
2825
2999
  }),
2826
3000
  ),
2827
- React.createElement('div', { className: 'dkb-field' },
2828
- React.createElement('span', { className: 'dkb-label' }, t('task.cron')),
2829
- React.createElement('input', {
3001
+ h('div', { className: 'dkb-field' },
3002
+ h('span', { className: 'dkb-label' }, t('task.cron')),
3003
+ h('input', {
2830
3004
  className: 'dkb-input', value: editCron,
2831
3005
  placeholder: '* * * * *',
2832
3006
  onChange: (e) => setEditCron(e.target.value),
2833
3007
  }),
2834
3008
  openTask.nextRunAt
2835
- ? React.createElement('p', { className: 'dkb-hint' },
3009
+ ? h('p', { className: 'dkb-hint' },
2836
3010
  t('task.nextRun', { date: new Date(openTask.nextRunAt).toLocaleString() }))
2837
3011
  : null,
2838
3012
  ),
2839
- React.createElement('button', {
3013
+ h('button', {
2840
3014
  type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
2841
3015
  onClick: async () => {
2842
3016
  try {
2843
3017
  const labels = editLabels.split(',').map((s) => s.trim()).filter(Boolean)
2844
- // Пустая дата снимает дедлайн (0); заполненная — полночь
2845
- // этого дня по местному времени.
2846
3018
  const dueAt = editDue === ''
2847
3019
  ? 0
2848
3020
  : new Date(editDue + 'T00:00:00').getTime()
@@ -2857,9 +3029,9 @@ window.__ModuleLoader__.load({
2857
3029
  }, t('panel.saveEdit')),
2858
3030
  ),
2859
3031
  openTask.progressDump
2860
- ? React.createElement('div', null,
2861
- React.createElement('div', { className: 'dkb-group' }, t('task.progressDump')),
2862
- React.createElement('pre', {
3032
+ ? h('div', null,
3033
+ h('div', { className: 'dkb-group' }, t('task.progressDump')),
3034
+ h('pre', {
2863
3035
  style: {
2864
3036
  background: 'var(--dsw-alias-bg-layer-1, #f6f8fa)', padding: 8, borderRadius: 6, fontSize: 12,
2865
3037
  whiteSpace: 'pre-wrap', maxHeight: 150, overflowY: 'auto', margin: 0,
@@ -2867,21 +3039,155 @@ window.__ModuleLoader__.load({
2867
3039
  }, openTask.progressDump),
2868
3040
  )
2869
3041
  : null,
3042
+ 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 } },
3043
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
3044
+ h('span', { className: 'dkb-group', style: { margin: 0 } },
3045
+ '👑 ' + t('epic.subtasks') + (subtasks.length > 0 ? ` (${subtasks.filter(s => s.column === 'done').length}/${subtasks.length})` : '')
3046
+ ),
3047
+ h('button', {
3048
+ type: 'button', className: 'dkb-discard',
3049
+ style: { padding: '2px 8px', fontSize: 11 },
3050
+ onClick: () => setTriageOpen(!triageOpen),
3051
+ }, triageOpen ? t('dialog.cancel') : '⚡ ' + t('epic.triage')),
3052
+ ),
3053
+ 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)' } },
3054
+ h('span', { style: { fontSize: 12, fontWeight: 600 } }, t('epic.triageTitle')),
3055
+ h('span', { className: 'dkb-hint', style: { margin: 0 } }, t('epic.triageHelp')),
3056
+ h('textarea', {
3057
+ className: 'dkb-area',
3058
+ rows: 4,
3059
+ placeholder: 'Subtask 1\nSubtask 2\nSubtask 3...',
3060
+ value: triageText,
3061
+ onChange: (e) => setTriageText(e.target.value),
3062
+ }),
3063
+ openTask.owner && openTask.repo ? h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer' } },
3064
+ h('input', {
3065
+ type: 'checkbox',
3066
+ checked: triageGitea,
3067
+ onChange: (e) => setTriageGitea(e.target.checked),
3068
+ }),
3069
+ h('span', null, t('epic.createGiteaIssues'))
3070
+ ) : null,
3071
+ h('div', { style: { display: 'flex', gap: 6 } },
3072
+ h('button', {
3073
+ type: 'button', className: 'dkb-save',
3074
+ disabled: triageSubmitting || !triageText.trim(),
3075
+ onClick: async () => {
3076
+ const lines = triageText.split('\n').map(s => s.trim()).filter(Boolean)
3077
+ if (lines.length === 0) return
3078
+ setTriageSubmitting(true)
3079
+ try {
3080
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/triage', {
3081
+ method: 'POST',
3082
+ body: JSON.stringify({
3083
+ subtasks: lines.map(title => ({ title })),
3084
+ createGiteaIssues: triageGitea,
3085
+ }),
3086
+ })
3087
+ if (out && out.subtasks) setSubtasks(out.subtasks)
3088
+ if (out && out.epic) setOpenTask(out.epic)
3089
+ setTriageText('')
3090
+ setTriageOpen(false)
3091
+ await load(board)
3092
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
3093
+ finally { setTriageSubmitting(false) }
3094
+ },
3095
+ }, triageSubmitting ? '...' : t('epic.triageSubmit')),
3096
+ h('button', {
3097
+ type: 'button', className: 'dkb-discard',
3098
+ onClick: () => setTriageOpen(false),
3099
+ }, t('dialog.cancel')),
3100
+ )
3101
+ ) : null,
3102
+ subtasks.length > 0 ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
3103
+ subtasks.map((st) => h('li', {
3104
+ key: st.id,
3105
+ style: {
3106
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
3107
+ padding: '4px 8px', background: 'var(--dsw-alias-bg-layer-2)',
3108
+ borderRadius: 4, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
3109
+ },
3110
+ },
3111
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flex: 1 } },
3112
+ h('span', { style: { color: st.column === 'done' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-tertiary)' } },
3113
+ st.column === 'done' ? '✓' : '•'
3114
+ ),
3115
+ h('span', { style: { textDecoration: st.column === 'done' ? 'line-through' : 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, st.title),
3116
+ st.issueNumber ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, '#' + st.issueNumber) : null,
3117
+ ),
3118
+ h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('column.' + st.column)),
3119
+ ))
3120
+ ) : h('p', { className: 'dkb-hint', style: { margin: '4px 0' } }, t('epic.noSubtasks')),
3121
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 6 } },
3122
+ h('input', {
3123
+ className: 'dkb-input',
3124
+ style: { flex: 1, fontSize: 12 },
3125
+ placeholder: t('epic.addSubtask'),
3126
+ value: newSubtaskTitle,
3127
+ onChange: (e) => setNewSubtaskTitle(e.target.value),
3128
+ onKeyDown: async (e) => {
3129
+ if (e.key === 'Enter' && newSubtaskTitle.trim()) {
3130
+ e.preventDefault()
3131
+ const title = newSubtaskTitle.trim()
3132
+ setNewSubtaskTitle('')
3133
+ try {
3134
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
3135
+ method: 'POST',
3136
+ body: JSON.stringify({ title }),
3137
+ })
3138
+ if (out && out.subtask) {
3139
+ setSubtasks(prev => [...prev, out.subtask])
3140
+ await load(board)
3141
+ }
3142
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
3143
+ }
3144
+ },
3145
+ }),
3146
+ h('button', {
3147
+ type: 'button', className: 'dkb-save',
3148
+ style: { padding: '3px 10px', fontSize: 12 },
3149
+ onClick: async () => {
3150
+ const title = newSubtaskTitle.trim()
3151
+ if (!title) return
3152
+ setNewSubtaskTitle('')
3153
+ try {
3154
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
3155
+ method: 'POST',
3156
+ body: JSON.stringify({ title }),
3157
+ })
3158
+ if (out && out.subtask) {
3159
+ setSubtasks(prev => [...prev, out.subtask])
3160
+ await load(board)
3161
+ }
3162
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
3163
+ },
3164
+ }, '+'),
3165
+ )
3166
+ ),
3167
+ openTask.sessionId ? h('div', { style: { margin: '6px 0' } },
3168
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3169
+ h('span', null, '🖥️ ' + t('console.tab')),
3170
+ h('span', { className: 'dkb-tag', style: { fontSize: 10, fontFamily: 'monospace' } }, openTask.sessionId.slice(0, 16) + '...'),
3171
+ ),
3172
+ h('pre', { className: 'dkb-consolePre' },
3173
+ 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'))
3174
+ )
3175
+ ) : null,
2870
3176
  openTask.runs && openTask.runs.length
2871
- ? React.createElement('div', null,
2872
- React.createElement('div', { className: 'dkb-group' }, t('task.runs')),
2873
- React.createElement('ul', { className: 'dkb-log' }, openTask.runs.map((r) => React.createElement('li', {
3177
+ ? h('div', null,
3178
+ h('div', { className: 'dkb-group' }, t('task.runs')),
3179
+ h('ul', { className: 'dkb-log' }, openTask.runs.map((r) => h('li', {
2874
3180
  className: 'dkb-logRow', key: r.id,
2875
3181
  style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
2876
3182
  },
2877
- React.createElement('span', null,
3183
+ h('span', null,
2878
3184
  (r.result === 'succeeded' ? '✓ ' : r.result === 'failed' ? '✕ ' : '▸ ')
2879
3185
  + new Date(r.startedAt).toLocaleTimeString()
2880
3186
  + (r.model ? ' · ' + r.model : '')
2881
3187
  + (r.error ? ' · ' + r.error : '')
2882
3188
  ),
2883
3189
  r.sessionId
2884
- ? React.createElement('button', {
3190
+ ? h('button', {
2885
3191
  type: 'button',
2886
3192
  className: 'dkb-discard',
2887
3193
  style: { padding: '2px 6px', fontSize: 11 },
@@ -2897,9 +3203,7 @@ window.__ModuleLoader__.load({
2897
3203
  : null,
2898
3204
  ))))
2899
3205
  : null,
2900
- // Взять и отдать — одна кнопка в двух состояниях: держать рядом
2901
- // «взять» и «отказаться» значит предлагать бессмысленное действие.
2902
- React.createElement('button', {
3206
+ h('button', {
2903
3207
  type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
2904
3208
  onClick: async () => {
2905
3209
  const mine = openTask.assignee !== '' && openTask.assignee !== undefined
@@ -2914,12 +3218,9 @@ window.__ModuleLoader__.load({
2914
3218
  },
2915
3219
  }, openTask.assignee ? t('panel.assignDrop') : t('panel.assignMe')),
2916
3220
  openTask.sessionId
2917
- ? React.createElement('button', {
3221
+ ? h('button', {
2918
3222
  type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
2919
3223
  onClick: () => {
2920
- // Тот же путь, что и сразу после запуска: задача, идущая
2921
- // вторые сутки, иначе ищется в общем списке по имени вида
2922
- // kanban-9e84cae6-…
2923
3224
  if (!openSession(props.ctx, openTask.sessionId)) {
2924
3225
  setError(t('error.sessionNotOpened', { id: openTask.sessionId }))
2925
3226
  return
@@ -2930,31 +3231,31 @@ window.__ModuleLoader__.load({
2930
3231
  }, t('panel.openChat'))
2931
3232
  : null,
2932
3233
  log.length
2933
- ? React.createElement('div', null,
2934
- React.createElement('div', { className: 'dkb-group' }, t('panel.log')),
2935
- React.createElement('ul', { className: 'dkb-log' }, log.map((row) => React.createElement('li', {
3234
+ ? h('div', null,
3235
+ h('div', { className: 'dkb-group' }, t('panel.log')),
3236
+ h('ul', { className: 'dkb-log' }, log.map((row) => h('li', {
2936
3237
  className: 'dkb-logRow', key: row.id,
2937
3238
  }, `${t('column.' + row.toCol)} ← ${row.source}${row.detail ? ' · ' + row.detail : ''}`))))
2938
3239
  : null,
2939
3240
  openTask.plan && openTask.plan.items && openTask.plan.items.length
2940
- ? React.createElement('div', null,
2941
- React.createElement('div', { className: 'dkb-group' }, t('plan.title')),
2942
- React.createElement('p', { className: 'dkb-hint' }, planLine(openTask, t)),
2943
- React.createElement('ul', { className: 'dkb-planList' },
2944
- openTask.plan.items.map((item, i) => React.createElement('li', {
3241
+ ? h('div', null,
3242
+ h('div', { className: 'dkb-group' }, t('plan.title')),
3243
+ h('p', { className: 'dkb-hint' }, planLine(openTask, t)),
3244
+ h('ul', { className: 'dkb-planList' },
3245
+ openTask.plan.items.map((item, i) => h('li', {
2945
3246
  key: i,
2946
3247
  className: 'dkb-planItem'
2947
3248
  + (item.done ? ' dkb-planDone' : '')
2948
3249
  + (item.active ? ' dkb-planActive' : ''),
2949
3250
  },
2950
- React.createElement('span', { className: 'dkb-planMark' },
3251
+ h('span', { className: 'dkb-planMark' },
2951
3252
  item.done ? '✓' : item.active ? '▸' : '·'),
2952
- React.createElement('span', null, item.text),
3253
+ h('span', null, item.text),
2953
3254
  ))),
2954
3255
  )
2955
3256
  : null,
2956
3257
  openTask.rejectReason
2957
- ? React.createElement('div', {
3258
+ ? h('div', {
2958
3259
  className: 'dkb-warnBox',
2959
3260
  style: {
2960
3261
  margin: '4px 0', padding: 10,
@@ -2963,13 +3264,13 @@ window.__ModuleLoader__.load({
2963
3264
  borderRadius: 6,
2964
3265
  },
2965
3266
  },
2966
- React.createElement('div', {
3267
+ h('div', {
2967
3268
  style: { marginBottom: 4, fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-danger-primary, #cf222e)' },
2968
3269
  }, '⚠️ ' + t('acceptance.rejectedAlert', { reason: openTask.rejectReason })),
2969
3270
  )
2970
3271
  : null,
2971
3272
  openTask.worktree
2972
- ? React.createElement('div', {
3273
+ ? h('div', {
2973
3274
  style: {
2974
3275
  margin: '8px 0', padding: 12,
2975
3276
  background: 'var(--dsw-alias-bg-layer-3)',
@@ -2977,13 +3278,13 @@ window.__ModuleLoader__.load({
2977
3278
  borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
2978
3279
  },
2979
3280
  },
2980
- React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
2981
- React.createElement('div', { className: 'dkb-group', style: { margin: 0 } },
3281
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3282
+ h('div', { className: 'dkb-group', style: { margin: 0 } },
2982
3283
  t('diff.title'),
2983
3284
  commitsData.length > 0 ? ` · ${t('diff.commits', { n: commitsData.length })}` : '',
2984
3285
  diffData?.filesChanged ? ` · ${t('diff.filesChanged', { n: diffData.filesChanged })}` : '',
2985
3286
  ),
2986
- React.createElement('button', {
3287
+ h('button', {
2987
3288
  type: 'button', className: 'dkb-discard',
2988
3289
  style: { padding: '2px 8px', fontSize: 11 },
2989
3290
  onClick: () => {
@@ -2992,37 +3293,37 @@ window.__ModuleLoader__.load({
2992
3293
  },
2993
3294
  }, diffExpanded ? t('diff.hide') : t('diff.show')),
2994
3295
  ),
2995
- diffExpanded ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
3296
+ diffExpanded ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
2996
3297
  commitsData.length > 0
2997
- ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
2998
- commitsData.map((cm) => React.createElement('div', {
3298
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
3299
+ commitsData.map((cm) => h('div', {
2999
3300
  key: cm.hash,
3000
3301
  style: { fontSize: 11, display: 'flex', gap: 6, opacity: 0.85, fontFamily: 'monospace' },
3001
3302
  },
3002
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-link, #3b82f6)' } }, cm.hash),
3003
- React.createElement('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
3004
- React.createElement('span', { style: { opacity: 0.6 } }, cm.date),
3303
+ h('span', { style: { color: 'var(--dsw-alias-label-link, #3b82f6)' } }, cm.hash),
3304
+ h('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
3305
+ h('span', { style: { opacity: 0.6 } }, cm.date),
3005
3306
  ))
3006
3307
  )
3007
3308
  : null,
3008
3309
  diffLoading
3009
- ? React.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
3310
+ ? h('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
3010
3311
  : diffData?.diff
3011
- ? React.createElement('pre', {
3312
+ ? h('pre', {
3012
3313
  style: {
3013
3314
  margin: 0, padding: 8, maxHeight: 280, overflowY: 'auto',
3014
- background: 'var(--dsw-alias-bg-layer-2, #1e1e1e)',
3015
- color: 'var(--dsw-alias-label-primary, #ddd)',
3315
+ background: 'var(--dsw-alias-bg-layer-2)',
3316
+ color: 'var(--dsw-alias-label-primary)',
3016
3317
  fontSize: 11, fontFamily: 'monospace', borderRadius: 4, whiteSpace: 'pre-wrap',
3017
3318
  border: '1px solid var(--dsw-alias-border-l1)',
3018
3319
  },
3019
3320
  }, diffData.diff)
3020
- : React.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
3321
+ : h('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
3021
3322
  ) : null
3022
3323
  )
3023
3324
  : null,
3024
3325
  openTask.column === 'review'
3025
- ? React.createElement('div', {
3326
+ ? h('div', {
3026
3327
  style: {
3027
3328
  margin: '8px 0', padding: 12,
3028
3329
  background: 'var(--dsw-alias-bg-layer-3)',
@@ -3030,13 +3331,13 @@ window.__ModuleLoader__.load({
3030
3331
  borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
3031
3332
  },
3032
3333
  },
3033
- React.createElement('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
3034
- React.createElement('div', { style: { display: 'flex', gap: 8 } },
3334
+ h('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
3335
+ h('div', { style: { display: 'flex', gap: 8 } },
3035
3336
  openTask.worktree
3036
- ? React.createElement('button', {
3337
+ ? h('button', {
3037
3338
  type: 'button', className: 'dkb-save',
3038
3339
  disabled: merging,
3039
- style: { background: 'var(--dsw-alias-bg-success, #16a34a)', color: '#fff' },
3340
+ style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-inverse, #fff)' },
3040
3341
  onClick: async () => {
3041
3342
  try {
3042
3343
  setMerging(true)
@@ -3048,7 +3349,7 @@ window.__ModuleLoader__.load({
3048
3349
  },
3049
3350
  }, merging ? t('acceptance.merging') : '🔀 ' + t('acceptance.acceptAndMerge'))
3050
3351
  : null,
3051
- React.createElement('button', {
3352
+ h('button', {
3052
3353
  type: 'button', className: 'dkb-save',
3053
3354
  onClick: async () => {
3054
3355
  try {
@@ -3058,23 +3359,50 @@ window.__ModuleLoader__.load({
3058
3359
  } catch (e) { setError(t(e.key || 'error.unknown')) }
3059
3360
  },
3060
3361
  }, '✓ ' + t('acceptance.accept')),
3061
- React.createElement('button', {
3362
+ h('button', {
3062
3363
  type: 'button', className: 'dkb-discard',
3063
3364
  onClick: () => { setRejectModalOpen(true); setRejectReasonText('') },
3064
3365
  }, '✗ ' + t('acceptance.reject')),
3366
+ openTask.owner && openTask.repo
3367
+ ? h('button', {
3368
+ type: 'button', className: 'dkb-save',
3369
+ disabled: prCreating,
3370
+ style: { background: 'var(--dsw-alias-state-info-primary, #0969da)', color: 'var(--dsw-alias-label-inverse, #fff)' },
3371
+ onClick: async () => {
3372
+ setPrCreating(true)
3373
+ try {
3374
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/pr', {
3375
+ method: 'POST',
3376
+ body: JSON.stringify({ base: 'main' }),
3377
+ })
3378
+ if (out && out.pr) setCreatedPr(out.pr)
3379
+ await load(board)
3380
+ } catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
3381
+ finally { setPrCreating(false) }
3382
+ },
3383
+ }, prCreating ? t('pr.creating') : '🐙 ' + t('pr.create'))
3384
+ : null,
3385
+ createdPr
3386
+ ? h('a', {
3387
+ className: 'dkb-save',
3388
+ style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-inverse, #fff)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
3389
+ href: createdPr.html_url,
3390
+ target: '_blank', rel: 'noopener noreferrer',
3391
+ }, '↗ ' + t('pr.view') + ` #${createdPr.number}`)
3392
+ : null,
3065
3393
  ),
3066
3394
  rejectModalOpen
3067
- ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 } },
3068
- React.createElement('span', { className: 'dkb-label' }, t('acceptance.rejectTitle')),
3069
- React.createElement('textarea', {
3395
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 } },
3396
+ h('span', { className: 'dkb-label' }, t('acceptance.rejectTitle')),
3397
+ h('textarea', {
3070
3398
  className: 'dkb-area',
3071
3399
  value: rejectReasonText,
3072
3400
  placeholder: t('acceptance.rejectReasonPlaceholder'),
3073
3401
  onChange: (e) => setRejectReasonText(e.target.value),
3074
3402
  rows: 3,
3075
3403
  }),
3076
- React.createElement('div', { style: { display: 'flex', gap: 6 } },
3077
- React.createElement('button', {
3404
+ h('div', { style: { display: 'flex', gap: 6 } },
3405
+ h('button', {
3078
3406
  type: 'button', className: 'dkb-save',
3079
3407
  onClick: async () => {
3080
3408
  if (!rejectReasonText.trim()) return
@@ -3089,7 +3417,7 @@ window.__ModuleLoader__.load({
3089
3417
  } catch (e) { setError(t(e.key || 'error.unknown')) }
3090
3418
  },
3091
3419
  }, t('acceptance.rejectSubmit')),
3092
- React.createElement('button', {
3420
+ h('button', {
3093
3421
  type: 'button', className: 'dkb-discard',
3094
3422
  onClick: () => setRejectModalOpen(false),
3095
3423
  }, t('dialog.cancel')),
@@ -3098,11 +3426,11 @@ window.__ModuleLoader__.load({
3098
3426
  : null,
3099
3427
  )
3100
3428
  : null,
3101
- React.createElement('div', { style: { margin: '4px 0' } },
3102
- React.createElement('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3103
- React.createElement('span', null, t('checklist.title')),
3429
+ h('div', { style: { margin: '4px 0' } },
3430
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3431
+ h('span', null, t('checklist.title')),
3104
3432
  Array.isArray(openTask.checklist) && openTask.checklist.length > 0
3105
- ? React.createElement('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
3433
+ ? h('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
3106
3434
  t('checklist.progress', {
3107
3435
  done: openTask.checklist.filter((i) => i.completed).length,
3108
3436
  total: openTask.checklist.length,
@@ -3110,8 +3438,8 @@ window.__ModuleLoader__.load({
3110
3438
  : null,
3111
3439
  ),
3112
3440
  Array.isArray(openTask.checklist) && openTask.checklist.length > 0
3113
- ? React.createElement('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
3114
- openTask.checklist.map((item, idx) => React.createElement('li', {
3441
+ ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
3442
+ openTask.checklist.map((item, idx) => h('li', {
3115
3443
  key: idx,
3116
3444
  style: {
3117
3445
  display: 'flex', flexDirection: 'column', gap: 2, padding: '4px 8px',
@@ -3119,8 +3447,8 @@ window.__ModuleLoader__.load({
3119
3447
  borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)',
3120
3448
  },
3121
3449
  },
3122
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
3123
- React.createElement('input', {
3450
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
3451
+ h('input', {
3124
3452
  type: 'checkbox',
3125
3453
  checked: Boolean(item.completed),
3126
3454
  onChange: async (e) => {
@@ -3136,7 +3464,7 @@ window.__ModuleLoader__.load({
3136
3464
  } catch (err) { setError(t(err.key || 'error.unknown')) }
3137
3465
  },
3138
3466
  }),
3139
- React.createElement('span', {
3467
+ h('span', {
3140
3468
  style: {
3141
3469
  textDecoration: item.completed ? 'line-through' : 'none',
3142
3470
  color: item.completed ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-primary)',
@@ -3144,11 +3472,11 @@ window.__ModuleLoader__.load({
3144
3472
  },
3145
3473
  }, item.text),
3146
3474
  item.required !== false
3147
- ? React.createElement('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
3475
+ ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
3148
3476
  : null,
3149
3477
  ),
3150
3478
  item.evidence
3151
- ? React.createElement('div', {
3479
+ ? h('div', {
3152
3480
  style: {
3153
3481
  fontSize: 11, color: 'var(--dsw-alias-label-secondary)', marginLeft: 24,
3154
3482
  fontFamily: 'var(--dsw-font-family-mono, monospace)', whiteSpace: 'pre-wrap',
@@ -3156,9 +3484,9 @@ window.__ModuleLoader__.load({
3156
3484
  }, t('checklist.evidence', { text: item.evidence }))
3157
3485
  : null,
3158
3486
  )))
3159
- : React.createElement('p', { className: 'dkb-hint' }, t('checklist.empty')),
3160
- React.createElement('div', { style: { display: 'flex', gap: 6, marginTop: 4 } },
3161
- React.createElement('input', {
3487
+ : h('p', { className: 'dkb-hint' }, t('checklist.empty')),
3488
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 4 } },
3489
+ h('input', {
3162
3490
  className: 'dkb-input',
3163
3491
  style: { flex: 1 },
3164
3492
  placeholder: t('checklist.placeholder'),
@@ -3180,7 +3508,7 @@ window.__ModuleLoader__.load({
3180
3508
  }
3181
3509
  },
3182
3510
  }),
3183
- React.createElement('button', {
3511
+ h('button', {
3184
3512
  type: 'button', className: 'dkb-save',
3185
3513
  onClick: async () => {
3186
3514
  if (!newChecklistText.trim()) return
@@ -3199,7 +3527,7 @@ window.__ModuleLoader__.load({
3199
3527
  ),
3200
3528
  ),
3201
3529
  openTask.report && openTask.report.summary
3202
- ? React.createElement('div', {
3530
+ ? h('div', {
3203
3531
  style: {
3204
3532
  margin: '6px 0', padding: 10,
3205
3533
  background: 'var(--dsw-alias-bg-layer-1)',
@@ -3207,79 +3535,79 @@ window.__ModuleLoader__.load({
3207
3535
  borderRadius: 6,
3208
3536
  },
3209
3537
  },
3210
- React.createElement('div', { className: 'dkb-group', style: { margin: '0 0 6px 0' } }, t('report.title')),
3211
- React.createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)', marginBottom: 6, fontWeight: 500 } },
3538
+ h('div', { className: 'dkb-group', style: { margin: '0 0 6px 0' } }, t('report.title')),
3539
+ h('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)', marginBottom: 6, fontWeight: 500 } },
3212
3540
  openTask.report.summary),
3213
3541
  openTask.report.changedFiles && openTask.report.changedFiles.length
3214
- ? React.createElement('div', { style: { fontSize: 12, marginBottom: 4 } },
3215
- React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.changedFiles') + ': '),
3216
- openTask.report.changedFiles.map((f) => React.createElement('span', { className: 'dkb-tag', key: f, style: { margin: '1px 2px', fontFamily: 'monospace' } }, f)),
3542
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
3543
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.changedFiles') + ': '),
3544
+ openTask.report.changedFiles.map((f) => h('span', { className: 'dkb-tag', key: f, style: { margin: '1px 2px', fontFamily: 'monospace' } }, f)),
3217
3545
  )
3218
3546
  : null,
3219
3547
  openTask.report.checksRun && openTask.report.checksRun.length
3220
- ? React.createElement('div', { style: { fontSize: 12, marginBottom: 4 } },
3221
- React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.checksRun') + ': '),
3222
- openTask.report.checksRun.map((chk, i) => React.createElement('div', { key: i, style: { fontFamily: 'monospace', fontSize: 11, marginLeft: 8 } }, '• ' + chk)),
3548
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
3549
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.checksRun') + ': '),
3550
+ openTask.report.checksRun.map((chk, i) => h('div', { key: i, style: { fontFamily: 'monospace', fontSize: 11, marginLeft: 8 } }, '• ' + chk)),
3223
3551
  )
3224
3552
  : null,
3225
3553
  openTask.report.artifacts && openTask.report.artifacts.length
3226
- ? React.createElement('div', { style: { fontSize: 12, marginBottom: 4 } },
3227
- React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.artifacts') + ': '),
3228
- openTask.report.artifacts.map((a, i) => React.createElement('span', { className: 'dkb-tag', key: i, style: { margin: '1px 2px' } }, a)),
3554
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
3555
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.artifacts') + ': '),
3556
+ openTask.report.artifacts.map((a, i) => h('span', { className: 'dkb-tag', key: i, style: { margin: '1px 2px' } }, a)),
3229
3557
  )
3230
3558
  : null,
3231
3559
  openTask.report.risks && openTask.report.risks.length
3232
- ? React.createElement('div', { style: { fontSize: 12 } },
3233
- React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.risks') + ': '),
3234
- openTask.report.risks.map((r, i) => React.createElement('div', { key: i, style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary, #b08800)', marginLeft: 8 } }, '⚠ ' + r)),
3560
+ ? h('div', { style: { fontSize: 12 } },
3561
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.risks') + ': '),
3562
+ openTask.report.risks.map((r, i) => h('div', { key: i, style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary, #b08800)', marginLeft: 8 } }, '⚠ ' + r)),
3235
3563
  )
3236
3564
  : null,
3237
3565
  )
3238
3566
  : null,
3239
- React.createElement('div', { style: { margin: '6px 0' } },
3240
- React.createElement('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3241
- React.createElement('span', null, t('comments.title')),
3567
+ h('div', { style: { margin: '6px 0' } },
3568
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3569
+ h('span', null, t('comments.title')),
3242
3570
  Array.isArray(openTask.comments) && openTask.comments.length > 0
3243
- ? React.createElement('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
3571
+ ? h('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
3244
3572
  : null,
3245
3573
  ),
3246
3574
  Array.isArray(openTask.comments) && openTask.comments.length > 0
3247
- ? React.createElement('ul', {
3575
+ ? h('ul', {
3248
3576
  style: {
3249
3577
  listStyle: 'none', padding: 0, margin: '4px 0', maxHeight: 200, overflowY: 'auto',
3250
3578
  display: 'flex', flexDirection: 'column', gap: 6,
3251
3579
  },
3252
3580
  },
3253
- openTask.comments.map((c, i) => React.createElement('li', {
3581
+ openTask.comments.map((c, i) => h('li', {
3254
3582
  key: c.id || i,
3255
3583
  style: {
3256
3584
  padding: 8, background: 'var(--dsw-alias-bg-layer-3)',
3257
3585
  borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
3258
3586
  },
3259
3587
  },
3260
- React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
3261
- React.createElement('span', {
3588
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
3589
+ h('span', {
3262
3590
  className: 'dkb-tag',
3263
3591
  style: {
3264
3592
  fontWeight: 600,
3265
3593
  background: c.role === 'agent' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-2)',
3266
3594
  },
3267
3595
  }, c.role === 'agent' ? t('comments.authorAgent') : (c.author || t('comments.authorUser'))),
3268
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
3596
+ h('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
3269
3597
  new Date(c.createdAt || Date.now()).toLocaleTimeString()),
3270
3598
  ),
3271
- React.createElement('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
3599
+ h('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
3272
3600
  )))
3273
- : React.createElement('p', { className: 'dkb-hint' }, t('comments.empty')),
3274
- React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
3275
- React.createElement('textarea', {
3601
+ : h('p', { className: 'dkb-hint' }, t('comments.empty')),
3602
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
3603
+ h('textarea', {
3276
3604
  className: 'dkb-area',
3277
3605
  placeholder: t('comments.placeholder'),
3278
3606
  value: newCommentText,
3279
3607
  rows: 2,
3280
3608
  onChange: (e) => setNewCommentText(e.target.value),
3281
3609
  }),
3282
- React.createElement('button', {
3610
+ h('button', {
3283
3611
  type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
3284
3612
  onClick: async () => {
3285
3613
  if (!newCommentText.trim()) return
@@ -3297,10 +3625,8 @@ window.__ModuleLoader__.load({
3297
3625
  ),
3298
3626
  ),
3299
3627
  openTask.archivedAt
3300
- ? React.createElement('button', {
3628
+ ? h('button', {
3301
3629
  type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
3302
- // Запускать работу по архивной задаче нечего: сперва она
3303
- // возвращается на доску, и это одно нажатие.
3304
3630
  onClick: async () => {
3305
3631
  try {
3306
3632
  await api('/task/' + encodeURIComponent(openTask.id) + '/restore', { method: 'POST', body: '{}' })
@@ -3310,70 +3636,59 @@ window.__ModuleLoader__.load({
3310
3636
  } catch (e) { setError(t(e.key || 'error.unknown')) }
3311
3637
  },
3312
3638
  }, t('archive.restore'))
3313
- : React.createElement('div', { className: 'dkb-start' },
3314
- React.createElement('div', { className: 'dkb-group' },
3639
+ : h('div', { className: 'dkb-start' },
3640
+ h('div', { className: 'dkb-group' },
3315
3641
  openTask.sessionId ? t('panel.continue') : t('panel.start')),
3316
- React.createElement('p', { className: 'dkb-hint' },
3642
+ h('p', { className: 'dkb-hint' },
3317
3643
  openTask.sessionId ? t('panel.continueHint') : t('panel.startHint')),
3318
- // Корень не задан настройкой — сессия пойдёт в рабочей папке
3319
- // харнесса. Молчать об этом нельзя: человек иначе узнает, где
3320
- // оказался агент, только из его первых слов. Показываем фактический
3321
- // корень, который приехал с доской.
3322
3644
  !openTask.sessionId && state && !state.projectRoot.set && state.projectRoot.path !== ''
3323
- ? React.createElement('p', { className: 'dkb-hint dkb-workdir' },
3645
+ ? h('p', { className: 'dkb-hint dkb-workdir' },
3324
3646
  t('panel.workDir', { path: state.projectRoot.path }),
3325
- React.createElement('br'),
3647
+ h('br'),
3326
3648
  t('panel.workDirHint'))
3327
3649
  : null,
3328
3650
  React.createElement(LaunchFields, {
3329
3651
  t, presets, agentPreset, permission, disabled: starting,
3330
3652
  onPreset: setAgentPreset, onPermission: setPermission,
3331
- // У идущей сессии профиль уже выбран: сменить его нельзя, а
3332
- // показывать поле, которое ничего не меняет, — врать.
3333
3653
  hidePreset: Boolean(openTask.sessionId),
3334
3654
  }),
3335
- React.createElement('div', { className: 'dkb-field' },
3336
- React.createElement('span', { className: 'dkb-label' }, t('panel.provider')),
3337
- React.createElement('select', {
3655
+ h('div', { className: 'dkb-field' },
3656
+ h('span', { className: 'dkb-label' }, t('panel.provider')),
3657
+ h('select', {
3338
3658
  className: 'dkb-input', value: provider, disabled: starting || providers.length === 0,
3339
3659
  onChange: (e) => { setProvider(e.target.value); setModel('') },
3340
3660
  },
3341
- React.createElement('option', { value: '' },
3661
+ h('option', { value: '' },
3342
3662
  providers.length === 0 ? t('panel.noProviders') : t('panel.pickProvider')),
3343
- providers.map((p) => React.createElement('option', { value: p.id, key: p.id }, p.name)),
3663
+ providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
3344
3664
  ),
3345
3665
  ),
3346
- React.createElement('div', { className: 'dkb-field' },
3347
- React.createElement('span', { className: 'dkb-label' }, t('panel.model')),
3348
- React.createElement('select', {
3666
+ h('div', { className: 'dkb-field' },
3667
+ h('span', { className: 'dkb-label' }, t('panel.model')),
3668
+ h('select', {
3349
3669
  className: 'dkb-input', value: model,
3350
3670
  disabled: starting || provider === '' || models.length === 0,
3351
3671
  onChange: (e) => setModel(e.target.value),
3352
3672
  },
3353
- React.createElement('option', { value: '' },
3673
+ h('option', { value: '' },
3354
3674
  provider === '' ? t('panel.pickProviderFirst')
3355
3675
  : models.length === 0 ? t('panel.noModels') : t('panel.pickModel')),
3356
- models.map((m) => React.createElement('option', { value: m.id, key: m.id }, m.name)),
3676
+ models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
3357
3677
  ),
3358
3678
  ),
3359
- React.createElement('div', { className: 'dkb-field' },
3360
- React.createElement('span', { className: 'dkb-label' }, t('panel.message')),
3361
- React.createElement('textarea', {
3679
+ h('div', { className: 'dkb-field' },
3680
+ h('span', { className: 'dkb-label' }, t('panel.message')),
3681
+ h('textarea', {
3362
3682
  className: 'dkb-area', value: draftText, disabled: starting,
3363
3683
  onChange: (e) => setDraftText(e.target.value),
3364
3684
  }),
3365
- React.createElement('p', { className: 'dkb-hint' }, t('panel.messageHint')),
3685
+ h('p', { className: 'dkb-hint' }, t('panel.messageHint')),
3366
3686
  ),
3367
- // Опасное слева, обычное справа, всё в одной полосе. Отдельная строка
3368
- // под «Удалить» съедала высоту окна ради одной кнопки, которую
3369
- // нажимают раз в жизни.
3370
- React.createElement('div', { className: 'dkb-foot dkb-footSplit' },
3371
- React.createElement('div', { className: 'dkb-footLeft' },
3372
- React.createElement('button', {
3687
+ h('div', { className: 'dkb-foot dkb-footSplit' },
3688
+ h('div', { className: 'dkb-footLeft' },
3689
+ h('button', {
3373
3690
  type: 'button', className: 'dkb-danger',
3374
3691
  onClick: async () => {
3375
- // Подтверждение обязательно: удаление карточки необратимо, а
3376
- // сама кнопка стоит рядом с безобидными.
3377
3692
  if (!window.confirm(t('panel.deleteConfirm', { title: openTask.title }))) return
3378
3693
  try {
3379
3694
  await api('/task/' + encodeURIComponent(openTask.id), { method: 'DELETE' })
@@ -3385,7 +3700,7 @@ window.__ModuleLoader__.load({
3385
3700
  },
3386
3701
  }, t('panel.delete')),
3387
3702
  openTask.column === 'done'
3388
- ? React.createElement('button', {
3703
+ ? h('button', {
3389
3704
  type: 'button', className: 'dkb-discard',
3390
3705
  onClick: async () => {
3391
3706
  try {
@@ -3397,7 +3712,7 @@ window.__ModuleLoader__.load({
3397
3712
  }, t('panel.archive'))
3398
3713
  : null,
3399
3714
  openTask.column === 'cleanup' && openTask.worktree
3400
- ? React.createElement('button', {
3715
+ ? h('button', {
3401
3716
  type: 'button', className: 'dkb-discard',
3402
3717
  onClick: async () => {
3403
3718
  try {
@@ -3415,10 +3730,10 @@ window.__ModuleLoader__.load({
3415
3730
  }, t('panel.cleanupWorktree'))
3416
3731
  : null,
3417
3732
  ),
3418
- (openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? React.createElement('button', {
3733
+ (openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? h('button', {
3419
3734
  type: 'button',
3420
3735
  className: 'dkb-secondary',
3421
- style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-color-primary, #444)' },
3736
+ style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
3422
3737
  disabled: starting || model === '',
3423
3738
  onClick: async () => {
3424
3739
  setStarting(true)
@@ -3442,11 +3757,9 @@ window.__ModuleLoader__.load({
3442
3757
  }
3443
3758
  },
3444
3759
  }, t('action.resume')) : null,
3445
- React.createElement('button', {
3760
+ h('button', {
3446
3761
  type: 'button', className: 'dkb-save', disabled: starting || model === '',
3447
3762
  onClick: async () => {
3448
- // Кнопка блокируется на время запроса: двойное нажатие подняло
3449
- // бы две сессии по одной задаче.
3450
3763
  setStarting(true)
3451
3764
  try {
3452
3765
  rememberLaunch(board, { provider, model, agentPreset, permission })
@@ -3456,9 +3769,6 @@ window.__ModuleLoader__.load({
3456
3769
  })
3457
3770
  setOpenTask(null)
3458
3771
  await load(board)
3459
- // Сессия поднята — но пока её не открыли, для человека
3460
- // «ничего не произошло»: он остаётся на доске и не видит
3461
- // чата, ради которого нажимал.
3462
3772
  if (!openSession(props.ctx, out && out.sessionId)) {
3463
3773
  setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
3464
3774
  return
@@ -3478,18 +3788,18 @@ window.__ModuleLoader__.load({
3478
3788
  : null
3479
3789
 
3480
3790
  const moveDialogEl = dialog && dialog.mode === 'move'
3481
- ? React.createElement('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3482
- React.createElement('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3483
- React.createElement('span', { className: 'dkb-title' },
3791
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3792
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3793
+ h('span', { className: 'dkb-title' },
3484
3794
  t('move.title', { column: t('column.' + dialog.column) })),
3485
- React.createElement('p', { className: 'dkb-note' },
3795
+ h('p', { className: 'dkb-note' },
3486
3796
  cardRef(dialog.task) ? cardRef(dialog.task) + ' · ' + dialog.task.title : dialog.task.title),
3487
- React.createElement('p', { className: 'dkb-hint' }, t('move.' + dialog.column)),
3488
- React.createElement('div', { className: 'dkb-row' },
3489
- React.createElement('button', {
3797
+ h('p', { className: 'dkb-hint' }, t('move.' + dialog.column)),
3798
+ h('div', { className: 'dkb-row' },
3799
+ h('button', {
3490
3800
  type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
3491
3801
  }, t('dialog.cancel')),
3492
- React.createElement('button', {
3802
+ h('button', {
3493
3803
  type: 'button', className: 'dkb-save',
3494
3804
  onClick: () => {
3495
3805
  const d = dialog
@@ -3501,51 +3811,49 @@ window.__ModuleLoader__.load({
3501
3811
  ))
3502
3812
  : null
3503
3813
 
3504
- // Групповой запуск. Провайдер и модель спрашиваем один раз на всю пачку:
3505
- // сессия одна, значит и модель одна.
3506
3814
  const batchEl = dialog && dialog.mode === 'batch'
3507
- ? React.createElement('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3508
- React.createElement('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3509
- React.createElement('span', { className: 'dkb-title' },
3815
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3816
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3817
+ h('span', { className: 'dkb-title' },
3510
3818
  t('batch.title', { n: dialog.ids.length })),
3511
- React.createElement('p', { className: 'dkb-hint' }, t('batch.hint')),
3512
- dialog.error ? React.createElement('p', { className: 'dkb-failed' }, dialog.error) : null,
3513
- React.createElement('ul', { className: 'dkb-batchList' },
3819
+ h('p', { className: 'dkb-hint' }, t('batch.hint')),
3820
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
3821
+ h('ul', { className: 'dkb-batchList' },
3514
3822
  dialog.ids.map((id, at) => {
3515
3823
  const task = state.tasks.find((x) => x.id === id)
3516
- return React.createElement('li', { className: 'dkb-batchRow', key: id },
3517
- React.createElement('span', { className: 'dkb-refNum' }, String(at + 1)),
3518
- React.createElement('span', null, task ? task.title : id))
3824
+ return h('li', { className: 'dkb-batchRow', key: id },
3825
+ h('span', { className: 'dkb-refNum' }, String(at + 1)),
3826
+ h('span', null, task ? task.title : id))
3519
3827
  })),
3520
- React.createElement('div', { className: 'dkb-field' },
3521
- React.createElement('span', { className: 'dkb-label' }, t('panel.provider')),
3522
- React.createElement('select', {
3828
+ h('div', { className: 'dkb-field' },
3829
+ h('span', { className: 'dkb-label' }, t('panel.provider')),
3830
+ h('select', {
3523
3831
  className: 'dkb-input', value: provider,
3524
3832
  onChange: (e) => { setProvider(e.target.value); setModel('') },
3525
3833
  },
3526
- React.createElement('option', { value: '' }, t('panel.pickProvider')),
3527
- providers.map((p) => React.createElement('option', { value: p.id, key: p.id }, p.name)),
3834
+ h('option', { value: '' }, t('panel.pickProvider')),
3835
+ providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
3528
3836
  ),
3529
3837
  ),
3530
- React.createElement('div', { className: 'dkb-field' },
3531
- React.createElement('span', { className: 'dkb-label' }, t('panel.model')),
3532
- React.createElement('select', {
3838
+ h('div', { className: 'dkb-field' },
3839
+ h('span', { className: 'dkb-label' }, t('panel.model')),
3840
+ h('select', {
3533
3841
  className: 'dkb-input', value: model, disabled: provider === '',
3534
3842
  onChange: (e) => setModel(e.target.value),
3535
3843
  },
3536
- React.createElement('option', { value: '' }, t('panel.pickModel')),
3537
- models.map((m) => React.createElement('option', { value: m.id, key: m.id }, m.name)),
3844
+ h('option', { value: '' }, t('panel.pickModel')),
3845
+ models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
3538
3846
  ),
3539
3847
  ),
3540
3848
  React.createElement(LaunchFields, {
3541
3849
  t, presets, agentPreset, permission,
3542
3850
  onPreset: setAgentPreset, onPermission: setPermission,
3543
3851
  }),
3544
- React.createElement('div', { className: 'dkb-row' },
3545
- React.createElement('button', {
3852
+ h('div', { className: 'dkb-row' },
3853
+ h('button', {
3546
3854
  type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
3547
3855
  }, t('dialog.cancel')),
3548
- React.createElement('button', {
3856
+ h('button', {
3549
3857
  type: 'button', className: 'dkb-save', disabled: model === '',
3550
3858
  onClick: async () => {
3551
3859
  try {
@@ -3557,8 +3865,6 @@ window.__ModuleLoader__.load({
3557
3865
  setDialog(null)
3558
3866
  setPicked({})
3559
3867
  await load(board)
3560
- // Сессия поднята — но пока её не открыли, для человека
3561
- // «ничего не произошло».
3562
3868
  if (!openSession(props.ctx, out && out.sessionId)) {
3563
3869
  setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
3564
3870
  return
@@ -3574,28 +3880,28 @@ window.__ModuleLoader__.load({
3574
3880
  : null
3575
3881
 
3576
3882
  const queueEl = dialog && dialog.mode === 'queue'
3577
- ? React.createElement('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3578
- React.createElement('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3579
- React.createElement('span', { className: 'dkb-title' }, t('queue.title')),
3580
- React.createElement('p', { className: 'dkb-hint' }, t('queue.hint')),
3581
- dialog.error ? React.createElement('p', { className: 'dkb-failed' }, dialog.error) : null,
3582
- React.createElement('div', { className: 'dkb-field' },
3583
- React.createElement('span', { className: 'dkb-label' }, t('queue.session')),
3584
- React.createElement('select', {
3883
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3884
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3885
+ h('span', { className: 'dkb-title' }, t('queue.title')),
3886
+ h('p', { className: 'dkb-hint' }, t('queue.hint')),
3887
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
3888
+ h('div', { className: 'dkb-field' },
3889
+ h('span', { className: 'dkb-label' }, t('queue.session')),
3890
+ h('select', {
3585
3891
  className: 'dkb-input', value: dialog.sessionId,
3586
3892
  onChange: (e) => setDialog(Object.assign({}, dialog, { sessionId: e.target.value })),
3587
- }, dialog.sessions.map((one) => React.createElement('option', {
3893
+ }, dialog.sessions.map((one) => h('option', {
3588
3894
  value: one.sessionId, key: one.sessionId,
3589
3895
  }, t('queue.option', {
3590
3896
  n: one.tasks.length,
3591
3897
  title: (one.tasks[0] && one.tasks[0].title) || one.sessionId,
3592
3898
  })))),
3593
3899
  ),
3594
- React.createElement('div', { className: 'dkb-row' },
3595
- React.createElement('button', {
3900
+ h('div', { className: 'dkb-row' },
3901
+ h('button', {
3596
3902
  type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
3597
3903
  }, t('dialog.cancel')),
3598
- React.createElement('button', {
3904
+ h('button', {
3599
3905
  type: 'button', className: 'dkb-save',
3600
3906
  onClick: async () => {
3601
3907
  try {
@@ -3615,17 +3921,15 @@ window.__ModuleLoader__.load({
3615
3921
  : null
3616
3922
 
3617
3923
  const dialogEl = dialog && dialog.mode !== 'move' && dialog.mode !== 'batch' && dialog.mode !== 'queue'
3618
- ? React.createElement('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3619
- React.createElement('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3620
- React.createElement('span', { className: 'dkb-title' }, t('dialog.title')),
3621
- React.createElement('div', { className: 'dkb-row' },
3622
- React.createElement('button', {
3924
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3925
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3926
+ h('span', { className: 'dkb-title' }, t('dialog.title')),
3927
+ h('div', { className: 'dkb-row' },
3928
+ h('button', {
3623
3929
  type: 'button', className: dialog.mode === 'own' ? 'dkb-save' : 'dkb-discard',
3624
3930
  onClick: () => setDialog(Object.assign({}, dialog, { mode: 'own', error: '' })),
3625
3931
  }, t('dialog.own')),
3626
- // Простая доска — для задач без issue, и предлагать на ней импорт
3627
- // значит звать туда, откуда сервер вернёт отказ.
3628
- state.kind === 'simple' ? null : React.createElement('button', {
3932
+ state.kind === 'simple' ? null : h('button', {
3629
3933
  type: 'button', className: dialog.mode === 'gitea' ? 'dkb-save' : 'dkb-discard',
3630
3934
  onClick: async () => {
3631
3935
  setDialog(Object.assign({}, dialog, { mode: 'gitea', error: '' }))
@@ -3639,93 +3943,86 @@ window.__ModuleLoader__.load({
3639
3943
  },
3640
3944
  }, t('dialog.fromGitea')),
3641
3945
  ),
3642
- dialog.error ? React.createElement('p', { className: 'dkb-failed' }, dialog.error) : null,
3946
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
3643
3947
  dialog.mode === 'own'
3644
- ? React.createElement('div', null,
3645
- // Проект или нет — это выбор доски, и он же выбор судьбы
3646
- // задачи: у проектной появляется issue, у остальной нет.
3647
- // Правило без исключений: есть issue — проектная доска.
3648
- React.createElement('div', { className: 'dkb-field' },
3649
- React.createElement('span', { className: 'dkb-label' }, t('dialog.kind')),
3650
- React.createElement('div', { className: 'dkb-row' },
3651
- React.createElement('button', {
3948
+ ? h('div', null,
3949
+ h('div', { className: 'dkb-field' },
3950
+ h('span', { className: 'dkb-label' }, t('dialog.kind')),
3951
+ h('div', { className: 'dkb-row' },
3952
+ h('button', {
3652
3953
  type: 'button', className: dialog.project ? 'dkb-save' : 'dkb-discard',
3653
3954
  onClick: () => setDialog(Object.assign({}, dialog, { project: true, error: '' })),
3654
3955
  }, t('dialog.kindProject')),
3655
- React.createElement('button', {
3956
+ h('button', {
3656
3957
  type: 'button', className: dialog.project ? 'dkb-discard' : 'dkb-save',
3657
3958
  onClick: () => setDialog(Object.assign({}, dialog, { project: false, error: '' })),
3658
3959
  }, t('dialog.kindPlain')),
3659
3960
  ),
3660
- React.createElement('p', { className: 'dkb-hint' },
3961
+ h('p', { className: 'dkb-hint' },
3661
3962
  t(dialog.project ? 'dialog.kindProjectHint' : 'dialog.kindPlainHint')),
3662
3963
  ),
3663
3964
  dialog.project
3664
- ? React.createElement('div', { className: 'dkb-field' },
3665
- React.createElement('span', { className: 'dkb-label' }, t('dialog.repo')),
3666
- React.createElement('select', {
3965
+ ? h('div', { className: 'dkb-field' },
3966
+ h('span', { className: 'dkb-label' }, t('dialog.repo')),
3967
+ h('select', {
3667
3968
  className: 'dkb-input', value: dialog.repo || '',
3668
3969
  onChange: (e) => setDialog(Object.assign({}, dialog, { repo: e.target.value })),
3669
3970
  },
3670
- React.createElement('option', { value: '' }, t('dialog.repoNew')),
3971
+ h('option', { value: '' }, t('dialog.repoNew')),
3671
3972
  (state.facets || [])
3672
3973
  .filter((f) => f.ns === REPO_FACET)
3673
3974
  .flatMap((f) => f.values)
3674
- .map((v) => React.createElement('option', { value: v.value, key: v.value }, v.value)),
3975
+ .map((v) => h('option', { value: v.value, key: v.value }, v.value)),
3675
3976
  ),
3676
3977
  (dialog.repo || '') === ''
3677
- ? React.createElement('div', null,
3678
- React.createElement('input', {
3978
+ ? h('div', null,
3979
+ h('input', {
3679
3980
  className: 'dkb-input', value: dialog.newRepo || '',
3680
3981
  placeholder: t('dialog.repoNamePlaceholder'),
3681
3982
  onChange: (e) => setDialog(Object.assign({}, dialog, { newRepo: e.target.value })),
3682
3983
  }),
3683
- // Создание репозитория необратимо в один клик, и
3684
- // человек должен видеть это ДО нажатия, а не после.
3685
- React.createElement('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
3984
+ h('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
3686
3985
  )
3687
3986
  : null,
3688
3987
  )
3689
3988
  : null,
3690
- React.createElement('div', { className: 'dkb-field' },
3691
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
3692
- React.createElement('input', {
3989
+ h('div', { className: 'dkb-field' },
3990
+ h('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
3991
+ h('input', {
3693
3992
  className: 'dkb-input', value: dialog.title, autoFocus: true,
3694
3993
  onChange: (e) => setDialog(Object.assign({}, dialog, { title: e.target.value })),
3695
3994
  }),
3696
3995
  ),
3697
- React.createElement('div', { className: 'dkb-field' },
3698
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskBody')),
3699
- React.createElement('textarea', {
3996
+ h('div', { className: 'dkb-field' },
3997
+ h('span', { className: 'dkb-label' }, t('dialog.taskBody')),
3998
+ h('textarea', {
3700
3999
  className: 'dkb-area', value: dialog.body || '',
3701
4000
  onChange: (e) => setDialog(Object.assign({}, dialog, { body: e.target.value })),
3702
4001
  }),
3703
- React.createElement('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
4002
+ h('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
3704
4003
  ),
3705
- React.createElement('div', { className: 'dkb-field' },
3706
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
3707
- React.createElement('input', {
4004
+ h('div', { className: 'dkb-field' },
4005
+ h('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
4006
+ h('input', {
3708
4007
  className: 'dkb-input', value: dialog.labels || '',
3709
4008
  placeholder: t('dialog.labelsHint'),
3710
4009
  onChange: (e) => setDialog(Object.assign({}, dialog, { labels: e.target.value })),
3711
4010
  }),
3712
4011
  ),
3713
- React.createElement('div', { className: 'dkb-field' },
3714
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
3715
- React.createElement('select', {
4012
+ h('div', { className: 'dkb-field' },
4013
+ h('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
4014
+ h('select', {
3716
4015
  className: 'dkb-input', value: dialog.column || 'backlog',
3717
4016
  onChange: (e) => setDialog(Object.assign({}, dialog, { column: e.target.value })),
3718
- }, (state.columns || []).map((c) => React.createElement('option', { value: c.id, key: c.id }, t('column.' + c.id)))),
4017
+ }, (state.columns || []).map((c) => h('option', { value: c.id, key: c.id }, t('column.' + c.id)))),
3719
4018
  ),
3720
- React.createElement('div', { className: 'dkb-foot' },
3721
- React.createElement('button', { type: 'button', className: 'dkb-discard', onClick: () => setDialog(null) }, t('dialog.cancel')),
3722
- React.createElement('button', {
4019
+ h('div', { className: 'dkb-foot' },
4020
+ h('button', { type: 'button', className: 'dkb-discard', onClick: () => setDialog(null) }, t('dialog.cancel')),
4021
+ h('button', {
3723
4022
  type: 'button', className: 'dkb-save', disabled: String(dialog.title || '').trim() === '',
3724
4023
  onClick: async () => {
3725
4024
  try {
3726
4025
  if (dialog.project) {
3727
- // Проектная задача заводит issue и привязывается к
3728
- // нему; метки ей ставит Gitea, а не мы.
3729
4026
  await api('/project-task', {
3730
4027
  method: 'POST',
3731
4028
  body: JSON.stringify({
@@ -3742,12 +4039,8 @@ window.__ModuleLoader__.load({
3742
4039
  body: JSON.stringify({
3743
4040
  title: dialog.title,
3744
4041
  body: dialog.body || '',
3745
- // Метки вводятся через запятую: отдельный редактор
3746
- // меток ради своей задачи — лишняя мебель.
3747
4042
  labels: String(dialog.labels || '').split(',').map((x) => x.trim()).filter(Boolean),
3748
4043
  column: dialog.column || 'backlog',
3749
- // Задача без issue живёт на простой доске: правило
3750
- // без исключений, и помнить, где её завели, не надо.
3751
4044
  board: 'simple',
3752
4045
  }),
3753
4046
  })
@@ -3761,9 +4054,9 @@ window.__ModuleLoader__.load({
3761
4054
  }, t('dialog.create')),
3762
4055
  ),
3763
4056
  )
3764
- : React.createElement('div', null,
3765
- React.createElement('div', { className: 'dkb-row' },
3766
- React.createElement('select', {
4057
+ : h('div', null,
4058
+ h('div', { className: 'dkb-row' },
4059
+ h('select', {
3767
4060
  className: 'dkb-input', value: dialog.full,
3768
4061
  disabled: dialog.repos === null || dialog.repos.length === 0,
3769
4062
  onChange: async (e) => {
@@ -3780,20 +4073,20 @@ window.__ModuleLoader__.load({
3780
4073
  }
3781
4074
  },
3782
4075
  },
3783
- React.createElement('option', { value: '' },
4076
+ h('option', { value: '' },
3784
4077
  dialog.repos === null ? t('dialog.loadingRepos')
3785
4078
  : dialog.repos.length === 0 ? t('dialog.noRepos') : t('dialog.pickRepo')),
3786
4079
  (dialog.repos || []).map((r) => {
3787
4080
  const full = r.fullName || (r.owner + '/' + r.repo)
3788
- return React.createElement('option', { value: full, key: full }, repoOption(r, t))
4081
+ return h('option', { value: full, key: full }, repoOption(r, t))
3789
4082
  }),
3790
4083
  ),
3791
4084
  ),
3792
4085
  dialog.issues === null ? null
3793
4086
  : dialog.issues.length === 0
3794
- ? React.createElement('p', { className: 'dkb-note' }, t('dialog.noIssues'))
3795
- : React.createElement('div', { className: 'dkb-list' }, dialog.issues.map((issue) =>
3796
- React.createElement('button', {
4087
+ ? h('p', { className: 'dkb-note' }, t('dialog.noIssues'))
4088
+ : h('div', { className: 'dkb-list' }, dialog.issues.map((issue) =>
4089
+ h('button', {
3797
4090
  type: 'button',
3798
4091
  className: 'dkb-issue' + (issue.imported ? ' dkb-issueUsed' : ''),
3799
4092
  key: issue.number,
@@ -3816,25 +4109,21 @@ window.__ModuleLoader__.load({
3816
4109
  ))
3817
4110
  : null
3818
4111
 
3819
- // Архив — свой экран, а не колонка: колонка это стадия работы, а архив
3820
- // стадией не является. Возврат ставит карточку ровно туда, где она была.
3821
4112
  const archiveEl = archive
3822
- ? React.createElement('div', { className: 'dkb-archive' },
3823
- React.createElement('p', { className: 'dkb-hint' }, t('archive.hint')),
4113
+ ? h('div', { className: 'dkb-archive' },
4114
+ h('p', { className: 'dkb-hint' }, t('archive.hint')),
3824
4115
  archive.length === 0
3825
- ? React.createElement('p', { className: 'dkb-note' }, t('archive.empty'))
3826
- : React.createElement('ul', { className: 'dkb-archiveList' },
3827
- archive.map((task) => React.createElement('li', { className: 'dkb-archiveRow', key: task.id },
3828
- React.createElement('button', {
4116
+ ? h('p', { className: 'dkb-note' }, t('archive.empty'))
4117
+ : h('ul', { className: 'dkb-archiveList' },
4118
+ archive.map((task) => h('li', { className: 'dkb-archiveRow', key: task.id },
4119
+ h('button', {
3829
4120
  type: 'button', className: 'dkb-archiveTitle dkb-archiveOpen',
3830
- // Архив — не братская могила: задачу туда убрали, а не
3831
- // стёрли, и прочитать её должно быть можно, не возвращая.
3832
4121
  onClick: () => setOpenTask(task),
3833
4122
  },
3834
4123
  [cardRef(task), task.title].filter(Boolean).join(' · ')),
3835
- React.createElement('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
4124
+ h('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
3836
4125
  t('archive.since', { ago: agoText(state.now, task.archivedAt, t) })),
3837
- React.createElement('button', {
4126
+ h('button', {
3838
4127
  type: 'button', className: 'dkb-discard',
3839
4128
  onClick: async () => {
3840
4129
  try {
@@ -3848,45 +4137,43 @@ window.__ModuleLoader__.load({
3848
4137
  )
3849
4138
  : null
3850
4139
 
3851
- // Метрика — это взгляд на журнал, а не новые данные: экран ничего не
3852
- // меняет, поэтому и кнопок действий на нём нет.
3853
4140
  const metricsEl = metrics
3854
- ? React.createElement('div', { className: 'dkb-archive' },
3855
- React.createElement('p', { className: 'dkb-hint' },
4141
+ ? h('div', { className: 'dkb-archive' },
4142
+ h('p', { className: 'dkb-hint' },
3856
4143
  t('metrics.hint', { n: metrics.tasks, week: metrics.done.week, month: metrics.done.month })),
3857
- React.createElement('div', { className: 'dkb-group' }, t('metrics.columns')),
4144
+ h('div', { className: 'dkb-group' }, t('metrics.columns')),
3858
4145
  metrics.columns.length === 0
3859
- ? React.createElement('p', { className: 'dkb-note' }, t('metrics.empty'))
3860
- : React.createElement('ul', { className: 'dkb-archiveList' },
3861
- metrics.columns.map((row) => React.createElement('li', {
4146
+ ? h('p', { className: 'dkb-note' }, t('metrics.empty'))
4147
+ : h('ul', { className: 'dkb-archiveList' },
4148
+ metrics.columns.map((row) => h('li', {
3862
4149
  className: 'dkb-archiveRow', key: row.column,
3863
4150
  },
3864
- React.createElement('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
3865
- React.createElement('span', { className: 'dkb-taskRef' },
4151
+ h('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
4152
+ h('span', { className: 'dkb-taskRef' },
3866
4153
  t('metrics.median', { time: agoText(0, -row.median, t) })),
3867
- React.createElement('span', { className: 'dkb-taskRef' },
4154
+ h('span', { className: 'dkb-taskRef' },
3868
4155
  t('metrics.mean', { time: agoText(0, -row.mean, t) })),
3869
- React.createElement('span', { className: 'dkb-count' }, String(row.tasks)),
4156
+ h('span', { className: 'dkb-count' }, String(row.tasks)),
3870
4157
  ))),
3871
- React.createElement('div', { className: 'dkb-group' },
4158
+ h('div', { className: 'dkb-group' },
3872
4159
  t('metrics.stale', { days: metrics.staleDays })),
3873
4160
  metrics.stale.length === 0
3874
- ? React.createElement('p', { className: 'dkb-note' }, t('metrics.noStale'))
3875
- : React.createElement('ul', { className: 'dkb-archiveList' },
3876
- metrics.stale.map((row) => React.createElement('li', {
4161
+ ? h('p', { className: 'dkb-note' }, t('metrics.noStale'))
4162
+ : h('ul', { className: 'dkb-archiveList' },
4163
+ metrics.stale.map((row) => h('li', {
3877
4164
  className: 'dkb-archiveRow', key: row.id,
3878
4165
  },
3879
- React.createElement('span', { className: 'dkb-archiveTitle' }, row.title),
3880
- React.createElement('span', { className: 'dkb-taskRef' }, columnTitle({ id: row.column }, t)),
3881
- React.createElement('span', { className: 'dkb-taskRef', title: exactAt(row.since) },
4166
+ h('span', { className: 'dkb-archiveTitle' }, row.title),
4167
+ h('span', { className: 'dkb-taskRef' }, columnTitle({ id: row.column }, t)),
4168
+ h('span', { className: 'dkb-taskRef', title: exactAt(row.since) },
3882
4169
  agoText(state.now, row.since, t)),
3883
4170
  ))),
3884
4171
  )
3885
4172
  : null
3886
4173
 
3887
- return React.createElement('div', { className: 'dkb-screen' },
4174
+ return h('div', { className: 'dkb-screen' },
3888
4175
  style, bar,
3889
- error ? React.createElement('p', { className: 'dkb-failed' }, error) : null,
4176
+ error ? h('p', { className: 'dkb-failed' }, error) : null,
3890
4177
  metrics ? metricsEl : archive ? archiveEl : columns,
3891
4178
  panel, dialogEl, moveDialogEl, batchEl, queueEl,
3892
4179
  )
@@ -3928,41 +4215,38 @@ window.__ModuleLoader__.load({
3928
4215
  })
3929
4216
  setTask(out.task)
3930
4217
  setOpen(false)
3931
- } catch { /* причину показывает доска; шапку не ломаем */ }
4218
+ } catch { /* board handles error; keep header intact */ }
3932
4219
  }, [task])
3933
4220
 
3934
4221
  if (!task) return null
3935
4222
 
3936
4223
  const label = [cardRef(task) || task.title, t('column.' + task.column)].filter(Boolean).join(' · ')
3937
- return React.createElement('span', { className: 'dkb-chipWrap' },
3938
- React.createElement('style', null, css),
3939
- React.createElement('button', {
4224
+ ensureCss()
4225
+ return h('span', { className: 'dkb-chipWrap' },
4226
+ h('button', {
3940
4227
  type: 'button', className: 'dkb-chip', title: task.title,
3941
4228
  onClick: () => setOpen((v) => !v),
3942
4229
  }, '▦ ' + label),
3943
- open ? React.createElement('div', { className: 'dkb-chipPanel' },
3944
- React.createElement('span', { className: 'dkb-hint' }, t('chip.moveTo')),
3945
- columns.map((id) => React.createElement('button', {
4230
+ open ? h('div', { className: 'dkb-chipPanel' },
4231
+ h('span', { className: 'dkb-hint' }, t('chip.moveTo')),
4232
+ columns.map((id) => h('button', {
3946
4233
  type: 'button', key: id,
3947
4234
  className: id === task.column ? 'dkb-save' : 'dkb-discard',
3948
4235
  onClick: () => move(id),
3949
4236
  }, t('column.' + id))),
3950
4237
  props.toggle
3951
- ? React.createElement('button', {
4238
+ ? h('button', {
3952
4239
  type: 'button', className: 'dkb-discard',
3953
4240
  onClick: () => { setOpen(false); props.toggle.show(task.id, task.board) },
3954
4241
  }, t('chip.openCard'))
3955
4242
  : null,
3956
- React.createElement('span', { className: 'dkb-hint' }, t('chip.note')),
3957
- React.createElement('textarea', {
4243
+ h('span', { className: 'dkb-hint' }, t('chip.note')),
4244
+ h('textarea', {
3958
4245
  className: 'dkb-area', value: note, disabled: saving,
3959
4246
  onChange: (e) => setNote(e.target.value),
3960
4247
  }),
3961
- // Тело уходит агенту первым сообщением при запуске: заметка до
3962
- // запуска попадёт в работу, после — нет. Молчать об этом значит
3963
- // сделать поведение случайным на вид.
3964
- React.createElement('span', { className: 'dkb-hint' }, t('chip.noteHint')),
3965
- React.createElement('button', {
4248
+ h('span', { className: 'dkb-hint' }, t('chip.noteHint')),
4249
+ h('button', {
3966
4250
  type: 'button', className: 'dkb-save',
3967
4251
  disabled: saving || note.trim() === '',
3968
4252
  onClick: async () => {
@@ -3974,7 +4258,7 @@ window.__ModuleLoader__.load({
3974
4258
  setTask(out.task)
3975
4259
  setNote('')
3976
4260
  setOpen(false)
3977
- } catch { /* причину показывает доска; шапку не ломаем */ } finally {
4261
+ } catch { /* board handles error; keep header intact */ } finally {
3978
4262
  setSaving(false)
3979
4263
  }
3980
4264
  },
@@ -3984,17 +4268,6 @@ window.__ModuleLoader__.load({
3984
4268
  }
3985
4269
 
3986
4270
 
3987
- // ------------------------------------------------- встраивание в оболочку
3988
- //
3989
- // Раздела верхнего уровня в этой сборке нет: слоты `app.section` и
3990
- // `sidebar.section` отсутствуют, а `settings.section` уводит доску в
3991
- // настройки, где ей не место. Поэтому строка и экран вставляются прямо в
3992
- // вёрстку оболочки — тем же приёмом, каким это делает штатный task board.
3993
- //
3994
- // Приём хрупкий по своей природе: он опирается на чужую вёрстку. Смягчение
3995
- // — селекторы по подстроке класса, наблюдатели за перерисовкой React и
3996
- // мягкий отказ: не нашли корень — просто ничего не показали, ничего не
3997
- // сломали.
3998
4271
 
3999
4272
  const PANEL = 'kanban'
4000
4273
  const ACTIVATE_EVENT = 'dsh-panel-activate'
@@ -4012,8 +4285,6 @@ window.__ModuleLoader__.load({
4012
4285
  /** @see helpers.createToggle */
4013
4286
  function createToggle() {
4014
4287
  let open = false
4015
- // Задача, которую просили показать при открытии. Состояние окна живёт
4016
- // внутри экрана доски, и снаружи до него иначе не дотянуться.
4017
4288
  let wanted = undefined
4018
4289
  const listeners = new Set()
4019
4290
  return {
@@ -4026,8 +4297,6 @@ window.__ModuleLoader__.load({
4026
4297
  toggle() { this.set(!open) },
4027
4298
  /** Открыть доску на конкретной задаче. */
4028
4299
  show(taskId, boardId) {
4029
- // Доска едет вместе с задачей: карточка может лежать на простой, а
4030
- // открытой быть проектная, и тогда искать её было бы негде.
4031
4300
  wanted = { id: String(taskId || ''), board: String(boardId || 'main') }
4032
4301
  if (open) { for (const fn of listeners) fn() } else this.set(true)
4033
4302
  },
@@ -4124,8 +4393,6 @@ window.__ModuleLoader__.load({
4124
4393
  else delete entry.dataset.active
4125
4394
  }
4126
4395
 
4127
- // Оболочка перерисовывается сама по себе: без наблюдателя кнопка исчезнет
4128
- // при первом же обновлении списка сессий.
4129
4396
  const observer = new MutationObserver(tryPlace)
4130
4397
  observer.observe(document.body, { childList: true, subtree: true })
4131
4398
  const unsubscribe = toggle.subscribe(syncActive)
@@ -4154,7 +4421,7 @@ window.__ModuleLoader__.load({
4154
4421
  container.setAttribute('data-dsh-plugin', 'kanban')
4155
4422
  column.appendChild(container)
4156
4423
  root = ReactDOM.createRoot(container)
4157
- root.render(React.createElement(BoardScreen, { ctx, toggle, onClose: () => toggle.set(false) }))
4424
+ root.render(h(BoardScreen, { ctx, toggle, onClose: () => toggle.set(false) }))
4158
4425
  }
4159
4426
 
4160
4427
  const applyActive = () => {
@@ -4166,8 +4433,6 @@ window.__ModuleLoader__.load({
4166
4433
  }
4167
4434
  }
4168
4435
 
4169
- // Соседние панели объявляют себя тем же событием: две панели поверх одной
4170
- // колонки одновременно — это каша, поэтому уступаем.
4171
4436
  const onOtherActivate = (event) => {
4172
4437
  if (event.detail !== PANEL && toggle.isOpen()) toggle.set(false)
4173
4438
  }
@@ -4198,7 +4463,6 @@ window.__ModuleLoader__.load({
4198
4463
  }
4199
4464
  }
4200
4465
 
4201
- // ------------------------------------------------------------ регистрация
4202
4466
 
4203
4467
  /**
4204
4468
  * Зарегистрировать компонент в первый слот, который его примет.
@@ -4225,7 +4489,7 @@ window.__ModuleLoader__.load({
4225
4489
  if (registeredInSlot || (res !== null && res !== false && res !== undefined)) {
4226
4490
  return entry.name
4227
4491
  }
4228
- } catch { /* слота нет в этой сборке — пробуем следующий */ }
4492
+ } catch { /* slot missing in build — try next */ }
4229
4493
  }
4230
4494
  return undefined
4231
4495
  }
@@ -4233,32 +4497,25 @@ window.__ModuleLoader__.load({
4233
4497
  helpers.createToggle = createToggle
4234
4498
 
4235
4499
  function apply(ctx) {
4236
- ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-kanban: словари')
4237
-
4238
- // Карточка настроек в Настройки → Плагины → Настройки плагинов.
4239
- // Ключ обязан совпадать с пространством настроек: вкладка перебирает
4240
- // объявленные пространства и рисует слот settings.plugin.item с entryKey,
4241
- // равным имени пространства.
4242
- // Собственный раздел верхнего уровня (settings.section) не регистрируется,
4243
- // чтобы не засорять плоский общий список настроек (#238).
4500
+ ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-kanban: locales')
4501
+
4502
+ refreshMirrorUntilVisible(ctx)
4503
+ const SettingsErrorBoundary = createErrorBoundary()
4504
+ const SafeSettingsCard = (props) => h(SettingsErrorBoundary, null, h(KanbanSettingsCard, props))
4244
4505
  const cardSlot = registerFirst(ctx, [
4245
4506
  { name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
4246
- ], KanbanSettingsCard)
4507
+ ], SafeSettingsCard)
4247
4508
 
4248
- // Экран доски встраивается в оболочку, а не в слот: раздела верхнего
4249
- // уровня в сборке нет, а настройки — не место для доски.
4250
4509
  const toggle = createToggle()
4251
4510
  ctx.effect(() => {
4252
4511
  const off = [mountSidebarEntry(toggle, fallbackT('section.label')), mountBoard(ctx, toggle)]
4253
4512
  return () => { for (const dispose of off) dispose() }
4254
- }, 'dsh-kanban: доска в оболочке')
4513
+ }, 'dsh-kanban: shell board')
4255
4514
 
4256
- // Чип задачи в шапке чата. Слот подтверждён живым плагином dsh-gitea.
4257
4515
  const chipSlot = registerFirst(ctx, [
4258
4516
  { name: 'conversation.session.header.utilities', id: '@goodandready/dsh-kanban.chip', order: 26, locale: NS, inject: () => ({ ctx, toggle }) },
4259
4517
  ], TaskChip)
4260
4518
 
4261
- // Куда встали — видно снаружи: живая проверка сверяет это с интерфейсом.
4262
4519
  exports.slots = { card: cardSlot, chip: chipSlot }
4263
4520
  }
4264
4521