@goodandready/dsh-kanban 0.2.1 → 0.2.7

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
  * Соседи для переноса карточки. Хранилище работает не с числовым индексом,
@@ -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,9 +626,95 @@ 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 (_) {}
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}' +
@@ -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}' +
@@ -797,13 +855,9 @@ 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
861
  '.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}' +
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}}' +
@@ -819,23 +873,21 @@ window.__ModuleLoader__.load({
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:#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:#0d1117;color:#58a6ff;padding:12px;border-radius:8px;font-family:monospace;font-size:11px;max-height:260px;overflow-y:auto;white-space:pre-wrap;border:1px solid #30363d}'
827
888
 
828
889
 
829
- // ---------------------------------------------------------------- строки
830
890
 
831
- // Правило регистра, чтобы подписи не расползались:
832
- //
833
- // подпись УПРАВЛЕНИЯ — с прописной: «Архив», «Поиск», «Обновить». Это
834
- // имя кнопки, а не продолжение фразы;
835
- // встроенный текст и состояние — со строчной: «ждёт ответа», «здесь
836
- // 3 дн», «показано 12 из 81». Это часть предложения, а не ярлык.
837
- //
838
- // Смешение того и другого в одном ряду и выглядит халтурой.
839
891
  const en = {
840
892
  'title': 'Kanban',
841
893
  'subtitle': 'Task board: Gitea issues and a session per task',
@@ -1176,15 +1228,33 @@ window.__ModuleLoader__.load({
1176
1228
  'comments.placeholder': 'Add a note or feedback...',
1177
1229
  'comments.authorUser': 'User',
1178
1230
  'comments.authorAgent': 'Agent',
1231
+ 'epic.badge': 'Epic',
1232
+ 'epic.progress': '{done}/{total} subtasks',
1233
+ 'epic.triage': 'Triage / Decompose',
1234
+ 'epic.triageTitle': 'Decompose Task into Subtasks',
1235
+ 'epic.triageHelp': 'Enter one subtask title per line. Child tasks will be linked to this Epic.',
1236
+ 'epic.triageSubmit': 'Create Subtasks',
1237
+ 'epic.createGiteaIssues': 'Also create linked Gitea issues',
1238
+ 'epic.subtasks': 'Subtasks',
1239
+ 'epic.addSubtask': 'Add subtask',
1240
+ 'epic.noSubtasks': 'No subtasks yet.',
1241
+ 'console.tab': 'Live Console',
1242
+ 'console.empty': 'No agent output recorded yet.',
1243
+ 'filter.all': 'All',
1244
+ 'filter.active': 'Active',
1245
+ 'filter.review': 'Review',
1246
+ 'filter.epics': 'Epics',
1247
+ 'filter.stale': 'Silent',
1248
+ 'pr.create': 'Create Gitea PR',
1249
+ 'pr.creating': 'Creating PR...',
1250
+ 'pr.created': 'PR #{n} created',
1251
+ 'pr.view': 'View PR',
1179
1252
  'error.issueNotFound': 'Issue not found.',
1180
1253
  'error.titleRequired': 'A title is required.',
1181
1254
  'error.crossSite': 'Request rejected as cross-site.',
1182
1255
  'error.unknown': 'Request failed.',
1183
1256
  }
1184
1257
 
1185
- // Китайский — вторая встроенная локаль ядра. Русского здесь нет:
1186
- // он приезжает языковым пакетом `dsh-russian-lang`, который
1187
- // регистрирует `ru` для этого пространства извне.
1188
1258
  const zh = {
1189
1259
  'title': '看板',
1190
1260
  'subtitle': '任务看板:Gitea issue,每个任务一个会话',
@@ -1525,6 +1595,27 @@ window.__ModuleLoader__.load({
1525
1595
  'comments.placeholder': '添加说明或反馈...',
1526
1596
  'comments.authorUser': '用户',
1527
1597
  'comments.authorAgent': 'Agent',
1598
+ 'epic.badge': '史诗',
1599
+ 'epic.progress': '{done}/{total} 子任务',
1600
+ 'epic.triage': '分解子任务',
1601
+ 'epic.triageTitle': '将任务分解为子任务',
1602
+ 'epic.triageHelp': '每行输入一个子任务标题。子任务将关联到此史诗。',
1603
+ 'epic.triageSubmit': '创建子任务',
1604
+ 'epic.createGiteaIssues': '同时在 Gitea 中创建关联 issue',
1605
+ 'epic.subtasks': '子任务',
1606
+ 'epic.addSubtask': '添加子任务',
1607
+ 'epic.noSubtasks': '暂无子任务。',
1608
+ 'console.tab': '实时终端',
1609
+ 'console.empty': '暂无 Agent 活动输出。',
1610
+ 'filter.all': '全部',
1611
+ 'filter.active': '进行中',
1612
+ 'filter.review': '待复核',
1613
+ 'filter.epics': '史诗',
1614
+ 'filter.stale': '沉默',
1615
+ 'pr.create': '创建 Gitea PR',
1616
+ 'pr.creating': '正在创建 PR...',
1617
+ 'pr.created': '已创建 PR #{n}',
1618
+ 'pr.view': '查看 PR',
1528
1619
  'error.issueNotFound': '没有找到 issue。',
1529
1620
  'error.titleRequired': '标题不能为空。',
1530
1621
  'error.crossSite': '请求被判定为跨站而拒绝。',
@@ -1532,8 +1623,6 @@ window.__ModuleLoader__.load({
1532
1623
  }
1533
1624
 
1534
1625
  function fallbackT(key, vars) {
1535
- // Запасной перевод — английский: русский приезжает языковым пакетом,
1536
- // а до службы локализации доска обязана говорить хоть на чём-то.
1537
1626
  let text = en[key] !== undefined ? en[key] : key
1538
1627
  if (vars) {
1539
1628
  for (const name of Object.keys(vars)) {
@@ -1543,7 +1632,6 @@ window.__ModuleLoader__.load({
1543
1632
  return text
1544
1633
  }
1545
1634
 
1546
- // ------------------------------------------------------- карточка настроек
1547
1635
 
1548
1636
  const FIELDS = [
1549
1637
  { field: 'giteaUrl', kind: 'text', group: 'gitea' },
@@ -1584,18 +1672,16 @@ window.__ModuleLoader__.load({
1584
1672
  }
1585
1673
 
1586
1674
  function Chevron(props) {
1587
- return React.createElement('svg', {
1675
+ return h('svg', {
1588
1676
  className: 'dkb-chevron' + (props.open ? ' dkb-chevronOpen' : ''),
1589
1677
  width: 16, height: 16, viewBox: '0 0 16 16', 'aria-hidden': 'true',
1590
- }, React.createElement('path', {
1678
+ }, h('path', {
1591
1679
  d: 'M4 6l4 4 4-4', fill: 'none', stroke: 'currentColor', strokeWidth: '1.5',
1592
1680
  strokeLinecap: 'round', strokeLinejoin: 'round',
1593
1681
  }))
1594
1682
  }
1595
1683
 
1596
1684
  function KanbanSettingsCard(props) {
1597
- // Все хуки объявлены выше любого возврата: ранний возврат над хуком даёт
1598
- // React error 310 при первом же изменении состояния.
1599
1685
  const ctx = props.ctx
1600
1686
  const t = props.t || fallbackT
1601
1687
  const [open, setOpen] = React.useState(false)
@@ -1605,7 +1691,10 @@ window.__ModuleLoader__.load({
1605
1691
  const [saved, setSaved] = React.useState(false)
1606
1692
 
1607
1693
  const scope = React.useMemo(
1608
- () => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
1694
+ () => {
1695
+ const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1696
+ return s ? s.bind({ namespace: NS }) : undefined
1697
+ },
1609
1698
  [ctx],
1610
1699
  )
1611
1700
  const snapshot = React.useSyncExternalStore(
@@ -1633,9 +1722,6 @@ window.__ModuleLoader__.load({
1633
1722
  const save = React.useCallback(async () => {
1634
1723
  if (!scope || !draft) return
1635
1724
  setSaving(true); setFailed(''); setSaved(false)
1636
- // Пишем ВСЕ поля, а не до первой ошибки: обрыв на первой неудаче
1637
- // оставил бы остальные поля незаписанными, а снаружи это выглядит как
1638
- // «кнопка не работает».
1639
1725
  const broken = []
1640
1726
  for (const spec of FIELDS) {
1641
1727
  if (!draft || !Object.prototype.hasOwnProperty.call(draft, spec.field)) continue
@@ -1657,82 +1743,108 @@ window.__ModuleLoader__.load({
1657
1743
  setDraft(null); setSaved(true)
1658
1744
  }, [scope, draft, t])
1659
1745
 
1660
- const head = React.createElement('button', {
1746
+ const head = h('button', {
1661
1747
  type: 'button', className: 'dkb-head', onClick: () => setOpen((v) => !v),
1662
1748
  },
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')),
1749
+ h('span', { className: 'dkb-headText' },
1750
+ h('span', { className: 'dkb-title' }, t('title')),
1751
+ h('span', { className: 'dkb-sub' }, t('subtitle')),
1666
1752
  ),
1667
- React.createElement(Chevron, { open }),
1753
+ h(Chevron, { open }),
1668
1754
  )
1669
- const style = React.createElement('style', null, css)
1755
+ ensureCss()
1756
+ const style = null
1670
1757
 
1671
- if (!open) return React.createElement('li', { className: 'dkb-card' }, style, head)
1758
+ if (!open) return h('li', { className: 'dkb-card' }, head)
1672
1759
 
1673
1760
  let body
1674
1761
  if (status === 'loading') {
1675
- body = React.createElement('p', { className: 'dkb-note', role: 'status' }, t('settings.loading'))
1762
+ body = h('p', { className: 'dkb-note', role: 'status' }, t('settings.loading'))
1676
1763
  } else if (status !== 'ready') {
1677
- // Проверяем СТАТУС, а не значение: при `unavailable` признак writable
1678
- // остаётся истинным, и карточка нарисовала бы пустую, но с виду
1679
- // рабочую форму.
1680
- body = React.createElement('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
1764
+ body = h('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
1681
1765
  } else {
1682
1766
  const writable = !snapshot || snapshot.writable !== false
1683
- const rows = []
1684
- let lastGroup
1767
+ const groups = []
1768
+ const groupMap = {}
1685
1769
  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)))
1770
+ if (!groupMap[spec.group]) {
1771
+ groupMap[spec.group] = []
1772
+ groups.push(spec.group)
1689
1773
  }
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))),
1774
+ groupMap[spec.group].push(spec)
1775
+ }
1776
+
1777
+ const rows = []
1778
+ for (const grp of groups) {
1779
+ const fieldsInGroup = groupMap[grp]
1780
+ const fieldElements = []
1781
+ for (const spec of fieldsInGroup) {
1782
+ if (spec.kind === 'bool') {
1783
+ fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
1784
+ h('label', { className: 'dkb-row' },
1785
+ h('input', {
1786
+ type: 'checkbox',
1787
+ checked: String(valueOf(spec.field)) === 'true',
1788
+ disabled: !writable || saving,
1789
+ onChange: (e) => edit(spec.field, e.target.checked ? 'true' : 'false'),
1790
+ }),
1791
+ h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
1792
+ ),
1793
+ h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
1794
+ ))
1795
+ continue
1796
+ }
1797
+ const common = {
1798
+ className: spec.kind === 'area' ? 'dkb-area' : 'dkb-input',
1799
+ value: valueOf(spec.field),
1800
+ disabled: !writable || saving,
1801
+ onChange: (e) => edit(spec.field, e.target.value),
1802
+ }
1803
+ if (spec.kind === 'number') common.type = 'number'
1804
+ fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
1805
+ h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
1806
+ h(spec.kind === 'area' ? 'textarea' : 'input', common),
1807
+ h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
1704
1808
  ))
1705
- continue
1706
1809
  }
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),
1810
+
1811
+ let badge = null
1812
+ if (grp === 'gitea') {
1813
+ const hasUrl = Boolean(valueOf('giteaUrl'))
1814
+ badge = h('span', {
1815
+ className: 'dkb-section-badge' + (hasUrl ? ' dkb-section-badge-ok' : ''),
1816
+ }, hasUrl ? 'Gitea Configured' : 'No URL')
1817
+ } else if (grp === 'sync') {
1818
+ const interval = Number(valueOf('syncIntervalSec')) || 0
1819
+ badge = h('span', {
1820
+ className: 'dkb-section-badge' + (interval > 0 ? ' dkb-section-badge-ok' : ''),
1821
+ }, interval > 0 ? `${interval}s` : 'Manual')
1712
1822
  }
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))),
1823
+
1824
+ rows.push(h('div', { className: 'dkb-section-card', key: 'g-' + grp },
1825
+ h('div', { className: 'dkb-section-title' },
1826
+ h('span', null, t('group.' + grp)),
1827
+ badge,
1828
+ ),
1829
+ ...fieldElements,
1718
1830
  ))
1719
1831
  }
1720
1832
  const dirty = draft !== null && Object.keys(draft).length > 0
1721
1833
  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' },
1834
+ if (failed) note = h('p', { className: 'dkb-failed', role: 'status' }, failed)
1835
+ else if (!writable) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.readOnly'))
1836
+ else if (saved) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.saved'))
1837
+ else note = h('p', { className: 'dkb-note' })
1838
+ rows.push(h('div', { className: 'dkb-foot', key: 'foot' },
1727
1839
  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')),
1840
+ h('button', { type: 'button', className: 'dkb-discard', disabled: !dirty || saving, onClick: discard }, t('discard')),
1841
+ h('button', { type: 'button', className: 'dkb-save', disabled: !dirty || saving || !writable, onClick: save }, t('save')),
1730
1842
  ))
1731
1843
  body = rows
1732
1844
  }
1733
1845
 
1734
- return React.createElement('li', { className: 'dkb-card dkb-cardOpen' },
1735
- style, head, React.createElement('div', { className: 'dkb-body' }, body))
1846
+ return h('li', { className: 'dkb-card dkb-cardOpen' },
1847
+ head, h('div', { className: 'dkb-body' }, body))
1736
1848
  }
1737
1849
 
1738
1850
  /** Стрелка влево для кнопки возврата: 14x14, обводка цветом текста. */
@@ -1740,9 +1852,8 @@ window.__ModuleLoader__.load({
1740
1852
  + ' stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
1741
1853
  + '<path d="M10 3.5 5.5 8l4.5 4.5"/></svg>'
1742
1854
 
1743
- // ------------------------------------------------------------ экран доски
1744
1855
 
1745
- function TaskCard(props) {
1856
+ const TaskCard = React.memo(function TaskCard(props) {
1746
1857
  const { task, t, now } = props
1747
1858
  const ref = cardRef(task)
1748
1859
  const status = cardStatus(task)
@@ -1752,26 +1863,19 @@ window.__ModuleLoader__.load({
1752
1863
  ? t('state.queued', { n: task.queuePos || 1 })
1753
1864
  : stateLabel(task, t)
1754
1865
  const pack = props.pack
1755
- // Дата заведения, а не время в колонке. «Здесь 34 минуты» у задачи,
1756
- // приехавшей подхватом полчаса назад, не значит ничего: это возраст
1757
- // карточки на доске, а спрашивают про возраст самой задачи.
1758
1866
  const born = shortDate(task.createdAt)
1759
1867
  const touched = task.sessionId ? agoText(now, task.updatedAt, t) : ''
1760
1868
  const menu = props.menuOpen
1761
1869
  const urgent = priorityColor(task)
1762
1870
  const overdue = task.overdue === true
1763
- return React.createElement('div', {
1871
+ return h('div', {
1764
1872
  className: 'dkb-taskCard' + (urgent ? ' dkb-urgent' : '')
1765
1873
  + (overdue ? ' dkb-overdue' : '')
1766
1874
  + (props.sameSession ? ' dkb-sameSession' : ''),
1767
1875
  onMouseEnter: () => { if (props.onHover) props.onHover(task.sessionId || '') },
1768
1876
  onMouseLeave: () => { if (props.onHover) props.onHover('') },
1769
1877
  style: urgent ? { borderLeftColor: urgent } : undefined,
1770
- // Полный заголовок в подсказке: на карточке он обрезан двумя строками,
1771
- // и прочитать его иначе можно было только открыв её.
1772
1878
  title: task.title,
1773
- // Карточка становится точкой табуляции: доска была мышиной целиком, а
1774
- // перенос теперь ещё и команда агенту — действие с последствиями.
1775
1879
  tabIndex: 0,
1776
1880
  draggable: true,
1777
1881
  onDragStart: (e) => {
@@ -1780,10 +1884,8 @@ window.__ModuleLoader__.load({
1780
1884
  },
1781
1885
  onClick: () => props.onOpen(task),
1782
1886
  },
1783
- // Шапка карточки: чей это проект и что за работа. Оба ответа нужны
1784
- // раньше заголовка — по ним карточку и находят глазами в колонке.
1785
- React.createElement('div', { className: 'dkb-taskHead' },
1786
- React.createElement('input', {
1887
+ h('div', { className: 'dkb-taskHead' },
1888
+ h('input', {
1787
1889
  type: 'checkbox',
1788
1890
  className: 'dkb-pick' + (props.picked ? ' dkb-pickOn' : ''),
1789
1891
  checked: props.picked === true,
@@ -1791,113 +1893,114 @@ window.__ModuleLoader__.load({
1791
1893
  onClick: (e) => e.stopPropagation(),
1792
1894
  onChange: () => props.onPick(task),
1793
1895
  }),
1794
- waiting ? React.createElement('span', { className: 'dkb-waitDot' }) : null,
1795
- waiting ? React.createElement('span', { className: 'dkb-waiting' }, t('board.waiting')) : null,
1896
+ waiting ? h('span', { className: 'dkb-waitDot' }) : null,
1897
+ waiting ? h('span', { className: 'dkb-waiting' }, t('board.waiting')) : null,
1796
1898
  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,
1899
+ task.isEpic ? h('span', { className: 'dkb-tag dkb-epicTag', title: t('epic.badge') }, '👑 ' + t('epic.badge')) : null,
1900
+ task.parentId ? h('span', { className: 'dkb-tag', style: { opacity: 0.75 } }, '↳') : null,
1901
+ task.subtasksCount && task.subtasksCount.total > 0
1902
+ ? h('span', { className: 'dkb-tag', title: 'Subtasks' }, t('epic.progress', { done: task.subtasksCount.completed, total: task.subtasksCount.total }))
1903
+ : null,
1904
+ task.cron ? h('span', { className: 'dkb-tag', title: task.cron }, '⏰ cron') : null,
1905
+ task.progressDump ? h('span', { className: 'dkb-tag', title: 'PROGRESSDUMP' }, '📋 dump') : null,
1906
+ 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
1907
  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}`)
1908
+ ? h('span', { className: 'dkb-tag', title: 'DoD Checklist' }, `☑ ${task.checklist.filter((i) => i.completed).length}/${task.checklist.length}`)
1802
1909
  : null,
1803
1910
  task.report && task.report.summary
1804
- ? React.createElement('span', { className: 'dkb-tag', title: 'Execution Report' }, '📄 report')
1911
+ ? h('span', { className: 'dkb-tag', title: 'Execution Report' }, '📄 report')
1805
1912
  : null,
1806
1913
  Array.isArray(task.comments) && task.comments.length > 0
1807
- ? React.createElement('span', { className: 'dkb-tag', title: 'Comments' }, `💬 ${task.comments.length}`)
1914
+ ? h('span', { className: 'dkb-tag', title: 'Comments' }, `💬 ${task.comments.length}`)
1808
1915
  : null,
1809
- (task.labels || []).map((l) => React.createElement('span', {
1916
+ (task.labels || []).map((l) => h('span', {
1810
1917
  className: 'dkb-tag', key: l, style: tagStyle(task, l),
1811
1918
  }, 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', {
1919
+ h('div', { className: 'dkb-taskTitle' }, task.title),
1920
+ task.subtasksCount && task.subtasksCount.total > 0
1921
+ ? h('div', { className: 'dkb-progressBar', title: `${task.subtasksCount.completed}/${task.subtasksCount.total}` },
1922
+ h('div', {
1923
+ className: 'dkb-progressFill',
1924
+ style: { width: Math.round((task.subtasksCount.completed / task.subtasksCount.total) * 100) + '%' },
1925
+ }))
1926
+ : null,
1927
+ plan ? h('div', { className: 'dkb-planLine' }, plan) : null,
1928
+ h('div', { className: 'dkb-cardMenu' },
1929
+ h('button', {
1819
1930
  type: 'button', className: 'dkb-moveBtn',
1820
- // Скрыто до наведения — но появляется и при фокусе с клавиатуры:
1821
- // прятать ради красоты то, что чинили ради доступности, нельзя.
1822
1931
  'aria-expanded': menu === task.id,
1823
1932
  onClick: (e) => { e.stopPropagation(); props.onMenu(menu === task.id ? '' : task.id) },
1824
1933
  }, t('card.move')),
1825
1934
  menu === task.id
1826
- ? React.createElement('div', { className: 'dkb-facetList' },
1827
- (props.columns || []).filter((c) => c !== task.column).map((c) => React.createElement('button', {
1935
+ ? h('div', { className: 'dkb-facetList' },
1936
+ (props.columns || []).filter((c) => c !== task.column).map((c) => h('button', {
1828
1937
  type: 'button', className: 'dkb-facetRow', key: c,
1829
1938
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onMove(task, c) },
1830
1939
  }, t('column.' + c))),
1831
- // Очередь стоит рядом с переносом: и то и другое — «отдать
1832
- // работу», разница лишь в том, кому и когда.
1833
- // Останавливать нечего у того, кто не идёт: пункт, который
1834
- // ничего не делает, хуже отсутствующего.
1835
1940
  task.state === 'stopped'
1836
- ? React.createElement('button', {
1941
+ ? h('button', {
1837
1942
  type: 'button', className: 'dkb-facetRow', key: 'revive',
1838
1943
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onRevive(task) },
1839
1944
  }, t('card.revive'))
1840
1945
  : null,
1841
1946
  task.state === 'running' || task.state === 'waiting'
1842
- ? React.createElement('button', {
1947
+ ? h('button', {
1843
1948
  type: 'button', className: 'dkb-facetRow', key: 'stop',
1844
1949
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onStop(task) },
1845
1950
  }, t('card.stop'))
1846
1951
  : null,
1847
1952
  task.state === 'queued'
1848
- ? React.createElement('button', {
1953
+ ? h('button', {
1849
1954
  type: 'button', className: 'dkb-facetRow', key: 'unqueue',
1850
1955
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onUnqueue(task) },
1851
1956
  }, t('card.unqueue'))
1852
- : React.createElement('button', {
1957
+ : h('button', {
1853
1958
  type: 'button', className: 'dkb-facetRow', key: 'queue',
1854
1959
  onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onQueue(task) },
1855
1960
  }, t('card.queue')))
1856
1961
  : null,
1857
1962
  ),
1858
1963
  status || state || born || touched
1859
- ? React.createElement('div', { className: 'dkb-taskRef' },
1964
+ ? h('div', { className: 'dkb-taskRef' },
1860
1965
  state
1861
- ? React.createElement('span', {
1966
+ ? h('span', {
1862
1967
  className: 'dkb-state' + (task.state === 'running' ? ' dkb-stateLive' : ''),
1863
1968
  }, state)
1864
1969
  : null,
1865
- // Ссылка в чат стоит рядом с отметкой работы: «идёт» без пути к
1866
- // тому, что идёт, — сведение, которым нельзя воспользоваться.
1867
1970
  task.sessionId
1868
- ? React.createElement('button', {
1971
+ ? h('button', {
1869
1972
  type: 'button', className: 'dkb-toChat',
1870
1973
  onClick: (e) => { e.stopPropagation(); props.onOpenChat(task) },
1871
1974
  }, t('card.toChat'))
1872
1975
  : null,
1873
1976
  touched
1874
- ? React.createElement('span', {
1977
+ ? h('span', {
1875
1978
  className: task.stale ? 'dkb-countOver' : '',
1876
1979
  title: exactAt(task.updatedAt),
1877
1980
  }, touched)
1878
1981
  : null,
1879
1982
  born
1880
- ? React.createElement('span', { title: exactAt(task.createdAt) }, born)
1983
+ ? h('span', { title: exactAt(task.createdAt) }, born)
1881
1984
  : null,
1882
1985
  task.dueAt > 0
1883
- ? React.createElement('span', {
1986
+ ? h('span', {
1884
1987
  className: overdue ? 'dkb-dueOver' : '',
1885
1988
  title: exactAt(task.dueAt),
1886
1989
  }, t('card.due', { date: shortDate(task.dueAt) }))
1887
1990
  : null,
1888
1991
  task.assignee
1889
- ? React.createElement('span', { className: 'dkb-who', title: t('card.assigneeHint') },
1992
+ ? h('span', { className: 'dkb-who', title: t('card.assigneeHint') },
1890
1993
  '@' + task.assignee)
1891
1994
  : null,
1892
1995
  pack
1893
- ? React.createElement('span', {
1996
+ ? h('span', {
1894
1997
  className: 'dkb-pack', title: t('card.packHint'),
1895
1998
  }, t('card.pack', { at: pack.at, total: pack.total }))
1896
1999
  : null,
1897
- status ? React.createElement('span', null, status) : null)
2000
+ status ? h('span', null, status) : null)
1898
2001
  : null,
1899
2002
  )
1900
- }
2003
+ })
1901
2004
 
1902
2005
  /**
1903
2006
  * Чем запускать: профиль агента и уровень доступа.
@@ -1913,36 +2016,34 @@ window.__ModuleLoader__.load({
1913
2016
  const t = props.t
1914
2017
  const rows = []
1915
2018
  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', {
2019
+ rows.push(h('div', { className: 'dkb-field', key: 'preset' },
2020
+ h('span', { className: 'dkb-label' }, t('panel.agentPreset')),
2021
+ h('select', {
1919
2022
  className: 'dkb-input', value: props.agentPreset, disabled: props.disabled,
1920
2023
  onChange: (e) => props.onPreset(e.target.value),
1921
2024
  },
1922
- React.createElement('option', { value: '' }, t('panel.presetDefault')),
1923
- props.presets.agentPresets.map((one) => React.createElement('option', {
2025
+ h('option', { value: '' }, t('panel.presetDefault')),
2026
+ props.presets.agentPresets.map((one) => h('option', {
1924
2027
  value: one.id, key: one.id, title: one.description || '',
1925
2028
  }, one.name || one.id))),
1926
2029
  ))
1927
2030
  }
1928
2031
  if ((props.presets.access || []).length > 0) {
1929
2032
  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', {
2033
+ rows.push(h('div', { className: 'dkb-field', key: 'access' },
2034
+ h('span', { className: 'dkb-label' }, t('panel.permission')),
2035
+ h('select', {
1933
2036
  className: 'dkb-input', value: props.permission, disabled: props.disabled,
1934
2037
  onChange: (e) => props.onPermission(e.target.value),
1935
- }, props.presets.access.map((one) => React.createElement('option', {
2038
+ }, props.presets.access.map((one) => h('option', {
1936
2039
  value: one.value, key: one.value,
1937
2040
  }, one.name || one.value))),
1938
- // Пояснение к уровню доступа пишет ядро: свои слова разошлись бы с
1939
- // тем, что этот уровень значит на самом деле.
1940
2041
  picked && picked.description
1941
- ? React.createElement('p', { className: 'dkb-hint' }, picked.description)
2042
+ ? h('p', { className: 'dkb-hint' }, picked.description)
1942
2043
  : null,
1943
2044
  ))
1944
2045
  }
1945
- return rows.length === 0 ? null : React.createElement('div', { className: 'dkb-start' }, rows)
2046
+ return rows.length === 0 ? null : h('div', { className: 'dkb-start' }, rows)
1946
2047
  }
1947
2048
 
1948
2049
  function BoardScreen(props) {
@@ -1954,11 +2055,17 @@ window.__ModuleLoader__.load({
1954
2055
  const [rejectReasonText, setRejectReasonText] = React.useState('')
1955
2056
  const [newChecklistText, setNewChecklistText] = React.useState('')
1956
2057
  const [newCommentText, setNewCommentText] = React.useState('')
2058
+ const [quickFilter, setQuickFilter] = React.useState('all')
2059
+ const [subtasks, setSubtasks] = React.useState([])
2060
+ const [triageOpen, setTriageOpen] = React.useState(false)
2061
+ const [triageText, setTriageText] = React.useState('')
2062
+ const [triageGitea, setTriageGitea] = React.useState(false)
2063
+ const [triageSubmitting, setTriageSubmitting] = React.useState(false)
2064
+ const [newSubtaskTitle, setNewSubtaskTitle] = React.useState('')
2065
+ const [prCreating, setPrCreating] = React.useState(false)
2066
+ const [createdPr, setCreatedPr] = React.useState(null)
1957
2067
  const [log, setLog] = React.useState([])
1958
2068
  const [dialog, setDialog] = React.useState(null)
1959
- // Черновик правки приоритета и меток в окне задачи. Живёт отдельно от
1960
- // карточки: пока человек не нажал «Сохранить», доска не должна видеть
1961
- // полувведённое.
1962
2069
  const [editPriority, setEditPriority] = React.useState('')
1963
2070
  const [editLabels, setEditLabels] = React.useState('')
1964
2071
  const [editDue, setEditDue] = React.useState('')
@@ -1971,41 +2078,30 @@ window.__ModuleLoader__.load({
1971
2078
  const [starting, setStarting] = React.useState(false)
1972
2079
  const [draftText, setDraftText] = React.useState('')
1973
2080
  const [query, setQuery] = React.useState('')
2081
+ const [debouncedQuery, setDebouncedQuery] = React.useState('')
2082
+ React.useEffect(() => {
2083
+ const timer = setTimeout(() => setDebouncedQuery(query), 150)
2084
+ return () => clearTimeout(timer)
2085
+ }, [query])
1974
2086
  const [dropAt, setDropAt] = React.useState(null)
1975
2087
  const [board, setBoard] = React.useState('main')
1976
- // Решения о сворачивании держим в состоянии экрана, а не в хранилище:
1977
- // это взгляд одного человека на одну доску прямо сейчас, а не свойство
1978
- // задач. Обновление доски их переживает, перезагрузка страницы — нет.
1979
2088
  const [collapsed, setCollapsed] = React.useState({})
1980
- // Отборы живут в состоянии экрана: он смонтирован всё время работы
1981
- // харнесса, поэтому выбор переживает и закрытие доски, и обновление.
1982
2089
  const [filters, setFilters] = React.useState({})
1983
2090
  const [openFacet, setOpenFacet] = React.useState('')
1984
2091
  const [cardMenu, setCardMenu] = React.useState('')
1985
2092
  const [body, setBody] = React.useState(null)
1986
2093
  const [groups, setGroups] = React.useState({})
1987
2094
  const [syncing, setSyncing] = React.useState(false)
1988
- // Отмеченные для группового запуска. Живут отдельно от открытой карточки:
1989
- // выбор переживает и открытие окна, и обновление доски.
1990
2095
  const [picked, setPicked] = React.useState({})
1991
2096
  const [archive, setArchive] = React.useState(null)
1992
2097
  const [metrics, setMetrics] = React.useState(null)
1993
- // «Только ждущие» держим отдельно от отборов: это не значение измерения,
1994
- // а взгляд «покажи, где стоит работа», и складывается он с любым отбором.
1995
2098
  const [onlyWaiting, setOnlyWaiting] = React.useState(false)
1996
2099
  const [onlyOverdue, setOnlyOverdue] = React.useState(false)
1997
2100
  const [hoverSession, setHoverSession] = React.useState('')
1998
2101
  const [order, setOrder] = React.useState('manual')
1999
- // «По проектам» — как приходит с сервера; «по ответственному» — своя
2000
- // раскладка поверх того же порядка.
2001
2102
  const [grouping, setGrouping] = React.useState('repo')
2002
- // Кто «я»: спрашивается один раз у сервера. Пусто — Gitea не ответил, и
2003
- // кнопку «Мои» рисовать нельзя: соврать числом хуже, чем не показать.
2004
2103
  const [me, setMe] = React.useState('')
2005
2104
  const [onlyMine, setOnlyMine] = React.useState(false)
2006
- // Чем запускать: профиль агента и уровень доступа. Обе службы у ядра
2007
- // необязательны, поэтому пустой список — не поломка, а «выбирать не из
2008
- // чего»: поле тогда не рисуется вовсе.
2009
2105
  const [presets, setPresets] = React.useState({ agentPresets: [], access: [] })
2010
2106
  const [agentPreset, setAgentPreset] = React.useState('')
2011
2107
  const [permission, setPermission] = React.useState('')
@@ -2013,9 +2109,6 @@ window.__ModuleLoader__.load({
2013
2109
 
2014
2110
  const load = React.useCallback(async (nextBoard) => {
2015
2111
  try {
2016
- // Отбор по репозиторию делает браузер вместе с остальными отборами:
2017
- // серверный отбор спорил бы с ним и прятал бы часть значений из
2018
- // списка, потому что их не было бы в ответе.
2019
2112
  const query = '?board=' + encodeURIComponent(nextBoard || 'main')
2020
2113
  setState(normalizeBoard(await api('/board' + query)))
2021
2114
  setError('')
@@ -2026,7 +2119,6 @@ window.__ModuleLoader__.load({
2026
2119
 
2027
2120
  React.useEffect(() => { load(board) }, [load, board])
2028
2121
 
2029
- // Подписка на Server-Sent Events (SSE) для мгновенного обновления доски (#222).
2030
2122
  React.useEffect(() => {
2031
2123
  if (typeof window === 'undefined' || typeof window.EventSource !== 'function') return undefined
2032
2124
  let es = null
@@ -2035,12 +2127,16 @@ window.__ModuleLoader__.load({
2035
2127
  const connect = () => {
2036
2128
  try {
2037
2129
  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
- })
2130
+ let reloadTimer = null
2131
+ const scheduleReload = () => {
2132
+ if (reloadTimer) return
2133
+ reloadTimer = setTimeout(() => {
2134
+ reloadTimer = null
2135
+ load(board)
2136
+ }, 60)
2137
+ }
2138
+ es.addEventListener('board:change', scheduleReload)
2139
+ es.addEventListener('task:change', scheduleReload)
2044
2140
  es.onerror = () => {
2045
2141
  if (es) {
2046
2142
  try { es.close() } catch {}
@@ -2067,8 +2163,6 @@ window.__ModuleLoader__.load({
2067
2163
  }
2068
2164
  }, [load, board])
2069
2165
 
2070
- // Просьба чипа «покажи вот эту». Забираем её один раз: повтор открывал
2071
- // бы окно снова после каждого закрытия.
2072
2166
  const [wanted, setWanted] = React.useState('')
2073
2167
  React.useEffect(() => {
2074
2168
  const toggle = props.toggle
@@ -2083,8 +2177,6 @@ window.__ModuleLoader__.load({
2083
2177
  return toggle.subscribe(pull)
2084
2178
  }, [props.toggle])
2085
2179
 
2086
- // Открываем, когда задача приехала: доска могла переключаться, и в
2087
- // момент просьбы её ещё не было в списке.
2088
2180
  React.useEffect(() => {
2089
2181
  if (wanted === '' || state === null) return
2090
2182
  const task = state.tasks.find((x) => x.id === wanted)
@@ -2093,9 +2185,6 @@ window.__ModuleLoader__.load({
2093
2185
  openPanelRef.current(task)
2094
2186
  }, [wanted, state])
2095
2187
 
2096
- // Escape снимает по одному слою за нажатие: диалог, затем панель, затем
2097
- // саму доску. Закрывать всё разом — терять контекст, которого человек не
2098
- // просил лишаться.
2099
2188
  React.useEffect(() => {
2100
2189
  const onKey = (e) => {
2101
2190
  if (e.key !== 'Escape') return
@@ -2107,7 +2196,6 @@ window.__ModuleLoader__.load({
2107
2196
  return () => document.removeEventListener('keydown', onKey)
2108
2197
  }, [dialog, openTask, props])
2109
2198
 
2110
- // Сначала провайдеры и текущий выбор, затем модели выбранного провайдера.
2111
2199
  React.useEffect(() => {
2112
2200
  let alive = true
2113
2201
  api('/models')
@@ -2119,8 +2207,6 @@ window.__ModuleLoader__.load({
2119
2207
  const provider = stillOffered(saved.provider, list.map((p) => p.id), '')
2120
2208
  if (provider !== '') {
2121
2209
  setProvider(provider)
2122
- // Модель проверяется своим списком, когда он приедет: здесь её
2123
- // ещё не с чем сверить, а подставить чужую нельзя.
2124
2210
  setModel(typeof saved.model === 'string' ? saved.model : '')
2125
2211
  } else if (out.current) {
2126
2212
  setProvider(out.current.provider || '')
@@ -2142,8 +2228,6 @@ window.__ModuleLoader__.load({
2142
2228
  const rows = out.agentPresets || []
2143
2229
  const access = out.access || []
2144
2230
  setPresets({ agentPresets: rows, access })
2145
- // Пустое значение — «как у харнесса по умолчанию»; подставляем его
2146
- // же явно, чтобы человек видел, с чем задача пойдёт.
2147
2231
  const saved = recallLaunch(board)
2148
2232
  setAgentPreset(stillOffered(saved.agentPreset, rows.map((x) => x.id), out.agentPresetDefault || ''))
2149
2233
  setPermission(stillOffered(saved.permission, access.map((x) => x.value), out.accessDefault || ''))
@@ -2160,8 +2244,6 @@ window.__ModuleLoader__.load({
2160
2244
  if (!alive || !out) return
2161
2245
  const list = out.models || []
2162
2246
  setModels(list)
2163
- // Модель могла исчезнуть у провайдера: подставленная из памяти, она
2164
- // осталась бы в поле, а запуск отказал бы уже после нажатия.
2165
2247
  setModel((cur) => stillOffered(cur, list.map((m) => m.id), ''))
2166
2248
  })
2167
2249
  .catch(() => { if (alive) setModels([]) })
@@ -2169,8 +2251,6 @@ window.__ModuleLoader__.load({
2169
2251
  }, [provider])
2170
2252
 
2171
2253
  const commitMove = React.useCallback(async (task, column, where) => {
2172
- // Карточку не перерисовываем заранее: до подтверждения она обязана
2173
- // оставаться там, где лежала, иначе отмена выглядит откатом.
2174
2254
  try {
2175
2255
  await api('/task/' + encodeURIComponent(task.id) + '/move', {
2176
2256
  method: 'POST',
@@ -2192,7 +2272,6 @@ window.__ModuleLoader__.load({
2192
2272
  const askMove = React.useCallback((task, column, where) => {
2193
2273
  if (!task) return
2194
2274
  const place = where ?? neighboursFor(tasksOf((state && state.tasks) || [], column), task.id, Infinity)
2195
- // Перенос внутри колонки — это порядок, а не команда: спрашивать не о чем.
2196
2275
  if (task.column === column) { commitMove(task, column, place); return }
2197
2276
  setDialog({ mode: 'move', task, column, where: place })
2198
2277
  }, [state, commitMove])
@@ -2203,7 +2282,6 @@ window.__ModuleLoader__.load({
2203
2282
  setOverColumn('')
2204
2283
  setDropAt(null)
2205
2284
  if (!task || !state) return
2206
- // Мышь слишком легко задевает карточку, чтобы перенос уходил без спроса.
2207
2285
  askMove(task, column, neighboursFor(tasksOf(state.tasks, column), task.id, index))
2208
2286
  }, [state, askMove])
2209
2287
 
@@ -2218,6 +2296,15 @@ window.__ModuleLoader__.load({
2218
2296
  ? new Date(task.dueAt).toISOString().slice(0, 10)
2219
2297
  : '')
2220
2298
  setEditCron(typeof task?.cron === 'string' ? task.cron : '')
2299
+ setTriageOpen(false)
2300
+ setTriageText('')
2301
+ setTriageGitea(false)
2302
+ setCreatedPr(null)
2303
+ setNewSubtaskTitle('')
2304
+ setSubtasks([])
2305
+ api('/task/' + encodeURIComponent(task.id) + '/subtasks')
2306
+ .then((out) => setSubtasks((out && out.subtasks) || []))
2307
+ .catch(() => setSubtasks([]))
2221
2308
  api('/task/' + encodeURIComponent(task.id) + '/body')
2222
2309
  .then((out) => setBody((out && out.blocks) || []))
2223
2310
  .catch(() => setBody([]))
@@ -2231,75 +2318,65 @@ window.__ModuleLoader__.load({
2231
2318
  }, [])
2232
2319
  openPanelRef.current = openPanel
2233
2320
 
2234
- const style = React.createElement('style', null, css)
2321
+ ensureCss()
2322
+ const style = null
2235
2323
 
2236
2324
  if (state === null) {
2237
- return React.createElement('div', { className: 'dkb-screen' }, style,
2238
- React.createElement('p', { className: 'dkb-note' }, error || t('board.loading')))
2325
+ return h('div', { className: 'dkb-screen' },
2326
+ h('p', { className: 'dkb-note' }, error || t('board.loading')))
2239
2327
  }
2240
2328
 
2241
2329
  const boards = (state && state.boards) || []
2242
- const bar = React.createElement('div', { className: 'dkb-bar' },
2243
- // Первой в шапке и слева от заголовка — там она читается как «назад».
2244
- // Своего механизма закрытия не заводит: их уже три (Escape, повторный
2245
- // щелчок по боковой строке, переход в чужую сессию), и четвёртый
2246
- // разошёлся бы с ними при первой же правке.
2247
- React.createElement('button', {
2330
+ const bar = h('div', { className: 'dkb-bar' },
2331
+ h('button', {
2248
2332
  type: 'button', className: 'dkb-back', title: t('board.backHint'),
2249
2333
  onClick: () => { if (props.onClose) props.onClose() },
2250
2334
  },
2251
- React.createElement('span', {
2335
+ h('span', {
2252
2336
  className: 'dkb-backIcon',
2253
2337
  dangerouslySetInnerHTML: { __html: BACK_ICON },
2254
2338
  }),
2255
- React.createElement('span', null, t('board.back')),
2339
+ h('span', null, t('board.back')),
2256
2340
  ),
2257
- React.createElement('span', { className: 'dkb-barTitle' }, t('section.label')),
2341
+ h('span', { className: 'dkb-barTitle' }, t('section.label')),
2258
2342
  boards.length > 1
2259
- ? React.createElement('select', {
2343
+ ? h('select', {
2260
2344
  className: 'dkb-input', style: { width: 'auto' }, value: board,
2261
2345
  onChange: (e) => {
2262
- // Отборы собраны из меток ЭТОЙ доски: на другой те же значения
2263
- // могут не встречаться вовсе, и выбор превратился бы в пустоту.
2264
2346
  setOpenTask(null); setCollapsed({}); setFilters({}); setBoard(e.target.value)
2265
2347
  },
2266
- }, boards.map((b) => React.createElement('option', { value: b.id, key: b.id }, boardTitle(b, t))))
2348
+ }, boards.map((b) => h('option', { value: b.id, key: b.id }, boardTitle(b, t))))
2267
2349
  : null,
2268
2350
  (state.facets || []).map((facet) => {
2269
2351
  const picked = filters[facet.ns] || []
2270
- // Имена пространств приходят из Gitea сырыми. Известные переводим,
2271
- // незнакомое оставляем как есть: выдумывать перевод метке, которую
2272
- // завели вчера, доска не вправе.
2273
2352
  const label = facetLabel(facet.ns, t)
2274
- return React.createElement('div', { className: 'dkb-facet', key: facet.ns },
2275
- React.createElement('button', {
2353
+ return h('div', { className: 'dkb-facet', key: facet.ns },
2354
+ h('button', {
2276
2355
  type: 'button',
2277
2356
  className: 'dkb-facetHead' + (picked.length ? ' dkb-facetOn' : ''),
2278
2357
  'aria-expanded': openFacet === facet.ns,
2279
2358
  onClick: () => setOpenFacet((cur) => (cur === facet.ns ? '' : facet.ns)),
2280
2359
  }, picked.length ? label + ' · ' + picked.length : label),
2281
2360
  openFacet === facet.ns
2282
- ? React.createElement('div', { className: 'dkb-facetList' },
2283
- facet.values.map((v) => React.createElement('label', {
2361
+ ? h('div', { className: 'dkb-facetList' },
2362
+ facet.values.map((v) => h('label', {
2284
2363
  className: 'dkb-facetRow', key: v.value,
2285
2364
  },
2286
- React.createElement('input', {
2365
+ h('input', {
2287
2366
  type: 'checkbox',
2288
2367
  checked: picked.indexOf(v.value) >= 0,
2289
2368
  onChange: () => setFilters((cur) => toggleValue(cur, facet.ns, v.value)),
2290
2369
  }),
2291
- React.createElement('span', { className: 'dkb-facetName' }, v.value),
2292
- React.createElement('span', { className: 'dkb-count' }, String(v.count)),
2370
+ h('span', { className: 'dkb-facetName' }, v.value),
2371
+ h('span', { className: 'dkb-count' }, String(v.count)),
2293
2372
  )))
2294
2373
  : null,
2295
2374
  )
2296
2375
  }),
2297
2376
  (() => {
2298
- // Ноль в шапке — мебель, за которой перестают следить, поэтому
2299
- // счётчик появляется только когда кого-то действительно ждут.
2300
2377
  const waiting = state.tasks.filter((x) => x.state === 'waiting').length
2301
2378
  if (waiting === 0) return null
2302
- return React.createElement('button', {
2379
+ return h('button', {
2303
2380
  type: 'button',
2304
2381
  className: 'dkb-waitCount' + (onlyWaiting ? ' dkb-save' : ''),
2305
2382
  title: t('board.waitCountHint'),
@@ -2307,11 +2384,9 @@ window.__ModuleLoader__.load({
2307
2384
  }, t('board.waitCount', { n: waiting }))
2308
2385
  })(),
2309
2386
  (() => {
2310
- // Просроченные — те, чей дедлайн уже прошёл. Счётчик появляется
2311
- // только когда такие есть: ноль в шапке — мебель.
2312
2387
  const overdue = state.tasks.filter((x) => x.overdue === true).length
2313
2388
  if (overdue === 0) return null
2314
- return React.createElement('button', {
2389
+ return h('button', {
2315
2390
  type: 'button',
2316
2391
  className: 'dkb-waitCount' + (onlyOverdue ? ' dkb-save' : ''),
2317
2392
  title: t('board.overdueHint'),
@@ -2319,34 +2394,32 @@ window.__ModuleLoader__.load({
2319
2394
  }, t('board.overdueCount', { n: overdue }))
2320
2395
  })(),
2321
2396
  (() => {
2322
- // Ноль своих задач — кнопки нет: пустой счётчик в шапке перестают
2323
- // замечать вместе с непустым.
2324
2397
  if (me === '') return null
2325
2398
  const mine = state.tasks.filter((x) => x.assignee === me).length
2326
2399
  if (mine === 0) return null
2327
- return React.createElement('button', {
2400
+ return h('button', {
2328
2401
  type: 'button',
2329
2402
  className: 'dkb-waitCount' + (onlyMine ? ' dkb-save' : ''),
2330
2403
  title: t('board.mineHint', { who: me }),
2331
2404
  onClick: () => setOnlyMine((cur) => !cur),
2332
2405
  }, t('board.mine', { n: mine }))
2333
2406
  })(),
2334
- React.createElement('select', {
2407
+ h('select', {
2335
2408
  className: 'dkb-input', style: { width: 'auto' }, value: grouping,
2336
2409
  onChange: (e) => setGrouping(e.target.value),
2337
2410
  },
2338
- React.createElement('option', { value: 'repo' }, t('group.repo')),
2339
- React.createElement('option', { value: 'assignee' }, t('group.assignee')),
2411
+ h('option', { value: 'repo' }, t('group.repo')),
2412
+ h('option', { value: 'assignee' }, t('group.assignee')),
2340
2413
  ),
2341
- React.createElement('select', {
2414
+ h('select', {
2342
2415
  className: 'dkb-input', style: { width: 'auto' }, value: order,
2343
2416
  onChange: (e) => setOrder(e.target.value),
2344
2417
  },
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')),
2418
+ h('option', { value: 'manual' }, t('order.manual')),
2419
+ h('option', { value: 'old' }, t('order.old')),
2420
+ h('option', { value: 'new' }, t('order.new')),
2348
2421
  ),
2349
- React.createElement('button', {
2422
+ h('button', {
2350
2423
  type: 'button', className: metrics ? 'dkb-save' : 'dkb-discard',
2351
2424
  onClick: async () => {
2352
2425
  if (metrics) { setMetrics(null); return }
@@ -2356,7 +2429,7 @@ window.__ModuleLoader__.load({
2356
2429
  } catch (e) { setError(t(e.key || 'error.unknown')) }
2357
2430
  },
2358
2431
  }, t('board.metrics')),
2359
- React.createElement('button', {
2432
+ h('button', {
2360
2433
  type: 'button', className: archive ? 'dkb-save' : 'dkb-discard',
2361
2434
  onClick: async () => {
2362
2435
  if (archive) { setArchive(null); return }
@@ -2367,8 +2440,7 @@ window.__ModuleLoader__.load({
2367
2440
  } catch (e) { setError(t(e.key || 'error.unknown')) }
2368
2441
  },
2369
2442
  }, t('board.archive')),
2370
- // Экспорт скачивает полный снимок доски; импорт восстанавливает его.
2371
- React.createElement('button', {
2443
+ h('button', {
2372
2444
  type: 'button', className: 'dkb-discard',
2373
2445
  title: t('board.exportHint'),
2374
2446
  onClick: async () => {
@@ -2384,9 +2456,9 @@ window.__ModuleLoader__.load({
2384
2456
  } catch (e) { setError(t(e.key || 'error.unknown')) }
2385
2457
  },
2386
2458
  }, t('board.export')),
2387
- React.createElement('label', { className: 'dkb-discard dkb-import' },
2459
+ h('label', { className: 'dkb-discard dkb-import' },
2388
2460
  t('board.import'),
2389
- React.createElement('input', {
2461
+ h('input', {
2390
2462
  type: 'file', accept: 'application/json,.json', style: { display: 'none' },
2391
2463
  onChange: async (e) => {
2392
2464
  const file = e.target.files && e.target.files[0]
@@ -2400,42 +2472,34 @@ window.__ModuleLoader__.load({
2400
2472
  } catch { setError(t('error.badImport')) }
2401
2473
  },
2402
2474
  })),
2403
- // «Показано 12 из 81» появляется, только когда что-то отобрано или
2404
- // введён поиск: на нетронутой доске эта строка была бы шумом.
2405
2475
  anySelected(filters) || query.trim() !== ''
2406
- ? React.createElement('span', { className: 'dkb-shown' }, t('board.shown', {
2476
+ ? h('span', { className: 'dkb-shown' }, t('board.shown', {
2407
2477
  shown: state.tasks.filter((x) => matchesFilters(x, filters) && matchesQuery(x, query)).length,
2408
2478
  total: state.tasks.length,
2409
2479
  }))
2410
2480
  : null,
2411
2481
  anySelected(filters)
2412
- ? React.createElement('button', {
2482
+ ? h('button', {
2413
2483
  type: 'button', className: 'dkb-discard',
2414
2484
  onClick: () => { setFilters({}); setOpenFacet('') },
2415
2485
  }, t('board.clearFilters'))
2416
2486
  : null,
2417
- React.createElement('input', {
2487
+ h('input', {
2418
2488
  className: 'dkb-search', value: query, placeholder: t('board.search'),
2419
2489
  onChange: (e) => setQuery(e.target.value),
2420
2490
  }),
2421
2491
  (() => {
2422
- // Пока запрос в пути, состояние на доске ещё вчерашнее: оно приедет
2423
- // только со следующим ответом. Поэтому ход показываем сами, а не
2424
- // ждём, когда сервер расскажет о нём задним числом.
2425
2492
  const line = syncing
2426
2493
  ? { tone: 'idle', text: t('sync.running'), title: '' }
2427
2494
  : syncLine(state.sync, state.now, t)
2428
- return React.createElement('span', {
2495
+ return h('span', {
2429
2496
  className: 'dkb-sync' + (line.tone === 'bad' ? ' dkb-syncBad' : ''),
2430
2497
  title: line.title,
2431
2498
  }, line.text)
2432
2499
  })(),
2433
- React.createElement('button', {
2500
+ h('button', {
2434
2501
  type: 'button', className: 'dkb-discard', disabled: syncing,
2435
2502
  onClick: async () => {
2436
- // Кнопка гаснет на время прохода: нажать её второй раз значило бы
2437
- // попросить сверку, которая и так идёт, а сервер ответил бы
2438
- // «пропущено» — и человек решил бы, что ничего не случилось.
2439
2503
  setSyncing(true)
2440
2504
  try {
2441
2505
  await api('/sync', { method: 'POST', body: '{}' })
@@ -2447,21 +2511,32 @@ window.__ModuleLoader__.load({
2447
2511
  }
2448
2512
  },
2449
2513
  }, syncing ? t('board.syncing') : t('board.sync')),
2450
- React.createElement('button', { type: 'button', className: 'dkb-discard', onClick: () => load(board) }, t('board.refresh')),
2451
- // Полоса выбора появляется, только когда что-то отмечено: на пустом
2452
- // выборе она была бы мебелью.
2514
+ h('button', { type: 'button', className: 'dkb-discard', onClick: () => load(board) }, t('board.refresh')),
2515
+ h('div', { className: 'dkb-quickFilterBar' },
2516
+ [
2517
+ { id: 'all', label: t('filter.all') },
2518
+ { id: 'active', label: t('filter.active') },
2519
+ { id: 'review', label: t('filter.review') },
2520
+ { id: 'epics', label: t('filter.epics') },
2521
+ { id: 'stale', label: t('filter.stale') },
2522
+ ].map((chip) => h('button', {
2523
+ type: 'button',
2524
+ key: chip.id,
2525
+ className: 'dkb-filterChip' + (quickFilter === chip.id ? ' dkb-filterChipActive' : ''),
2526
+ onClick: () => setQuickFilter(chip.id),
2527
+ }, chip.label))),
2453
2528
  Object.keys(picked).length > 0
2454
- ? React.createElement('span', { className: 'dkb-shown' },
2529
+ ? h('span', { className: 'dkb-shown' },
2455
2530
  t('board.pickedCount', { n: Object.keys(picked).length }))
2456
2531
  : null,
2457
2532
  Object.keys(picked).length > 0
2458
- ? React.createElement('button', {
2533
+ ? h('button', {
2459
2534
  type: 'button', className: 'dkb-discard',
2460
2535
  onClick: () => setPicked({}),
2461
2536
  }, t('board.pickNone'))
2462
2537
  : null,
2463
2538
  Object.keys(picked).length > 0
2464
- ? React.createElement('button', {
2539
+ ? h('button', {
2465
2540
  type: 'button', className: 'dkb-save',
2466
2541
  onClick: () => setDialog({
2467
2542
  mode: 'batch',
@@ -2471,15 +2546,13 @@ window.__ModuleLoader__.load({
2471
2546
  }, t('board.pickStart'))
2472
2547
  : null,
2473
2548
  Object.keys(picked).length > 0
2474
- ? React.createElement('button', {
2549
+ ? h('button', {
2475
2550
  type: 'button', className: 'dkb-discard',
2476
2551
  onClick: async () => {
2477
2552
  const ids = Object.keys(picked)
2478
2553
  const out = await applyToEach(ids, (id) => api(
2479
2554
  '/task/' + encodeURIComponent(id) + '/archive', { method: 'POST', body: '{}' },
2480
2555
  ))
2481
- // Часть задач архивировать нельзя (архив только из «Выполнено»),
2482
- // и число отказов называется вслух.
2483
2556
  if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
2484
2557
  else setPicked({})
2485
2558
  await load(board)
@@ -2487,7 +2560,7 @@ window.__ModuleLoader__.load({
2487
2560
  }, t('board.pickArchive'))
2488
2561
  : null,
2489
2562
  Object.keys(picked).length > 0
2490
- ? React.createElement('button', {
2563
+ ? h('button', {
2491
2564
  type: 'button', className: 'dkb-discard',
2492
2565
  onClick: async () => {
2493
2566
  const ids = Object.keys(picked)
@@ -2501,14 +2574,12 @@ window.__ModuleLoader__.load({
2501
2574
  }, t('board.pickUnqueue'))
2502
2575
  : null,
2503
2576
  Object.keys(picked).length > 0
2504
- ? React.createElement('select', {
2505
- className: 'dkb-input', style: { width: 'auto' }, value: '',
2577
+ ? h('select', {
2578
+ className: 'dkb-input',
2506
2579
  onChange: async (e) => {
2507
2580
  const column = e.target.value
2508
- if (column === '') return
2581
+ if (!column) return
2509
2582
  const ids = Object.keys(picked)
2510
- // Групповой перенос — та же команда агенту, что и одиночный:
2511
- // маршрут один, и последствия у него те же.
2512
2583
  const out = await applyToEach(ids, (id) => api(
2513
2584
  '/task/' + encodeURIComponent(id) + '/move',
2514
2585
  { method: 'POST', body: JSON.stringify({ column }) },
@@ -2518,36 +2589,33 @@ window.__ModuleLoader__.load({
2518
2589
  await load(board)
2519
2590
  },
2520
2591
  },
2521
- React.createElement('option', { value: '' }, t('board.pickMove')),
2522
- (state.columns || []).map((c) => React.createElement('option', {
2592
+ h('option', { value: '' }, t('board.pickMove')),
2593
+ (state.columns || []).map((c) => h('option', {
2523
2594
  value: c.id, key: c.id,
2524
2595
  }, columnTitle(c, t))),
2525
2596
  )
2526
2597
  : 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')),
2598
+ 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
2599
  )
2529
2600
 
2530
- // Счётчик колонки считает ВСЕ карточки, а не найденные: иначе поиск
2531
- // сделал бы вид, что предел колонки соблюдён.
2532
2601
  const packs = packInfo(state.tasks)
2533
- const columns = React.createElement('div', { className: 'dkb-cols' },
2602
+ const columns = h('div', { className: 'dkb-cols' },
2534
2603
  state.columns.map((col) => {
2535
- // Счётчик колонки считает ОТОБРАННОЕ, а поиск — нет.
2536
- //
2537
- // Разница осмысленная, а не недосмотр: поиск временный, и предел
2538
- // «3 из 3» при найденной одной карточке врал бы. Отбор постоянный, и
2539
- // на отборе по одному проекту «в работе 3 из 3» должно значить «по
2540
- // этому проекту» — иначе предел ни о чём.
2541
2604
  const all = tasksOf(state.tasks, col.id).filter((task) => matchesFilters(task, filters))
2542
2605
  const items = sortTasks(
2543
- all.filter((task) => matchesQuery(task, query))
2606
+ all.filter((task) => matchesQuery(task, debouncedQuery))
2544
2607
  .filter((task) => !onlyWaiting || task.state === 'waiting')
2545
2608
  .filter((task) => !onlyOverdue || task.overdue === true)
2546
- .filter((task) => !onlyMine || task.assignee === me),
2609
+ .filter((task) => !onlyMine || task.assignee === me)
2610
+ .filter((task) => {
2611
+ if (quickFilter === 'active') return task.column === 'in-progress'
2612
+ if (quickFilter === 'review') return task.column === 'review'
2613
+ if (quickFilter === 'epics') return task.isEpic === true
2614
+ if (quickFilter === 'stale') return task.stale === true
2615
+ return true
2616
+ }),
2547
2617
  order,
2548
2618
  )
2549
- // Ключ группы решает переключатель: заголовки, сворачивание и
2550
- // счётчики остаются те же, меняется только по чему делим.
2551
2619
  const groupKeyOf = (task) => (grouping === 'assignee'
2552
2620
  ? (task.assignee || '')
2553
2621
  : (task.repo || ''))
@@ -2556,7 +2624,7 @@ window.__ModuleLoader__.load({
2556
2624
  : items
2557
2625
  const limit = col.limit
2558
2626
  const overLimit = limit !== undefined && limit !== null && all.length > limit
2559
- const slot = (index) => React.createElement('div', {
2627
+ const slot = (index) => h('div', {
2560
2628
  className: 'dkb-slot' + (dropAt && dropAt.column === col.id && dropAt.index === index ? ' dkb-slotOn' : ''),
2561
2629
  key: 's' + index,
2562
2630
  onDragOver: (e) => {
@@ -2569,8 +2637,6 @@ window.__ModuleLoader__.load({
2569
2637
 
2570
2638
  const rows = []
2571
2639
  rows.push(slot(0))
2572
- // Заголовок рисуем при смене проекта. Задачи приходят уже
2573
- // разложенными, поэтому группы идут подряд и разрываться не могут.
2574
2640
  let group = null
2575
2641
  const sizes = new Map()
2576
2642
  for (const x of ordered) sizes.set(groupKeyOf(x), (sizes.get(groupKeyOf(x)) || 0) + 1)
@@ -2581,20 +2647,18 @@ window.__ModuleLoader__.load({
2581
2647
  if (manyGroups && repo !== group) {
2582
2648
  group = repo
2583
2649
  const shut = groups[col.id + '/' + repo] === true
2584
- rows.push(React.createElement('button', {
2650
+ rows.push(h('button', {
2585
2651
  type: 'button', className: 'dkb-groupHead', key: 'g' + repo,
2586
2652
  'aria-expanded': !shut,
2587
2653
  onClick: () => setGroups((cur) => Object.assign({}, cur, { [col.id + '/' + repo]: !shut })),
2588
2654
  },
2589
- React.createElement('span', null,
2655
+ h('span', null,
2590
2656
  repo || t(grouping === 'assignee' ? 'group.nobody' : 'board.noRepo')),
2591
- // Счётчик виден и у свёрнутой: группа с работой, выглядящая
2592
- // пустой, — то же зло, что и свёрнутая колонка без числа.
2593
- React.createElement('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
2657
+ h('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
2594
2658
  ))
2595
2659
  }
2596
2660
  if (manyGroups && groups[col.id + '/' + (task.repo || '')] === true) return
2597
- rows.push(React.createElement(TaskCard, {
2661
+ rows.push(h(TaskCard, {
2598
2662
  key: task.id, task, t, now: state.now,
2599
2663
  columns: (state.columns || []).map((c) => c.id),
2600
2664
  menuOpen: cardMenu,
@@ -2610,9 +2674,6 @@ window.__ModuleLoader__.load({
2610
2674
  sameSession: hoverSession !== '' && task.sessionId === hoverSession,
2611
2675
  onHover: setHoverSession,
2612
2676
  onRevive: async (x) => {
2613
- // Переиспользуем обычный запуск: он сам пробует возобновить
2614
- // прежнюю сессию и только потом заводит новую. Здесь важно
2615
- // сказать, что именно вышло.
2616
2677
  try {
2617
2678
  const out = await api('/task/' + encodeURIComponent(x.id) + '/start', {
2618
2679
  method: 'POST', body: JSON.stringify({ revive: true }),
@@ -2624,8 +2685,6 @@ window.__ModuleLoader__.load({
2624
2685
  onStop: async (x) => {
2625
2686
  try {
2626
2687
  const out = await api('/task/' + encodeURIComponent(x.id) + '/stop', { method: 'POST', body: '{}' })
2627
- // «Остановлено» и «он и так стоял» — разные ответы, и второй
2628
- // нельзя выдавать за первый.
2629
2688
  if (out && out.acted !== 'stopped') setError(t('stop.' + out.acted))
2630
2689
  await load(board)
2631
2690
  } catch (e) { setError(t(e.key || 'error.unknown')) }
@@ -2635,8 +2694,6 @@ window.__ModuleLoader__.load({
2635
2694
  try {
2636
2695
  const out = await api('/sessions')
2637
2696
  const live = (out && out.sessions) || []
2638
- // Живых сессий нет — ставить некуда, и сказать об этом надо
2639
- // сразу, а не показывать пустой список.
2640
2697
  if (live.length === 0) { setError(t('error.no-live-sessions')); return }
2641
2698
  setDialog({ mode: 'queue', task: x, sessions: live, sessionId: live[0].sessionId, error: '' })
2642
2699
  } catch (e) { setError(t(e.key || 'error.unknown')) }
@@ -2666,37 +2723,33 @@ window.__ModuleLoader__.load({
2666
2723
  ? all.length + ' / ' + t('board.limit', { n: limit })
2667
2724
  : String(all.length)
2668
2725
 
2669
- return React.createElement('div', {
2726
+ return h('div', {
2670
2727
  className: 'dkb-col'
2671
2728
  + (overColumn === col.id ? ' dkb-colOver' : '')
2672
2729
  + (shut ? ' dkb-colShut' : ''),
2673
2730
  key: col.id,
2674
- // Свёрнутая колонка остаётся целью переноса: иначе карточку было бы
2675
- // некуда положить, не развернув её сперва.
2676
2731
  onDragOver: (e) => { e.preventDefault(); setOverColumn(col.id) },
2677
2732
  onDragLeave: () => { setOverColumn(''); setDropAt(null) },
2678
2733
  onDrop: (e) => { e.preventDefault(); drop(col.id, items.length) },
2679
2734
  },
2680
- React.createElement('button', {
2735
+ h('button', {
2681
2736
  type: 'button', className: 'dkb-colHead',
2682
2737
  'aria-expanded': !shut,
2683
2738
  title: t(shut ? 'board.expand' : 'board.collapse'),
2684
2739
  onClick: toggle,
2685
2740
  },
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', {
2741
+ h('span', { className: 'dkb-dot', 'data-col': col.id }),
2742
+ h('span', { className: 'dkb-colName' }, columnTitle(col, t)),
2743
+ h('span', {
2691
2744
  className: 'dkb-count' + (overLimit ? ' dkb-countOver' : ''),
2692
2745
  'data-col': col.id,
2693
2746
  }, count),
2694
2747
  ),
2695
- shut ? null : React.createElement('div', { className: 'dkb-list' },
2748
+ shut ? null : h('div', { className: 'dkb-list' },
2696
2749
  all.length === 0
2697
- ? React.createElement('div', { className: 'dkb-empty' }, t('board.empty'))
2750
+ ? h('div', { className: 'dkb-empty' }, t('board.empty'))
2698
2751
  : items.length === 0
2699
- ? React.createElement('div', { className: 'dkb-empty' }, t('board.noMatch'))
2752
+ ? h('div', { className: 'dkb-empty' }, t('board.noMatch'))
2700
2753
  : null,
2701
2754
  rows,
2702
2755
  ),
@@ -2704,24 +2757,18 @@ window.__ModuleLoader__.load({
2704
2757
  }),
2705
2758
  )
2706
2759
 
2707
- // Окно, а не панель справа: в 420 пикселей помещались описание и журнал,
2708
- // но не план с отметками. Живёт внутри экрана доски — сама доска лежит в
2709
- // колонке разговора, и выход за её пределы наложился бы на чужие части
2710
- // оболочки.
2711
2760
  const panel = openTask
2712
- ? React.createElement('div', {
2761
+ ? h('div', {
2713
2762
  className: 'dkb-modal',
2714
2763
  onClick: () => setOpenTask(null),
2715
- }, React.createElement('div', {
2764
+ }, h('div', {
2716
2765
  className: 'dkb-panel dkb-panelWide',
2717
2766
  role: 'dialog', 'aria-modal': 'true',
2718
2767
  onClick: (e) => e.stopPropagation(),
2719
2768
  },
2720
- // Шапка: чьё это, что за работа, и лишь потом название. Тот же
2721
- // порядок, что на карточке, — глаз не должен переучиваться.
2722
- React.createElement('div', { className: 'dkb-row' },
2723
- React.createElement('label', { className: 'dkb-label' }, t('reasoning.label')),
2724
- React.createElement('select', {
2769
+ h('div', { className: 'dkb-row' },
2770
+ h('label', { className: 'dkb-label' }, t('reasoning.label')),
2771
+ h('select', {
2725
2772
  className: 'dkb-select',
2726
2773
  value: openTask.reasoningEffort || '',
2727
2774
  onChange: async (e) => {
@@ -2735,50 +2782,48 @@ window.__ModuleLoader__.load({
2735
2782
  } catch {}
2736
2783
  },
2737
2784
  },
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')),
2785
+ h('option', { value: '' }, t('reasoning.default')),
2786
+ h('option', { value: 'low' }, t('reasoning.low')),
2787
+ h('option', { value: 'medium' }, t('reasoning.medium')),
2788
+ h('option', { value: 'high' }, t('reasoning.high')),
2742
2789
  ),
2743
2790
  ),
2744
- React.createElement('div', { className: 'dkb-row' },
2745
- React.createElement('div', { className: 'dkb-taskHead', style: { flex: 1 } },
2791
+ h('div', { className: 'dkb-row' },
2792
+ h('div', { className: 'dkb-taskHead', style: { flex: 1 } },
2746
2793
  taskRef(openTask, t),
2747
- (openTask.labels || []).map((l) => React.createElement('span', {
2794
+ (openTask.labels || []).map((l) => h('span', {
2748
2795
  className: 'dkb-tag', key: l, style: tagStyle(openTask, l),
2749
2796
  }, l))),
2750
- React.createElement('button', {
2797
+ h('button', {
2751
2798
  type: 'button', className: 'dkb-discard', onClick: () => setOpenTask(null),
2752
2799
  }, t('panel.close')),
2753
2800
  ),
2754
- React.createElement('div', { className: 'dkb-title' }, openTask.title),
2755
- React.createElement('div', { className: 'dkb-note' },
2801
+ h('div', { className: 'dkb-title' }, openTask.title),
2802
+ h('div', { className: 'dkb-note' },
2756
2803
  t('panel.created', { ago: agoText(state.now, openTask.createdAt, t) })
2757
2804
  + (openTask.assignee ? ' · ' + t('panel.assignee', { who: openTask.assignee }) : '')
2758
- // Автор стоит рядом с датой заведения: это одно событие — кто и
2759
- // когда завёл, — и разносить его по разным углам незачем.
2760
2805
  + (openTask.author ? ' · ' + t('panel.author', { who: openTask.author }) : ''),
2761
2806
  ' · ',
2762
2807
  t('panel.updated', { ago: agoText(state.now, openTask.updatedAt, t) }),
2763
2808
  openTask.issueUrl
2764
- ? React.createElement('a', {
2809
+ ? h('a', {
2765
2810
  className: 'dkb-issueLink', href: openTask.issueUrl,
2766
2811
  target: '_blank', rel: 'noreferrer noopener',
2767
2812
  }, ' · ' + t('panel.issue') + ' ↗')
2768
2813
  : null),
2769
2814
  openTask.worktree
2770
- ? React.createElement('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
2815
+ ? h('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
2771
2816
  t('panel.worktree', { path: openTask.worktree })
2772
2817
  + (openTask.branch ? ' (' + openTask.branch + ')' : ''))
2773
2818
  : null,
2774
2819
  openTask.permission && !openTask.permissionConfirmedAt
2775
- ? React.createElement('div', {
2820
+ ? h('div', {
2776
2821
  className: 'dkb-warnBox',
2777
2822
  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
2823
  },
2779
- React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
2824
+ h('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
2780
2825
  t('task.permissionGateWarning', { permission: openTask.permission })),
2781
- React.createElement('button', {
2826
+ h('button', {
2782
2827
  type: 'button', className: 'dkb-save',
2783
2828
  onClick: async () => {
2784
2829
  try {
@@ -2790,59 +2835,53 @@ window.__ModuleLoader__.load({
2790
2835
  }, t('task.confirmPermission')),
2791
2836
  )
2792
2837
  : null,
2793
- // Пустое тело — отсутствие раздела, а не пустой раздел с заголовком.
2794
2838
  body === null
2795
- ? React.createElement('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
2839
+ ? h('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
2796
2840
  : body.length === 0
2797
2841
  ? 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', {
2842
+ : h('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
2843
+ h('div', { className: 'dkb-edit' },
2844
+ h('div', { className: 'dkb-field' },
2845
+ h('span', { className: 'dkb-label' }, t('panel.priority')),
2846
+ h('select', {
2806
2847
  className: 'dkb-input', value: editPriority,
2807
2848
  onChange: (e) => setEditPriority(e.target.value),
2808
2849
  },
2809
- React.createElement('option', { value: '' }, t('panel.priorityNone')),
2810
- ['high', 'medium', 'low'].map((p) => React.createElement('option', { value: p, key: p }, t('priority.' + p)))),
2850
+ h('option', { value: '' }, t('panel.priorityNone')),
2851
+ ['high', 'medium', 'low'].map((p) => h('option', { value: p, key: p }, t('priority.' + p)))),
2811
2852
  ),
2812
- React.createElement('div', { className: 'dkb-field' },
2813
- React.createElement('span', { className: 'dkb-label' }, t('panel.labels')),
2814
- React.createElement('input', {
2853
+ h('div', { className: 'dkb-field' },
2854
+ h('span', { className: 'dkb-label' }, t('panel.labels')),
2855
+ h('input', {
2815
2856
  className: 'dkb-input', value: editLabels,
2816
2857
  placeholder: t('dialog.labelsHint'),
2817
2858
  onChange: (e) => setEditLabels(e.target.value),
2818
2859
  }),
2819
2860
  ),
2820
- React.createElement('div', { className: 'dkb-field' },
2821
- React.createElement('span', { className: 'dkb-label' }, t('panel.due')),
2822
- React.createElement('input', {
2861
+ h('div', { className: 'dkb-field' },
2862
+ h('span', { className: 'dkb-label' }, t('panel.due')),
2863
+ h('input', {
2823
2864
  type: 'date', className: 'dkb-input', value: editDue,
2824
2865
  onChange: (e) => setEditDue(e.target.value),
2825
2866
  }),
2826
2867
  ),
2827
- React.createElement('div', { className: 'dkb-field' },
2828
- React.createElement('span', { className: 'dkb-label' }, t('task.cron')),
2829
- React.createElement('input', {
2868
+ h('div', { className: 'dkb-field' },
2869
+ h('span', { className: 'dkb-label' }, t('task.cron')),
2870
+ h('input', {
2830
2871
  className: 'dkb-input', value: editCron,
2831
2872
  placeholder: '* * * * *',
2832
2873
  onChange: (e) => setEditCron(e.target.value),
2833
2874
  }),
2834
2875
  openTask.nextRunAt
2835
- ? React.createElement('p', { className: 'dkb-hint' },
2876
+ ? h('p', { className: 'dkb-hint' },
2836
2877
  t('task.nextRun', { date: new Date(openTask.nextRunAt).toLocaleString() }))
2837
2878
  : null,
2838
2879
  ),
2839
- React.createElement('button', {
2880
+ h('button', {
2840
2881
  type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
2841
2882
  onClick: async () => {
2842
2883
  try {
2843
2884
  const labels = editLabels.split(',').map((s) => s.trim()).filter(Boolean)
2844
- // Пустая дата снимает дедлайн (0); заполненная — полночь
2845
- // этого дня по местному времени.
2846
2885
  const dueAt = editDue === ''
2847
2886
  ? 0
2848
2887
  : new Date(editDue + 'T00:00:00').getTime()
@@ -2857,9 +2896,9 @@ window.__ModuleLoader__.load({
2857
2896
  }, t('panel.saveEdit')),
2858
2897
  ),
2859
2898
  openTask.progressDump
2860
- ? React.createElement('div', null,
2861
- React.createElement('div', { className: 'dkb-group' }, t('task.progressDump')),
2862
- React.createElement('pre', {
2899
+ ? h('div', null,
2900
+ h('div', { className: 'dkb-group' }, t('task.progressDump')),
2901
+ h('pre', {
2863
2902
  style: {
2864
2903
  background: 'var(--dsw-alias-bg-layer-1, #f6f8fa)', padding: 8, borderRadius: 6, fontSize: 12,
2865
2904
  whiteSpace: 'pre-wrap', maxHeight: 150, overflowY: 'auto', margin: 0,
@@ -2867,21 +2906,155 @@ window.__ModuleLoader__.load({
2867
2906
  }, openTask.progressDump),
2868
2907
  )
2869
2908
  : null,
2909
+ 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 } },
2910
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
2911
+ h('span', { className: 'dkb-group', style: { margin: 0 } },
2912
+ '👑 ' + t('epic.subtasks') + (subtasks.length > 0 ? ` (${subtasks.filter(s => s.column === 'done').length}/${subtasks.length})` : '')
2913
+ ),
2914
+ h('button', {
2915
+ type: 'button', className: 'dkb-discard',
2916
+ style: { padding: '2px 8px', fontSize: 11 },
2917
+ onClick: () => setTriageOpen(!triageOpen),
2918
+ }, triageOpen ? t('dialog.cancel') : '⚡ ' + t('epic.triage')),
2919
+ ),
2920
+ 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)' } },
2921
+ h('span', { style: { fontSize: 12, fontWeight: 600 } }, t('epic.triageTitle')),
2922
+ h('span', { className: 'dkb-hint', style: { margin: 0 } }, t('epic.triageHelp')),
2923
+ h('textarea', {
2924
+ className: 'dkb-area',
2925
+ rows: 4,
2926
+ placeholder: 'Subtask 1\nSubtask 2\nSubtask 3...',
2927
+ value: triageText,
2928
+ onChange: (e) => setTriageText(e.target.value),
2929
+ }),
2930
+ openTask.owner && openTask.repo ? h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer' } },
2931
+ h('input', {
2932
+ type: 'checkbox',
2933
+ checked: triageGitea,
2934
+ onChange: (e) => setTriageGitea(e.target.checked),
2935
+ }),
2936
+ h('span', null, t('epic.createGiteaIssues'))
2937
+ ) : null,
2938
+ h('div', { style: { display: 'flex', gap: 6 } },
2939
+ h('button', {
2940
+ type: 'button', className: 'dkb-save',
2941
+ disabled: triageSubmitting || !triageText.trim(),
2942
+ onClick: async () => {
2943
+ const lines = triageText.split('\n').map(s => s.trim()).filter(Boolean)
2944
+ if (lines.length === 0) return
2945
+ setTriageSubmitting(true)
2946
+ try {
2947
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/triage', {
2948
+ method: 'POST',
2949
+ body: JSON.stringify({
2950
+ subtasks: lines.map(title => ({ title })),
2951
+ createGiteaIssues: triageGitea,
2952
+ }),
2953
+ })
2954
+ if (out && out.subtasks) setSubtasks(out.subtasks)
2955
+ if (out && out.epic) setOpenTask(out.epic)
2956
+ setTriageText('')
2957
+ setTriageOpen(false)
2958
+ await load(board)
2959
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
2960
+ finally { setTriageSubmitting(false) }
2961
+ },
2962
+ }, triageSubmitting ? '...' : t('epic.triageSubmit')),
2963
+ h('button', {
2964
+ type: 'button', className: 'dkb-discard',
2965
+ onClick: () => setTriageOpen(false),
2966
+ }, t('dialog.cancel')),
2967
+ )
2968
+ ) : null,
2969
+ subtasks.length > 0 ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
2970
+ subtasks.map((st) => h('li', {
2971
+ key: st.id,
2972
+ style: {
2973
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
2974
+ padding: '4px 8px', background: 'var(--dsw-alias-bg-layer-2)',
2975
+ borderRadius: 4, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
2976
+ },
2977
+ },
2978
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flex: 1 } },
2979
+ h('span', { style: { color: st.column === 'done' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-tertiary)' } },
2980
+ st.column === 'done' ? '✓' : '•'
2981
+ ),
2982
+ h('span', { style: { textDecoration: st.column === 'done' ? 'line-through' : 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, st.title),
2983
+ st.issueNumber ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, '#' + st.issueNumber) : null,
2984
+ ),
2985
+ h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('column.' + st.column)),
2986
+ ))
2987
+ ) : h('p', { className: 'dkb-hint', style: { margin: '4px 0' } }, t('epic.noSubtasks')),
2988
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 6 } },
2989
+ h('input', {
2990
+ className: 'dkb-input',
2991
+ style: { flex: 1, fontSize: 12 },
2992
+ placeholder: t('epic.addSubtask'),
2993
+ value: newSubtaskTitle,
2994
+ onChange: (e) => setNewSubtaskTitle(e.target.value),
2995
+ onKeyDown: async (e) => {
2996
+ if (e.key === 'Enter' && newSubtaskTitle.trim()) {
2997
+ e.preventDefault()
2998
+ const title = newSubtaskTitle.trim()
2999
+ setNewSubtaskTitle('')
3000
+ try {
3001
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
3002
+ method: 'POST',
3003
+ body: JSON.stringify({ title }),
3004
+ })
3005
+ if (out && out.subtask) {
3006
+ setSubtasks(prev => [...prev, out.subtask])
3007
+ await load(board)
3008
+ }
3009
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
3010
+ }
3011
+ },
3012
+ }),
3013
+ h('button', {
3014
+ type: 'button', className: 'dkb-save',
3015
+ style: { padding: '3px 10px', fontSize: 12 },
3016
+ onClick: async () => {
3017
+ const title = newSubtaskTitle.trim()
3018
+ if (!title) return
3019
+ setNewSubtaskTitle('')
3020
+ try {
3021
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
3022
+ method: 'POST',
3023
+ body: JSON.stringify({ title }),
3024
+ })
3025
+ if (out && out.subtask) {
3026
+ setSubtasks(prev => [...prev, out.subtask])
3027
+ await load(board)
3028
+ }
3029
+ } catch (err) { setError(t(err.key || 'error.unknown')) }
3030
+ },
3031
+ }, '+'),
3032
+ )
3033
+ ),
3034
+ openTask.sessionId ? h('div', { style: { margin: '6px 0' } },
3035
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3036
+ h('span', null, '🖥️ ' + t('console.tab')),
3037
+ h('span', { className: 'dkb-tag', style: { fontSize: 10, fontFamily: 'monospace' } }, openTask.sessionId.slice(0, 16) + '...'),
3038
+ ),
3039
+ h('pre', { className: 'dkb-consolePre' },
3040
+ 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'))
3041
+ )
3042
+ ) : null,
2870
3043
  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', {
3044
+ ? h('div', null,
3045
+ h('div', { className: 'dkb-group' }, t('task.runs')),
3046
+ h('ul', { className: 'dkb-log' }, openTask.runs.map((r) => h('li', {
2874
3047
  className: 'dkb-logRow', key: r.id,
2875
3048
  style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
2876
3049
  },
2877
- React.createElement('span', null,
3050
+ h('span', null,
2878
3051
  (r.result === 'succeeded' ? '✓ ' : r.result === 'failed' ? '✕ ' : '▸ ')
2879
3052
  + new Date(r.startedAt).toLocaleTimeString()
2880
3053
  + (r.model ? ' · ' + r.model : '')
2881
3054
  + (r.error ? ' · ' + r.error : '')
2882
3055
  ),
2883
3056
  r.sessionId
2884
- ? React.createElement('button', {
3057
+ ? h('button', {
2885
3058
  type: 'button',
2886
3059
  className: 'dkb-discard',
2887
3060
  style: { padding: '2px 6px', fontSize: 11 },
@@ -2897,9 +3070,7 @@ window.__ModuleLoader__.load({
2897
3070
  : null,
2898
3071
  ))))
2899
3072
  : null,
2900
- // Взять и отдать — одна кнопка в двух состояниях: держать рядом
2901
- // «взять» и «отказаться» значит предлагать бессмысленное действие.
2902
- React.createElement('button', {
3073
+ h('button', {
2903
3074
  type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
2904
3075
  onClick: async () => {
2905
3076
  const mine = openTask.assignee !== '' && openTask.assignee !== undefined
@@ -2914,12 +3085,9 @@ window.__ModuleLoader__.load({
2914
3085
  },
2915
3086
  }, openTask.assignee ? t('panel.assignDrop') : t('panel.assignMe')),
2916
3087
  openTask.sessionId
2917
- ? React.createElement('button', {
3088
+ ? h('button', {
2918
3089
  type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
2919
3090
  onClick: () => {
2920
- // Тот же путь, что и сразу после запуска: задача, идущая
2921
- // вторые сутки, иначе ищется в общем списке по имени вида
2922
- // kanban-9e84cae6-…
2923
3091
  if (!openSession(props.ctx, openTask.sessionId)) {
2924
3092
  setError(t('error.sessionNotOpened', { id: openTask.sessionId }))
2925
3093
  return
@@ -2930,31 +3098,31 @@ window.__ModuleLoader__.load({
2930
3098
  }, t('panel.openChat'))
2931
3099
  : null,
2932
3100
  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', {
3101
+ ? h('div', null,
3102
+ h('div', { className: 'dkb-group' }, t('panel.log')),
3103
+ h('ul', { className: 'dkb-log' }, log.map((row) => h('li', {
2936
3104
  className: 'dkb-logRow', key: row.id,
2937
3105
  }, `${t('column.' + row.toCol)} ← ${row.source}${row.detail ? ' · ' + row.detail : ''}`))))
2938
3106
  : null,
2939
3107
  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', {
3108
+ ? h('div', null,
3109
+ h('div', { className: 'dkb-group' }, t('plan.title')),
3110
+ h('p', { className: 'dkb-hint' }, planLine(openTask, t)),
3111
+ h('ul', { className: 'dkb-planList' },
3112
+ openTask.plan.items.map((item, i) => h('li', {
2945
3113
  key: i,
2946
3114
  className: 'dkb-planItem'
2947
3115
  + (item.done ? ' dkb-planDone' : '')
2948
3116
  + (item.active ? ' dkb-planActive' : ''),
2949
3117
  },
2950
- React.createElement('span', { className: 'dkb-planMark' },
3118
+ h('span', { className: 'dkb-planMark' },
2951
3119
  item.done ? '✓' : item.active ? '▸' : '·'),
2952
- React.createElement('span', null, item.text),
3120
+ h('span', null, item.text),
2953
3121
  ))),
2954
3122
  )
2955
3123
  : null,
2956
3124
  openTask.rejectReason
2957
- ? React.createElement('div', {
3125
+ ? h('div', {
2958
3126
  className: 'dkb-warnBox',
2959
3127
  style: {
2960
3128
  margin: '4px 0', padding: 10,
@@ -2963,13 +3131,13 @@ window.__ModuleLoader__.load({
2963
3131
  borderRadius: 6,
2964
3132
  },
2965
3133
  },
2966
- React.createElement('div', {
3134
+ h('div', {
2967
3135
  style: { marginBottom: 4, fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-danger-primary, #cf222e)' },
2968
3136
  }, '⚠️ ' + t('acceptance.rejectedAlert', { reason: openTask.rejectReason })),
2969
3137
  )
2970
3138
  : null,
2971
3139
  openTask.worktree
2972
- ? React.createElement('div', {
3140
+ ? h('div', {
2973
3141
  style: {
2974
3142
  margin: '8px 0', padding: 12,
2975
3143
  background: 'var(--dsw-alias-bg-layer-3)',
@@ -2977,13 +3145,13 @@ window.__ModuleLoader__.load({
2977
3145
  borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
2978
3146
  },
2979
3147
  },
2980
- React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
2981
- React.createElement('div', { className: 'dkb-group', style: { margin: 0 } },
3148
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3149
+ h('div', { className: 'dkb-group', style: { margin: 0 } },
2982
3150
  t('diff.title'),
2983
3151
  commitsData.length > 0 ? ` · ${t('diff.commits', { n: commitsData.length })}` : '',
2984
3152
  diffData?.filesChanged ? ` · ${t('diff.filesChanged', { n: diffData.filesChanged })}` : '',
2985
3153
  ),
2986
- React.createElement('button', {
3154
+ h('button', {
2987
3155
  type: 'button', className: 'dkb-discard',
2988
3156
  style: { padding: '2px 8px', fontSize: 11 },
2989
3157
  onClick: () => {
@@ -2992,23 +3160,23 @@ window.__ModuleLoader__.load({
2992
3160
  },
2993
3161
  }, diffExpanded ? t('diff.hide') : t('diff.show')),
2994
3162
  ),
2995
- diffExpanded ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
3163
+ diffExpanded ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
2996
3164
  commitsData.length > 0
2997
- ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
2998
- commitsData.map((cm) => React.createElement('div', {
3165
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
3166
+ commitsData.map((cm) => h('div', {
2999
3167
  key: cm.hash,
3000
3168
  style: { fontSize: 11, display: 'flex', gap: 6, opacity: 0.85, fontFamily: 'monospace' },
3001
3169
  },
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),
3170
+ h('span', { style: { color: 'var(--dsw-alias-label-link, #3b82f6)' } }, cm.hash),
3171
+ h('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
3172
+ h('span', { style: { opacity: 0.6 } }, cm.date),
3005
3173
  ))
3006
3174
  )
3007
3175
  : null,
3008
3176
  diffLoading
3009
- ? React.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
3177
+ ? h('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
3010
3178
  : diffData?.diff
3011
- ? React.createElement('pre', {
3179
+ ? h('pre', {
3012
3180
  style: {
3013
3181
  margin: 0, padding: 8, maxHeight: 280, overflowY: 'auto',
3014
3182
  background: 'var(--dsw-alias-bg-layer-2, #1e1e1e)',
@@ -3017,12 +3185,12 @@ window.__ModuleLoader__.load({
3017
3185
  border: '1px solid var(--dsw-alias-border-l1)',
3018
3186
  },
3019
3187
  }, diffData.diff)
3020
- : React.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
3188
+ : h('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
3021
3189
  ) : null
3022
3190
  )
3023
3191
  : null,
3024
3192
  openTask.column === 'review'
3025
- ? React.createElement('div', {
3193
+ ? h('div', {
3026
3194
  style: {
3027
3195
  margin: '8px 0', padding: 12,
3028
3196
  background: 'var(--dsw-alias-bg-layer-3)',
@@ -3030,10 +3198,10 @@ window.__ModuleLoader__.load({
3030
3198
  borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
3031
3199
  },
3032
3200
  },
3033
- React.createElement('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
3034
- React.createElement('div', { style: { display: 'flex', gap: 8 } },
3201
+ h('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
3202
+ h('div', { style: { display: 'flex', gap: 8 } },
3035
3203
  openTask.worktree
3036
- ? React.createElement('button', {
3204
+ ? h('button', {
3037
3205
  type: 'button', className: 'dkb-save',
3038
3206
  disabled: merging,
3039
3207
  style: { background: 'var(--dsw-alias-bg-success, #16a34a)', color: '#fff' },
@@ -3048,7 +3216,7 @@ window.__ModuleLoader__.load({
3048
3216
  },
3049
3217
  }, merging ? t('acceptance.merging') : '🔀 ' + t('acceptance.acceptAndMerge'))
3050
3218
  : null,
3051
- React.createElement('button', {
3219
+ h('button', {
3052
3220
  type: 'button', className: 'dkb-save',
3053
3221
  onClick: async () => {
3054
3222
  try {
@@ -3058,23 +3226,50 @@ window.__ModuleLoader__.load({
3058
3226
  } catch (e) { setError(t(e.key || 'error.unknown')) }
3059
3227
  },
3060
3228
  }, '✓ ' + t('acceptance.accept')),
3061
- React.createElement('button', {
3229
+ h('button', {
3062
3230
  type: 'button', className: 'dkb-discard',
3063
3231
  onClick: () => { setRejectModalOpen(true); setRejectReasonText('') },
3064
3232
  }, '✗ ' + t('acceptance.reject')),
3233
+ openTask.owner && openTask.repo
3234
+ ? h('button', {
3235
+ type: 'button', className: 'dkb-save',
3236
+ disabled: prCreating,
3237
+ style: { background: 'var(--dsw-alias-state-info-primary, #0969da)', color: '#fff' },
3238
+ onClick: async () => {
3239
+ setPrCreating(true)
3240
+ try {
3241
+ const out = await api('/task/' + encodeURIComponent(openTask.id) + '/pr', {
3242
+ method: 'POST',
3243
+ body: JSON.stringify({ base: 'main' }),
3244
+ })
3245
+ if (out && out.pr) setCreatedPr(out.pr)
3246
+ await load(board)
3247
+ } catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
3248
+ finally { setPrCreating(false) }
3249
+ },
3250
+ }, prCreating ? t('pr.creating') : '🐙 ' + t('pr.create'))
3251
+ : null,
3252
+ createdPr
3253
+ ? h('a', {
3254
+ className: 'dkb-save',
3255
+ style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: '#fff', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
3256
+ href: createdPr.html_url,
3257
+ target: '_blank', rel: 'noopener noreferrer',
3258
+ }, '↗ ' + t('pr.view') + ` #${createdPr.number}`)
3259
+ : null,
3065
3260
  ),
3066
3261
  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', {
3262
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 } },
3263
+ h('span', { className: 'dkb-label' }, t('acceptance.rejectTitle')),
3264
+ h('textarea', {
3070
3265
  className: 'dkb-area',
3071
3266
  value: rejectReasonText,
3072
3267
  placeholder: t('acceptance.rejectReasonPlaceholder'),
3073
3268
  onChange: (e) => setRejectReasonText(e.target.value),
3074
3269
  rows: 3,
3075
3270
  }),
3076
- React.createElement('div', { style: { display: 'flex', gap: 6 } },
3077
- React.createElement('button', {
3271
+ h('div', { style: { display: 'flex', gap: 6 } },
3272
+ h('button', {
3078
3273
  type: 'button', className: 'dkb-save',
3079
3274
  onClick: async () => {
3080
3275
  if (!rejectReasonText.trim()) return
@@ -3089,7 +3284,7 @@ window.__ModuleLoader__.load({
3089
3284
  } catch (e) { setError(t(e.key || 'error.unknown')) }
3090
3285
  },
3091
3286
  }, t('acceptance.rejectSubmit')),
3092
- React.createElement('button', {
3287
+ h('button', {
3093
3288
  type: 'button', className: 'dkb-discard',
3094
3289
  onClick: () => setRejectModalOpen(false),
3095
3290
  }, t('dialog.cancel')),
@@ -3098,11 +3293,11 @@ window.__ModuleLoader__.load({
3098
3293
  : null,
3099
3294
  )
3100
3295
  : 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')),
3296
+ h('div', { style: { margin: '4px 0' } },
3297
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3298
+ h('span', null, t('checklist.title')),
3104
3299
  Array.isArray(openTask.checklist) && openTask.checklist.length > 0
3105
- ? React.createElement('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
3300
+ ? h('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
3106
3301
  t('checklist.progress', {
3107
3302
  done: openTask.checklist.filter((i) => i.completed).length,
3108
3303
  total: openTask.checklist.length,
@@ -3110,8 +3305,8 @@ window.__ModuleLoader__.load({
3110
3305
  : null,
3111
3306
  ),
3112
3307
  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', {
3308
+ ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
3309
+ openTask.checklist.map((item, idx) => h('li', {
3115
3310
  key: idx,
3116
3311
  style: {
3117
3312
  display: 'flex', flexDirection: 'column', gap: 2, padding: '4px 8px',
@@ -3119,8 +3314,8 @@ window.__ModuleLoader__.load({
3119
3314
  borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)',
3120
3315
  },
3121
3316
  },
3122
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
3123
- React.createElement('input', {
3317
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
3318
+ h('input', {
3124
3319
  type: 'checkbox',
3125
3320
  checked: Boolean(item.completed),
3126
3321
  onChange: async (e) => {
@@ -3136,7 +3331,7 @@ window.__ModuleLoader__.load({
3136
3331
  } catch (err) { setError(t(err.key || 'error.unknown')) }
3137
3332
  },
3138
3333
  }),
3139
- React.createElement('span', {
3334
+ h('span', {
3140
3335
  style: {
3141
3336
  textDecoration: item.completed ? 'line-through' : 'none',
3142
3337
  color: item.completed ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-primary)',
@@ -3144,11 +3339,11 @@ window.__ModuleLoader__.load({
3144
3339
  },
3145
3340
  }, item.text),
3146
3341
  item.required !== false
3147
- ? React.createElement('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
3342
+ ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
3148
3343
  : null,
3149
3344
  ),
3150
3345
  item.evidence
3151
- ? React.createElement('div', {
3346
+ ? h('div', {
3152
3347
  style: {
3153
3348
  fontSize: 11, color: 'var(--dsw-alias-label-secondary)', marginLeft: 24,
3154
3349
  fontFamily: 'var(--dsw-font-family-mono, monospace)', whiteSpace: 'pre-wrap',
@@ -3156,9 +3351,9 @@ window.__ModuleLoader__.load({
3156
3351
  }, t('checklist.evidence', { text: item.evidence }))
3157
3352
  : null,
3158
3353
  )))
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', {
3354
+ : h('p', { className: 'dkb-hint' }, t('checklist.empty')),
3355
+ h('div', { style: { display: 'flex', gap: 6, marginTop: 4 } },
3356
+ h('input', {
3162
3357
  className: 'dkb-input',
3163
3358
  style: { flex: 1 },
3164
3359
  placeholder: t('checklist.placeholder'),
@@ -3180,7 +3375,7 @@ window.__ModuleLoader__.load({
3180
3375
  }
3181
3376
  },
3182
3377
  }),
3183
- React.createElement('button', {
3378
+ h('button', {
3184
3379
  type: 'button', className: 'dkb-save',
3185
3380
  onClick: async () => {
3186
3381
  if (!newChecklistText.trim()) return
@@ -3199,7 +3394,7 @@ window.__ModuleLoader__.load({
3199
3394
  ),
3200
3395
  ),
3201
3396
  openTask.report && openTask.report.summary
3202
- ? React.createElement('div', {
3397
+ ? h('div', {
3203
3398
  style: {
3204
3399
  margin: '6px 0', padding: 10,
3205
3400
  background: 'var(--dsw-alias-bg-layer-1)',
@@ -3207,79 +3402,79 @@ window.__ModuleLoader__.load({
3207
3402
  borderRadius: 6,
3208
3403
  },
3209
3404
  },
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 } },
3405
+ h('div', { className: 'dkb-group', style: { margin: '0 0 6px 0' } }, t('report.title')),
3406
+ h('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)', marginBottom: 6, fontWeight: 500 } },
3212
3407
  openTask.report.summary),
3213
3408
  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)),
3409
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
3410
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.changedFiles') + ': '),
3411
+ openTask.report.changedFiles.map((f) => h('span', { className: 'dkb-tag', key: f, style: { margin: '1px 2px', fontFamily: 'monospace' } }, f)),
3217
3412
  )
3218
3413
  : null,
3219
3414
  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)),
3415
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
3416
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.checksRun') + ': '),
3417
+ openTask.report.checksRun.map((chk, i) => h('div', { key: i, style: { fontFamily: 'monospace', fontSize: 11, marginLeft: 8 } }, '• ' + chk)),
3223
3418
  )
3224
3419
  : null,
3225
3420
  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)),
3421
+ ? h('div', { style: { fontSize: 12, marginBottom: 4 } },
3422
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.artifacts') + ': '),
3423
+ openTask.report.artifacts.map((a, i) => h('span', { className: 'dkb-tag', key: i, style: { margin: '1px 2px' } }, a)),
3229
3424
  )
3230
3425
  : null,
3231
3426
  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)),
3427
+ ? h('div', { style: { fontSize: 12 } },
3428
+ h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.risks') + ': '),
3429
+ 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
3430
  )
3236
3431
  : null,
3237
3432
  )
3238
3433
  : 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')),
3434
+ h('div', { style: { margin: '6px 0' } },
3435
+ h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3436
+ h('span', null, t('comments.title')),
3242
3437
  Array.isArray(openTask.comments) && openTask.comments.length > 0
3243
- ? React.createElement('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
3438
+ ? h('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
3244
3439
  : null,
3245
3440
  ),
3246
3441
  Array.isArray(openTask.comments) && openTask.comments.length > 0
3247
- ? React.createElement('ul', {
3442
+ ? h('ul', {
3248
3443
  style: {
3249
3444
  listStyle: 'none', padding: 0, margin: '4px 0', maxHeight: 200, overflowY: 'auto',
3250
3445
  display: 'flex', flexDirection: 'column', gap: 6,
3251
3446
  },
3252
3447
  },
3253
- openTask.comments.map((c, i) => React.createElement('li', {
3448
+ openTask.comments.map((c, i) => h('li', {
3254
3449
  key: c.id || i,
3255
3450
  style: {
3256
3451
  padding: 8, background: 'var(--dsw-alias-bg-layer-3)',
3257
3452
  borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
3258
3453
  },
3259
3454
  },
3260
- React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
3261
- React.createElement('span', {
3455
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
3456
+ h('span', {
3262
3457
  className: 'dkb-tag',
3263
3458
  style: {
3264
3459
  fontWeight: 600,
3265
3460
  background: c.role === 'agent' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-2)',
3266
3461
  },
3267
3462
  }, c.role === 'agent' ? t('comments.authorAgent') : (c.author || t('comments.authorUser'))),
3268
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
3463
+ h('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
3269
3464
  new Date(c.createdAt || Date.now()).toLocaleTimeString()),
3270
3465
  ),
3271
- React.createElement('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
3466
+ h('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
3272
3467
  )))
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', {
3468
+ : h('p', { className: 'dkb-hint' }, t('comments.empty')),
3469
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
3470
+ h('textarea', {
3276
3471
  className: 'dkb-area',
3277
3472
  placeholder: t('comments.placeholder'),
3278
3473
  value: newCommentText,
3279
3474
  rows: 2,
3280
3475
  onChange: (e) => setNewCommentText(e.target.value),
3281
3476
  }),
3282
- React.createElement('button', {
3477
+ h('button', {
3283
3478
  type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
3284
3479
  onClick: async () => {
3285
3480
  if (!newCommentText.trim()) return
@@ -3297,10 +3492,8 @@ window.__ModuleLoader__.load({
3297
3492
  ),
3298
3493
  ),
3299
3494
  openTask.archivedAt
3300
- ? React.createElement('button', {
3495
+ ? h('button', {
3301
3496
  type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
3302
- // Запускать работу по архивной задаче нечего: сперва она
3303
- // возвращается на доску, и это одно нажатие.
3304
3497
  onClick: async () => {
3305
3498
  try {
3306
3499
  await api('/task/' + encodeURIComponent(openTask.id) + '/restore', { method: 'POST', body: '{}' })
@@ -3310,70 +3503,59 @@ window.__ModuleLoader__.load({
3310
3503
  } catch (e) { setError(t(e.key || 'error.unknown')) }
3311
3504
  },
3312
3505
  }, t('archive.restore'))
3313
- : React.createElement('div', { className: 'dkb-start' },
3314
- React.createElement('div', { className: 'dkb-group' },
3506
+ : h('div', { className: 'dkb-start' },
3507
+ h('div', { className: 'dkb-group' },
3315
3508
  openTask.sessionId ? t('panel.continue') : t('panel.start')),
3316
- React.createElement('p', { className: 'dkb-hint' },
3509
+ h('p', { className: 'dkb-hint' },
3317
3510
  openTask.sessionId ? t('panel.continueHint') : t('panel.startHint')),
3318
- // Корень не задан настройкой — сессия пойдёт в рабочей папке
3319
- // харнесса. Молчать об этом нельзя: человек иначе узнает, где
3320
- // оказался агент, только из его первых слов. Показываем фактический
3321
- // корень, который приехал с доской.
3322
3511
  !openTask.sessionId && state && !state.projectRoot.set && state.projectRoot.path !== ''
3323
- ? React.createElement('p', { className: 'dkb-hint dkb-workdir' },
3512
+ ? h('p', { className: 'dkb-hint dkb-workdir' },
3324
3513
  t('panel.workDir', { path: state.projectRoot.path }),
3325
- React.createElement('br'),
3514
+ h('br'),
3326
3515
  t('panel.workDirHint'))
3327
3516
  : null,
3328
3517
  React.createElement(LaunchFields, {
3329
3518
  t, presets, agentPreset, permission, disabled: starting,
3330
3519
  onPreset: setAgentPreset, onPermission: setPermission,
3331
- // У идущей сессии профиль уже выбран: сменить его нельзя, а
3332
- // показывать поле, которое ничего не меняет, — врать.
3333
3520
  hidePreset: Boolean(openTask.sessionId),
3334
3521
  }),
3335
- React.createElement('div', { className: 'dkb-field' },
3336
- React.createElement('span', { className: 'dkb-label' }, t('panel.provider')),
3337
- React.createElement('select', {
3522
+ h('div', { className: 'dkb-field' },
3523
+ h('span', { className: 'dkb-label' }, t('panel.provider')),
3524
+ h('select', {
3338
3525
  className: 'dkb-input', value: provider, disabled: starting || providers.length === 0,
3339
3526
  onChange: (e) => { setProvider(e.target.value); setModel('') },
3340
3527
  },
3341
- React.createElement('option', { value: '' },
3528
+ h('option', { value: '' },
3342
3529
  providers.length === 0 ? t('panel.noProviders') : t('panel.pickProvider')),
3343
- providers.map((p) => React.createElement('option', { value: p.id, key: p.id }, p.name)),
3530
+ providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
3344
3531
  ),
3345
3532
  ),
3346
- React.createElement('div', { className: 'dkb-field' },
3347
- React.createElement('span', { className: 'dkb-label' }, t('panel.model')),
3348
- React.createElement('select', {
3533
+ h('div', { className: 'dkb-field' },
3534
+ h('span', { className: 'dkb-label' }, t('panel.model')),
3535
+ h('select', {
3349
3536
  className: 'dkb-input', value: model,
3350
3537
  disabled: starting || provider === '' || models.length === 0,
3351
3538
  onChange: (e) => setModel(e.target.value),
3352
3539
  },
3353
- React.createElement('option', { value: '' },
3540
+ h('option', { value: '' },
3354
3541
  provider === '' ? t('panel.pickProviderFirst')
3355
3542
  : models.length === 0 ? t('panel.noModels') : t('panel.pickModel')),
3356
- models.map((m) => React.createElement('option', { value: m.id, key: m.id }, m.name)),
3543
+ models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
3357
3544
  ),
3358
3545
  ),
3359
- React.createElement('div', { className: 'dkb-field' },
3360
- React.createElement('span', { className: 'dkb-label' }, t('panel.message')),
3361
- React.createElement('textarea', {
3546
+ h('div', { className: 'dkb-field' },
3547
+ h('span', { className: 'dkb-label' }, t('panel.message')),
3548
+ h('textarea', {
3362
3549
  className: 'dkb-area', value: draftText, disabled: starting,
3363
3550
  onChange: (e) => setDraftText(e.target.value),
3364
3551
  }),
3365
- React.createElement('p', { className: 'dkb-hint' }, t('panel.messageHint')),
3552
+ h('p', { className: 'dkb-hint' }, t('panel.messageHint')),
3366
3553
  ),
3367
- // Опасное слева, обычное справа, всё в одной полосе. Отдельная строка
3368
- // под «Удалить» съедала высоту окна ради одной кнопки, которую
3369
- // нажимают раз в жизни.
3370
- React.createElement('div', { className: 'dkb-foot dkb-footSplit' },
3371
- React.createElement('div', { className: 'dkb-footLeft' },
3372
- React.createElement('button', {
3554
+ h('div', { className: 'dkb-foot dkb-footSplit' },
3555
+ h('div', { className: 'dkb-footLeft' },
3556
+ h('button', {
3373
3557
  type: 'button', className: 'dkb-danger',
3374
3558
  onClick: async () => {
3375
- // Подтверждение обязательно: удаление карточки необратимо, а
3376
- // сама кнопка стоит рядом с безобидными.
3377
3559
  if (!window.confirm(t('panel.deleteConfirm', { title: openTask.title }))) return
3378
3560
  try {
3379
3561
  await api('/task/' + encodeURIComponent(openTask.id), { method: 'DELETE' })
@@ -3385,7 +3567,7 @@ window.__ModuleLoader__.load({
3385
3567
  },
3386
3568
  }, t('panel.delete')),
3387
3569
  openTask.column === 'done'
3388
- ? React.createElement('button', {
3570
+ ? h('button', {
3389
3571
  type: 'button', className: 'dkb-discard',
3390
3572
  onClick: async () => {
3391
3573
  try {
@@ -3397,7 +3579,7 @@ window.__ModuleLoader__.load({
3397
3579
  }, t('panel.archive'))
3398
3580
  : null,
3399
3581
  openTask.column === 'cleanup' && openTask.worktree
3400
- ? React.createElement('button', {
3582
+ ? h('button', {
3401
3583
  type: 'button', className: 'dkb-discard',
3402
3584
  onClick: async () => {
3403
3585
  try {
@@ -3415,7 +3597,7 @@ window.__ModuleLoader__.load({
3415
3597
  }, t('panel.cleanupWorktree'))
3416
3598
  : null,
3417
3599
  ),
3418
- (openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? React.createElement('button', {
3600
+ (openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? h('button', {
3419
3601
  type: 'button',
3420
3602
  className: 'dkb-secondary',
3421
3603
  style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-color-primary, #444)' },
@@ -3442,11 +3624,9 @@ window.__ModuleLoader__.load({
3442
3624
  }
3443
3625
  },
3444
3626
  }, t('action.resume')) : null,
3445
- React.createElement('button', {
3627
+ h('button', {
3446
3628
  type: 'button', className: 'dkb-save', disabled: starting || model === '',
3447
3629
  onClick: async () => {
3448
- // Кнопка блокируется на время запроса: двойное нажатие подняло
3449
- // бы две сессии по одной задаче.
3450
3630
  setStarting(true)
3451
3631
  try {
3452
3632
  rememberLaunch(board, { provider, model, agentPreset, permission })
@@ -3456,9 +3636,6 @@ window.__ModuleLoader__.load({
3456
3636
  })
3457
3637
  setOpenTask(null)
3458
3638
  await load(board)
3459
- // Сессия поднята — но пока её не открыли, для человека
3460
- // «ничего не произошло»: он остаётся на доске и не видит
3461
- // чата, ради которого нажимал.
3462
3639
  if (!openSession(props.ctx, out && out.sessionId)) {
3463
3640
  setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
3464
3641
  return
@@ -3478,18 +3655,18 @@ window.__ModuleLoader__.load({
3478
3655
  : null
3479
3656
 
3480
3657
  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' },
3658
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3659
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3660
+ h('span', { className: 'dkb-title' },
3484
3661
  t('move.title', { column: t('column.' + dialog.column) })),
3485
- React.createElement('p', { className: 'dkb-note' },
3662
+ h('p', { className: 'dkb-note' },
3486
3663
  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', {
3664
+ h('p', { className: 'dkb-hint' }, t('move.' + dialog.column)),
3665
+ h('div', { className: 'dkb-row' },
3666
+ h('button', {
3490
3667
  type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
3491
3668
  }, t('dialog.cancel')),
3492
- React.createElement('button', {
3669
+ h('button', {
3493
3670
  type: 'button', className: 'dkb-save',
3494
3671
  onClick: () => {
3495
3672
  const d = dialog
@@ -3501,51 +3678,49 @@ window.__ModuleLoader__.load({
3501
3678
  ))
3502
3679
  : null
3503
3680
 
3504
- // Групповой запуск. Провайдер и модель спрашиваем один раз на всю пачку:
3505
- // сессия одна, значит и модель одна.
3506
3681
  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' },
3682
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3683
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3684
+ h('span', { className: 'dkb-title' },
3510
3685
  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' },
3686
+ h('p', { className: 'dkb-hint' }, t('batch.hint')),
3687
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
3688
+ h('ul', { className: 'dkb-batchList' },
3514
3689
  dialog.ids.map((id, at) => {
3515
3690
  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))
3691
+ return h('li', { className: 'dkb-batchRow', key: id },
3692
+ h('span', { className: 'dkb-refNum' }, String(at + 1)),
3693
+ h('span', null, task ? task.title : id))
3519
3694
  })),
3520
- React.createElement('div', { className: 'dkb-field' },
3521
- React.createElement('span', { className: 'dkb-label' }, t('panel.provider')),
3522
- React.createElement('select', {
3695
+ h('div', { className: 'dkb-field' },
3696
+ h('span', { className: 'dkb-label' }, t('panel.provider')),
3697
+ h('select', {
3523
3698
  className: 'dkb-input', value: provider,
3524
3699
  onChange: (e) => { setProvider(e.target.value); setModel('') },
3525
3700
  },
3526
- React.createElement('option', { value: '' }, t('panel.pickProvider')),
3527
- providers.map((p) => React.createElement('option', { value: p.id, key: p.id }, p.name)),
3701
+ h('option', { value: '' }, t('panel.pickProvider')),
3702
+ providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
3528
3703
  ),
3529
3704
  ),
3530
- React.createElement('div', { className: 'dkb-field' },
3531
- React.createElement('span', { className: 'dkb-label' }, t('panel.model')),
3532
- React.createElement('select', {
3705
+ h('div', { className: 'dkb-field' },
3706
+ h('span', { className: 'dkb-label' }, t('panel.model')),
3707
+ h('select', {
3533
3708
  className: 'dkb-input', value: model, disabled: provider === '',
3534
3709
  onChange: (e) => setModel(e.target.value),
3535
3710
  },
3536
- React.createElement('option', { value: '' }, t('panel.pickModel')),
3537
- models.map((m) => React.createElement('option', { value: m.id, key: m.id }, m.name)),
3711
+ h('option', { value: '' }, t('panel.pickModel')),
3712
+ models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
3538
3713
  ),
3539
3714
  ),
3540
3715
  React.createElement(LaunchFields, {
3541
3716
  t, presets, agentPreset, permission,
3542
3717
  onPreset: setAgentPreset, onPermission: setPermission,
3543
3718
  }),
3544
- React.createElement('div', { className: 'dkb-row' },
3545
- React.createElement('button', {
3719
+ h('div', { className: 'dkb-row' },
3720
+ h('button', {
3546
3721
  type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
3547
3722
  }, t('dialog.cancel')),
3548
- React.createElement('button', {
3723
+ h('button', {
3549
3724
  type: 'button', className: 'dkb-save', disabled: model === '',
3550
3725
  onClick: async () => {
3551
3726
  try {
@@ -3557,8 +3732,6 @@ window.__ModuleLoader__.load({
3557
3732
  setDialog(null)
3558
3733
  setPicked({})
3559
3734
  await load(board)
3560
- // Сессия поднята — но пока её не открыли, для человека
3561
- // «ничего не произошло».
3562
3735
  if (!openSession(props.ctx, out && out.sessionId)) {
3563
3736
  setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
3564
3737
  return
@@ -3574,28 +3747,28 @@ window.__ModuleLoader__.load({
3574
3747
  : null
3575
3748
 
3576
3749
  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', {
3750
+ ? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
3751
+ h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
3752
+ h('span', { className: 'dkb-title' }, t('queue.title')),
3753
+ h('p', { className: 'dkb-hint' }, t('queue.hint')),
3754
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
3755
+ h('div', { className: 'dkb-field' },
3756
+ h('span', { className: 'dkb-label' }, t('queue.session')),
3757
+ h('select', {
3585
3758
  className: 'dkb-input', value: dialog.sessionId,
3586
3759
  onChange: (e) => setDialog(Object.assign({}, dialog, { sessionId: e.target.value })),
3587
- }, dialog.sessions.map((one) => React.createElement('option', {
3760
+ }, dialog.sessions.map((one) => h('option', {
3588
3761
  value: one.sessionId, key: one.sessionId,
3589
3762
  }, t('queue.option', {
3590
3763
  n: one.tasks.length,
3591
3764
  title: (one.tasks[0] && one.tasks[0].title) || one.sessionId,
3592
3765
  })))),
3593
3766
  ),
3594
- React.createElement('div', { className: 'dkb-row' },
3595
- React.createElement('button', {
3767
+ h('div', { className: 'dkb-row' },
3768
+ h('button', {
3596
3769
  type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
3597
3770
  }, t('dialog.cancel')),
3598
- React.createElement('button', {
3771
+ h('button', {
3599
3772
  type: 'button', className: 'dkb-save',
3600
3773
  onClick: async () => {
3601
3774
  try {
@@ -3615,17 +3788,15 @@ window.__ModuleLoader__.load({
3615
3788
  : null
3616
3789
 
3617
3790
  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', {
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' }, t('dialog.title')),
3794
+ h('div', { className: 'dkb-row' },
3795
+ h('button', {
3623
3796
  type: 'button', className: dialog.mode === 'own' ? 'dkb-save' : 'dkb-discard',
3624
3797
  onClick: () => setDialog(Object.assign({}, dialog, { mode: 'own', error: '' })),
3625
3798
  }, t('dialog.own')),
3626
- // Простая доска — для задач без issue, и предлагать на ней импорт
3627
- // значит звать туда, откуда сервер вернёт отказ.
3628
- state.kind === 'simple' ? null : React.createElement('button', {
3799
+ state.kind === 'simple' ? null : h('button', {
3629
3800
  type: 'button', className: dialog.mode === 'gitea' ? 'dkb-save' : 'dkb-discard',
3630
3801
  onClick: async () => {
3631
3802
  setDialog(Object.assign({}, dialog, { mode: 'gitea', error: '' }))
@@ -3639,93 +3810,86 @@ window.__ModuleLoader__.load({
3639
3810
  },
3640
3811
  }, t('dialog.fromGitea')),
3641
3812
  ),
3642
- dialog.error ? React.createElement('p', { className: 'dkb-failed' }, dialog.error) : null,
3813
+ dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
3643
3814
  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', {
3815
+ ? h('div', null,
3816
+ h('div', { className: 'dkb-field' },
3817
+ h('span', { className: 'dkb-label' }, t('dialog.kind')),
3818
+ h('div', { className: 'dkb-row' },
3819
+ h('button', {
3652
3820
  type: 'button', className: dialog.project ? 'dkb-save' : 'dkb-discard',
3653
3821
  onClick: () => setDialog(Object.assign({}, dialog, { project: true, error: '' })),
3654
3822
  }, t('dialog.kindProject')),
3655
- React.createElement('button', {
3823
+ h('button', {
3656
3824
  type: 'button', className: dialog.project ? 'dkb-discard' : 'dkb-save',
3657
3825
  onClick: () => setDialog(Object.assign({}, dialog, { project: false, error: '' })),
3658
3826
  }, t('dialog.kindPlain')),
3659
3827
  ),
3660
- React.createElement('p', { className: 'dkb-hint' },
3828
+ h('p', { className: 'dkb-hint' },
3661
3829
  t(dialog.project ? 'dialog.kindProjectHint' : 'dialog.kindPlainHint')),
3662
3830
  ),
3663
3831
  dialog.project
3664
- ? React.createElement('div', { className: 'dkb-field' },
3665
- React.createElement('span', { className: 'dkb-label' }, t('dialog.repo')),
3666
- React.createElement('select', {
3832
+ ? h('div', { className: 'dkb-field' },
3833
+ h('span', { className: 'dkb-label' }, t('dialog.repo')),
3834
+ h('select', {
3667
3835
  className: 'dkb-input', value: dialog.repo || '',
3668
3836
  onChange: (e) => setDialog(Object.assign({}, dialog, { repo: e.target.value })),
3669
3837
  },
3670
- React.createElement('option', { value: '' }, t('dialog.repoNew')),
3838
+ h('option', { value: '' }, t('dialog.repoNew')),
3671
3839
  (state.facets || [])
3672
3840
  .filter((f) => f.ns === REPO_FACET)
3673
3841
  .flatMap((f) => f.values)
3674
- .map((v) => React.createElement('option', { value: v.value, key: v.value }, v.value)),
3842
+ .map((v) => h('option', { value: v.value, key: v.value }, v.value)),
3675
3843
  ),
3676
3844
  (dialog.repo || '') === ''
3677
- ? React.createElement('div', null,
3678
- React.createElement('input', {
3845
+ ? h('div', null,
3846
+ h('input', {
3679
3847
  className: 'dkb-input', value: dialog.newRepo || '',
3680
3848
  placeholder: t('dialog.repoNamePlaceholder'),
3681
3849
  onChange: (e) => setDialog(Object.assign({}, dialog, { newRepo: e.target.value })),
3682
3850
  }),
3683
- // Создание репозитория необратимо в один клик, и
3684
- // человек должен видеть это ДО нажатия, а не после.
3685
- React.createElement('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
3851
+ h('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
3686
3852
  )
3687
3853
  : null,
3688
3854
  )
3689
3855
  : null,
3690
- React.createElement('div', { className: 'dkb-field' },
3691
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
3692
- React.createElement('input', {
3856
+ h('div', { className: 'dkb-field' },
3857
+ h('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
3858
+ h('input', {
3693
3859
  className: 'dkb-input', value: dialog.title, autoFocus: true,
3694
3860
  onChange: (e) => setDialog(Object.assign({}, dialog, { title: e.target.value })),
3695
3861
  }),
3696
3862
  ),
3697
- React.createElement('div', { className: 'dkb-field' },
3698
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskBody')),
3699
- React.createElement('textarea', {
3863
+ h('div', { className: 'dkb-field' },
3864
+ h('span', { className: 'dkb-label' }, t('dialog.taskBody')),
3865
+ h('textarea', {
3700
3866
  className: 'dkb-area', value: dialog.body || '',
3701
3867
  onChange: (e) => setDialog(Object.assign({}, dialog, { body: e.target.value })),
3702
3868
  }),
3703
- React.createElement('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
3869
+ h('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
3704
3870
  ),
3705
- React.createElement('div', { className: 'dkb-field' },
3706
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
3707
- React.createElement('input', {
3871
+ h('div', { className: 'dkb-field' },
3872
+ h('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
3873
+ h('input', {
3708
3874
  className: 'dkb-input', value: dialog.labels || '',
3709
3875
  placeholder: t('dialog.labelsHint'),
3710
3876
  onChange: (e) => setDialog(Object.assign({}, dialog, { labels: e.target.value })),
3711
3877
  }),
3712
3878
  ),
3713
- React.createElement('div', { className: 'dkb-field' },
3714
- React.createElement('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
3715
- React.createElement('select', {
3879
+ h('div', { className: 'dkb-field' },
3880
+ h('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
3881
+ h('select', {
3716
3882
  className: 'dkb-input', value: dialog.column || 'backlog',
3717
3883
  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)))),
3884
+ }, (state.columns || []).map((c) => h('option', { value: c.id, key: c.id }, t('column.' + c.id)))),
3719
3885
  ),
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', {
3886
+ h('div', { className: 'dkb-foot' },
3887
+ h('button', { type: 'button', className: 'dkb-discard', onClick: () => setDialog(null) }, t('dialog.cancel')),
3888
+ h('button', {
3723
3889
  type: 'button', className: 'dkb-save', disabled: String(dialog.title || '').trim() === '',
3724
3890
  onClick: async () => {
3725
3891
  try {
3726
3892
  if (dialog.project) {
3727
- // Проектная задача заводит issue и привязывается к
3728
- // нему; метки ей ставит Gitea, а не мы.
3729
3893
  await api('/project-task', {
3730
3894
  method: 'POST',
3731
3895
  body: JSON.stringify({
@@ -3742,12 +3906,8 @@ window.__ModuleLoader__.load({
3742
3906
  body: JSON.stringify({
3743
3907
  title: dialog.title,
3744
3908
  body: dialog.body || '',
3745
- // Метки вводятся через запятую: отдельный редактор
3746
- // меток ради своей задачи — лишняя мебель.
3747
3909
  labels: String(dialog.labels || '').split(',').map((x) => x.trim()).filter(Boolean),
3748
3910
  column: dialog.column || 'backlog',
3749
- // Задача без issue живёт на простой доске: правило
3750
- // без исключений, и помнить, где её завели, не надо.
3751
3911
  board: 'simple',
3752
3912
  }),
3753
3913
  })
@@ -3761,9 +3921,9 @@ window.__ModuleLoader__.load({
3761
3921
  }, t('dialog.create')),
3762
3922
  ),
3763
3923
  )
3764
- : React.createElement('div', null,
3765
- React.createElement('div', { className: 'dkb-row' },
3766
- React.createElement('select', {
3924
+ : h('div', null,
3925
+ h('div', { className: 'dkb-row' },
3926
+ h('select', {
3767
3927
  className: 'dkb-input', value: dialog.full,
3768
3928
  disabled: dialog.repos === null || dialog.repos.length === 0,
3769
3929
  onChange: async (e) => {
@@ -3780,20 +3940,20 @@ window.__ModuleLoader__.load({
3780
3940
  }
3781
3941
  },
3782
3942
  },
3783
- React.createElement('option', { value: '' },
3943
+ h('option', { value: '' },
3784
3944
  dialog.repos === null ? t('dialog.loadingRepos')
3785
3945
  : dialog.repos.length === 0 ? t('dialog.noRepos') : t('dialog.pickRepo')),
3786
3946
  (dialog.repos || []).map((r) => {
3787
3947
  const full = r.fullName || (r.owner + '/' + r.repo)
3788
- return React.createElement('option', { value: full, key: full }, repoOption(r, t))
3948
+ return h('option', { value: full, key: full }, repoOption(r, t))
3789
3949
  }),
3790
3950
  ),
3791
3951
  ),
3792
3952
  dialog.issues === null ? null
3793
3953
  : 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', {
3954
+ ? h('p', { className: 'dkb-note' }, t('dialog.noIssues'))
3955
+ : h('div', { className: 'dkb-list' }, dialog.issues.map((issue) =>
3956
+ h('button', {
3797
3957
  type: 'button',
3798
3958
  className: 'dkb-issue' + (issue.imported ? ' dkb-issueUsed' : ''),
3799
3959
  key: issue.number,
@@ -3816,25 +3976,21 @@ window.__ModuleLoader__.load({
3816
3976
  ))
3817
3977
  : null
3818
3978
 
3819
- // Архив — свой экран, а не колонка: колонка это стадия работы, а архив
3820
- // стадией не является. Возврат ставит карточку ровно туда, где она была.
3821
3979
  const archiveEl = archive
3822
- ? React.createElement('div', { className: 'dkb-archive' },
3823
- React.createElement('p', { className: 'dkb-hint' }, t('archive.hint')),
3980
+ ? h('div', { className: 'dkb-archive' },
3981
+ h('p', { className: 'dkb-hint' }, t('archive.hint')),
3824
3982
  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', {
3983
+ ? h('p', { className: 'dkb-note' }, t('archive.empty'))
3984
+ : h('ul', { className: 'dkb-archiveList' },
3985
+ archive.map((task) => h('li', { className: 'dkb-archiveRow', key: task.id },
3986
+ h('button', {
3829
3987
  type: 'button', className: 'dkb-archiveTitle dkb-archiveOpen',
3830
- // Архив — не братская могила: задачу туда убрали, а не
3831
- // стёрли, и прочитать её должно быть можно, не возвращая.
3832
3988
  onClick: () => setOpenTask(task),
3833
3989
  },
3834
3990
  [cardRef(task), task.title].filter(Boolean).join(' · ')),
3835
- React.createElement('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
3991
+ h('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
3836
3992
  t('archive.since', { ago: agoText(state.now, task.archivedAt, t) })),
3837
- React.createElement('button', {
3993
+ h('button', {
3838
3994
  type: 'button', className: 'dkb-discard',
3839
3995
  onClick: async () => {
3840
3996
  try {
@@ -3848,45 +4004,43 @@ window.__ModuleLoader__.load({
3848
4004
  )
3849
4005
  : null
3850
4006
 
3851
- // Метрика — это взгляд на журнал, а не новые данные: экран ничего не
3852
- // меняет, поэтому и кнопок действий на нём нет.
3853
4007
  const metricsEl = metrics
3854
- ? React.createElement('div', { className: 'dkb-archive' },
3855
- React.createElement('p', { className: 'dkb-hint' },
4008
+ ? h('div', { className: 'dkb-archive' },
4009
+ h('p', { className: 'dkb-hint' },
3856
4010
  t('metrics.hint', { n: metrics.tasks, week: metrics.done.week, month: metrics.done.month })),
3857
- React.createElement('div', { className: 'dkb-group' }, t('metrics.columns')),
4011
+ h('div', { className: 'dkb-group' }, t('metrics.columns')),
3858
4012
  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', {
4013
+ ? h('p', { className: 'dkb-note' }, t('metrics.empty'))
4014
+ : h('ul', { className: 'dkb-archiveList' },
4015
+ metrics.columns.map((row) => h('li', {
3862
4016
  className: 'dkb-archiveRow', key: row.column,
3863
4017
  },
3864
- React.createElement('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
3865
- React.createElement('span', { className: 'dkb-taskRef' },
4018
+ h('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
4019
+ h('span', { className: 'dkb-taskRef' },
3866
4020
  t('metrics.median', { time: agoText(0, -row.median, t) })),
3867
- React.createElement('span', { className: 'dkb-taskRef' },
4021
+ h('span', { className: 'dkb-taskRef' },
3868
4022
  t('metrics.mean', { time: agoText(0, -row.mean, t) })),
3869
- React.createElement('span', { className: 'dkb-count' }, String(row.tasks)),
4023
+ h('span', { className: 'dkb-count' }, String(row.tasks)),
3870
4024
  ))),
3871
- React.createElement('div', { className: 'dkb-group' },
4025
+ h('div', { className: 'dkb-group' },
3872
4026
  t('metrics.stale', { days: metrics.staleDays })),
3873
4027
  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', {
4028
+ ? h('p', { className: 'dkb-note' }, t('metrics.noStale'))
4029
+ : h('ul', { className: 'dkb-archiveList' },
4030
+ metrics.stale.map((row) => h('li', {
3877
4031
  className: 'dkb-archiveRow', key: row.id,
3878
4032
  },
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) },
4033
+ h('span', { className: 'dkb-archiveTitle' }, row.title),
4034
+ h('span', { className: 'dkb-taskRef' }, columnTitle({ id: row.column }, t)),
4035
+ h('span', { className: 'dkb-taskRef', title: exactAt(row.since) },
3882
4036
  agoText(state.now, row.since, t)),
3883
4037
  ))),
3884
4038
  )
3885
4039
  : null
3886
4040
 
3887
- return React.createElement('div', { className: 'dkb-screen' },
4041
+ return h('div', { className: 'dkb-screen' },
3888
4042
  style, bar,
3889
- error ? React.createElement('p', { className: 'dkb-failed' }, error) : null,
4043
+ error ? h('p', { className: 'dkb-failed' }, error) : null,
3890
4044
  metrics ? metricsEl : archive ? archiveEl : columns,
3891
4045
  panel, dialogEl, moveDialogEl, batchEl, queueEl,
3892
4046
  )
@@ -3934,35 +4088,32 @@ window.__ModuleLoader__.load({
3934
4088
  if (!task) return null
3935
4089
 
3936
4090
  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', {
4091
+ ensureCss()
4092
+ return h('span', { className: 'dkb-chipWrap' },
4093
+ h('button', {
3940
4094
  type: 'button', className: 'dkb-chip', title: task.title,
3941
4095
  onClick: () => setOpen((v) => !v),
3942
4096
  }, '▦ ' + 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', {
4097
+ open ? h('div', { className: 'dkb-chipPanel' },
4098
+ h('span', { className: 'dkb-hint' }, t('chip.moveTo')),
4099
+ columns.map((id) => h('button', {
3946
4100
  type: 'button', key: id,
3947
4101
  className: id === task.column ? 'dkb-save' : 'dkb-discard',
3948
4102
  onClick: () => move(id),
3949
4103
  }, t('column.' + id))),
3950
4104
  props.toggle
3951
- ? React.createElement('button', {
4105
+ ? h('button', {
3952
4106
  type: 'button', className: 'dkb-discard',
3953
4107
  onClick: () => { setOpen(false); props.toggle.show(task.id, task.board) },
3954
4108
  }, t('chip.openCard'))
3955
4109
  : null,
3956
- React.createElement('span', { className: 'dkb-hint' }, t('chip.note')),
3957
- React.createElement('textarea', {
4110
+ h('span', { className: 'dkb-hint' }, t('chip.note')),
4111
+ h('textarea', {
3958
4112
  className: 'dkb-area', value: note, disabled: saving,
3959
4113
  onChange: (e) => setNote(e.target.value),
3960
4114
  }),
3961
- // Тело уходит агенту первым сообщением при запуске: заметка до
3962
- // запуска попадёт в работу, после — нет. Молчать об этом значит
3963
- // сделать поведение случайным на вид.
3964
- React.createElement('span', { className: 'dkb-hint' }, t('chip.noteHint')),
3965
- React.createElement('button', {
4115
+ h('span', { className: 'dkb-hint' }, t('chip.noteHint')),
4116
+ h('button', {
3966
4117
  type: 'button', className: 'dkb-save',
3967
4118
  disabled: saving || note.trim() === '',
3968
4119
  onClick: async () => {
@@ -3984,17 +4135,6 @@ window.__ModuleLoader__.load({
3984
4135
  }
3985
4136
 
3986
4137
 
3987
- // ------------------------------------------------- встраивание в оболочку
3988
- //
3989
- // Раздела верхнего уровня в этой сборке нет: слоты `app.section` и
3990
- // `sidebar.section` отсутствуют, а `settings.section` уводит доску в
3991
- // настройки, где ей не место. Поэтому строка и экран вставляются прямо в
3992
- // вёрстку оболочки — тем же приёмом, каким это делает штатный task board.
3993
- //
3994
- // Приём хрупкий по своей природе: он опирается на чужую вёрстку. Смягчение
3995
- // — селекторы по подстроке класса, наблюдатели за перерисовкой React и
3996
- // мягкий отказ: не нашли корень — просто ничего не показали, ничего не
3997
- // сломали.
3998
4138
 
3999
4139
  const PANEL = 'kanban'
4000
4140
  const ACTIVATE_EVENT = 'dsh-panel-activate'
@@ -4012,8 +4152,6 @@ window.__ModuleLoader__.load({
4012
4152
  /** @see helpers.createToggle */
4013
4153
  function createToggle() {
4014
4154
  let open = false
4015
- // Задача, которую просили показать при открытии. Состояние окна живёт
4016
- // внутри экрана доски, и снаружи до него иначе не дотянуться.
4017
4155
  let wanted = undefined
4018
4156
  const listeners = new Set()
4019
4157
  return {
@@ -4026,8 +4164,6 @@ window.__ModuleLoader__.load({
4026
4164
  toggle() { this.set(!open) },
4027
4165
  /** Открыть доску на конкретной задаче. */
4028
4166
  show(taskId, boardId) {
4029
- // Доска едет вместе с задачей: карточка может лежать на простой, а
4030
- // открытой быть проектная, и тогда искать её было бы негде.
4031
4167
  wanted = { id: String(taskId || ''), board: String(boardId || 'main') }
4032
4168
  if (open) { for (const fn of listeners) fn() } else this.set(true)
4033
4169
  },
@@ -4124,8 +4260,6 @@ window.__ModuleLoader__.load({
4124
4260
  else delete entry.dataset.active
4125
4261
  }
4126
4262
 
4127
- // Оболочка перерисовывается сама по себе: без наблюдателя кнопка исчезнет
4128
- // при первом же обновлении списка сессий.
4129
4263
  const observer = new MutationObserver(tryPlace)
4130
4264
  observer.observe(document.body, { childList: true, subtree: true })
4131
4265
  const unsubscribe = toggle.subscribe(syncActive)
@@ -4154,7 +4288,7 @@ window.__ModuleLoader__.load({
4154
4288
  container.setAttribute('data-dsh-plugin', 'kanban')
4155
4289
  column.appendChild(container)
4156
4290
  root = ReactDOM.createRoot(container)
4157
- root.render(React.createElement(BoardScreen, { ctx, toggle, onClose: () => toggle.set(false) }))
4291
+ root.render(h(BoardScreen, { ctx, toggle, onClose: () => toggle.set(false) }))
4158
4292
  }
4159
4293
 
4160
4294
  const applyActive = () => {
@@ -4166,8 +4300,6 @@ window.__ModuleLoader__.load({
4166
4300
  }
4167
4301
  }
4168
4302
 
4169
- // Соседние панели объявляют себя тем же событием: две панели поверх одной
4170
- // колонки одновременно — это каша, поэтому уступаем.
4171
4303
  const onOtherActivate = (event) => {
4172
4304
  if (event.detail !== PANEL && toggle.isOpen()) toggle.set(false)
4173
4305
  }
@@ -4198,7 +4330,6 @@ window.__ModuleLoader__.load({
4198
4330
  }
4199
4331
  }
4200
4332
 
4201
- // ------------------------------------------------------------ регистрация
4202
4333
 
4203
4334
  /**
4204
4335
  * Зарегистрировать компонент в первый слот, который его примет.
@@ -4235,31 +4366,23 @@ window.__ModuleLoader__.load({
4235
4366
  function apply(ctx) {
4236
4367
  ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-kanban: словари')
4237
4368
 
4238
- // Карточка настроек в Настройки → Плагины → Настройки плагинов.
4239
- // Ключ обязан совпадать с пространством настроек: вкладка перебирает
4240
- // объявленные пространства и рисует слот settings.plugin.item с entryKey,
4241
- // равным имени пространства.
4242
- // Карточка настроек. В сборке с вкладкой «Настройки плагинов» регистрируется
4243
- // в settings.plugin.item, иначе откатывается в отдельный раздел settings.section.
4369
+ refreshMirrorUntilVisible(ctx)
4370
+ const SettingsErrorBoundary = createErrorBoundary()
4371
+ const SafeSettingsCard = (props) => h(SettingsErrorBoundary, null, h(KanbanSettingsCard, props))
4244
4372
  const cardSlot = registerFirst(ctx, [
4245
4373
  { name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
4246
- { name: 'settings.section', id: '@goodandready/dsh-kanban', order: 32, locale: NS, label: () => fallbackT('title'), inject: () => ({ ctx }) },
4247
- ], KanbanSettingsCard)
4374
+ ], SafeSettingsCard)
4248
4375
 
4249
- // Экран доски встраивается в оболочку, а не в слот: раздела верхнего
4250
- // уровня в сборке нет, а настройки — не место для доски.
4251
4376
  const toggle = createToggle()
4252
4377
  ctx.effect(() => {
4253
4378
  const off = [mountSidebarEntry(toggle, fallbackT('section.label')), mountBoard(ctx, toggle)]
4254
4379
  return () => { for (const dispose of off) dispose() }
4255
4380
  }, 'dsh-kanban: доска в оболочке')
4256
4381
 
4257
- // Чип задачи в шапке чата. Слот подтверждён живым плагином dsh-gitea.
4258
4382
  const chipSlot = registerFirst(ctx, [
4259
4383
  { name: 'conversation.session.header.utilities', id: '@goodandready/dsh-kanban.chip', order: 26, locale: NS, inject: () => ({ ctx, toggle }) },
4260
4384
  ], TaskChip)
4261
4385
 
4262
- // Куда встали — видно снаружи: живая проверка сверяет это с интерфейсом.
4263
4386
  exports.slots = { card: cardSlot, chip: chipSlot }
4264
4387
  }
4265
4388