issue-flow 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/dist/agent-HMZTXCRT.js +30 -0
  2. package/dist/{analyze-556KCJKZ.js → analyze-Q4ONIWRZ.js} +22 -22
  3. package/dist/{apply-WVV7G7WN.js → apply-EEFPY2W4.js} +14 -14
  4. package/dist/{bench-UBFGKUHR.js → bench-WWWXLTFH.js} +23 -23
  5. package/dist/{chunk-GV2DABNZ.js → chunk-34OCSKOB.js} +12 -12
  6. package/dist/chunk-3R27MJ5R.js +25 -0
  7. package/dist/chunk-3R27MJ5R.js.map +1 -0
  8. package/dist/{chunk-PGN2VX4Y.js → chunk-45WZ6GMI.js} +32 -48
  9. package/dist/chunk-45WZ6GMI.js.map +1 -0
  10. package/dist/{chunk-H3EYBJZU.js → chunk-62GVVZQO.js} +10 -10
  11. package/dist/{chunk-GPLP7EPL.js → chunk-6K2LCFG6.js} +10 -10
  12. package/dist/{chunk-GPLP7EPL.js.map → chunk-6K2LCFG6.js.map} +1 -1
  13. package/dist/{chunk-OGVWJO4S.js → chunk-7F7LGMVO.js} +3 -3
  14. package/dist/{chunk-X6GY3CM2.js → chunk-7UMMFOL2.js} +104 -104
  15. package/dist/chunk-7UMMFOL2.js.map +1 -0
  16. package/dist/{chunk-OLUOZLU5.js → chunk-CWSQVHVV.js} +1691 -1075
  17. package/dist/chunk-CWSQVHVV.js.map +1 -0
  18. package/dist/{chunk-MXCOSPLJ.js → chunk-DGRTJD5R.js} +3 -3
  19. package/dist/{chunk-ROIIEVZH.js → chunk-DUS5ZNKL.js} +474 -361
  20. package/dist/chunk-DUS5ZNKL.js.map +1 -0
  21. package/dist/{chunk-BSBJXPLC.js → chunk-E4I6MMHD.js} +10 -10
  22. package/dist/{chunk-QZOBR6YF.js → chunk-ECU7YO7S.js} +4 -4
  23. package/dist/chunk-ECU7YO7S.js.map +1 -0
  24. package/dist/{chunk-EPFC4LHT.js → chunk-EG2MGDP5.js} +9 -9
  25. package/dist/chunk-HOAHMERN.js +229 -0
  26. package/dist/chunk-HOAHMERN.js.map +1 -0
  27. package/dist/{chunk-QVLIIGBU.js → chunk-ICM463EO.js} +60 -13
  28. package/dist/chunk-ICM463EO.js.map +1 -0
  29. package/dist/{chunk-4FUWWBTP.js → chunk-KMCS76ZW.js} +8 -8
  30. package/dist/{chunk-SW2MF47D.js → chunk-KRAGAUKD.js} +19 -19
  31. package/dist/{chunk-OOX3RMD3.js → chunk-LGHND47K.js} +4 -4
  32. package/dist/{chunk-VTF4RWS5.js → chunk-LI35PIIS.js} +7 -7
  33. package/dist/{chunk-Q2TZA6AC.js → chunk-LTH6XYZ3.js} +14 -14
  34. package/dist/{chunk-YA6FWFYW.js → chunk-NEWNXZ65.js} +62 -11
  35. package/dist/chunk-NEWNXZ65.js.map +1 -0
  36. package/dist/{chunk-2H43OVTU.js → chunk-OODXFCQV.js} +11 -11
  37. package/dist/{chunk-KV2K7MK2.js → chunk-PMLE7KAQ.js} +20 -12
  38. package/dist/chunk-PMLE7KAQ.js.map +1 -0
  39. package/dist/{chunk-R42YHLDV.js → chunk-PPZ3K4YM.js} +2 -2
  40. package/dist/{chunk-J2LJB2OH.js → chunk-QF7VEL3M.js} +739 -579
  41. package/dist/chunk-QF7VEL3M.js.map +1 -0
  42. package/dist/{chunk-VK65RNFA.js → chunk-QJ44VG5G.js} +3 -3
  43. package/dist/{chunk-FKWYD44M.js → chunk-QPGEFLMY.js} +2 -2
  44. package/dist/{chunk-NQCR3HSU.js → chunk-RNFUVKC3.js} +3 -3
  45. package/dist/{chunk-G5LACAXO.js → chunk-RYOUZ6EK.js} +2 -2
  46. package/dist/{chunk-MCLPSL6B.js → chunk-SA6HXL4Y.js} +3 -3
  47. package/dist/{chunk-GVOZ6NDO.js → chunk-SOM4QA6C.js} +3 -3
  48. package/dist/{chunk-73TS2H42.js → chunk-TZYINKCC.js} +154 -34
  49. package/dist/chunk-TZYINKCC.js.map +1 -0
  50. package/dist/{chunk-DBEMD34J.js → chunk-V7QOIQYC.js} +8 -8
  51. package/dist/{chunk-IAMIBZRF.js → chunk-WCCZJL7W.js} +2 -2
  52. package/dist/{chunk-OGN2NX57.js → chunk-XUEMATVY.js} +9 -9
  53. package/dist/{chunk-SVDEWOUD.js → chunk-XZUEFTUP.js} +6 -6
  54. package/dist/{chunk-WLJVL25O.js → chunk-YUNBOUQH.js} +6 -6
  55. package/dist/{chunk-WNROH2YE.js → chunk-YVPZUN6X.js} +2 -2
  56. package/dist/chunk-ZVZMWNCG.js +415 -0
  57. package/dist/chunk-ZVZMWNCG.js.map +1 -0
  58. package/dist/cli.js +57 -49
  59. package/dist/cli.js.map +1 -1
  60. package/dist/{config-UE7IP36J.js → config-LEBZZCJP.js} +13 -13
  61. package/dist/{conventions-Z3QC2NSG.js → conventions-XOT6YW5L.js} +15 -15
  62. package/dist/{diagnostics-2Z5HFQOO.js → diagnostics-HTCWF7F4.js} +7 -7
  63. package/dist/execute-LELOSR5T.js +37 -0
  64. package/dist/{generate-PB4L7V5N.js → generate-JGW5JSKB.js} +22 -22
  65. package/dist/{git-7MUOGVRG.js → git-A2IP32PQ.js} +2 -2
  66. package/dist/init-TO6MUXO4.js +26 -0
  67. package/dist/{operations-QDJAFZUN.js → operations-O4FORFIH.js} +15 -15
  68. package/dist/{permissions-XVXXENVP.js → permissions-G4XCAESF.js} +4 -4
  69. package/dist/plan-OXAT344Q.js +34 -0
  70. package/dist/{policy-6WQZB6B2.js → policy-U5NBFAO5.js} +14 -14
  71. package/dist/pr-UZHNKKZC.js +39 -0
  72. package/dist/pr-review-Q4ACAWDM.js +32 -0
  73. package/dist/prd-6CYEMLDY.js +33 -0
  74. package/dist/{ps-CX7756QG.js → ps-76L6ZAZL.js} +12 -12
  75. package/dist/{recorder-RK6XZYF3.js → recorder-2BAKPBRA.js} +7 -7
  76. package/dist/{registry-POSB35Q4.js → registry-XLHHYLTR.js} +5 -5
  77. package/dist/{resume-OQJZV3JH.js → resume-URCU6CIJ.js} +39 -38
  78. package/dist/{resume-OQJZV3JH.js.map → resume-URCU6CIJ.js.map} +1 -1
  79. package/dist/review-LVZT2TMW.js +35 -0
  80. package/dist/routing-EK6RBZM4.js +33 -0
  81. package/dist/run-2RXJCJKT.js +56 -0
  82. package/dist/{session-publisher-VVXOCJ2S.js → session-publisher-KAJQRHNZ.js} +3 -2
  83. package/dist/{state-manager-TIC73GQY.js → state-manager-UPHO2EA2.js} +4 -4
  84. package/dist/state-manager-UPHO2EA2.js.map +1 -0
  85. package/dist/{usage-WDDNSIHP.js → usage-KTMYXAPF.js} +12 -12
  86. package/dist/{web-IPGY52AK.js → web-B5UPHHR5.js} +20 -18
  87. package/dist/{web-IPGY52AK.js.map → web-B5UPHHR5.js.map} +1 -1
  88. package/package.json +6 -5
  89. package/web/AGENTS.md +125 -18
  90. package/web/public/app.css +420 -234
  91. package/web/public/app.js +371 -80
  92. package/web/public/index.html +106 -78
  93. package/dist/agent-PMWCDWOW.js +0 -30
  94. package/dist/chunk-73TS2H42.js.map +0 -1
  95. package/dist/chunk-7Z4Y3VKM.js +0 -26
  96. package/dist/chunk-7Z4Y3VKM.js.map +0 -1
  97. package/dist/chunk-AOAQU7PD.js +0 -180
  98. package/dist/chunk-AOAQU7PD.js.map +0 -1
  99. package/dist/chunk-J2LJB2OH.js.map +0 -1
  100. package/dist/chunk-KV2K7MK2.js.map +0 -1
  101. package/dist/chunk-OLUOZLU5.js.map +0 -1
  102. package/dist/chunk-PGN2VX4Y.js.map +0 -1
  103. package/dist/chunk-QVLIIGBU.js.map +0 -1
  104. package/dist/chunk-QZOBR6YF.js.map +0 -1
  105. package/dist/chunk-ROIIEVZH.js.map +0 -1
  106. package/dist/chunk-X6GY3CM2.js.map +0 -1
  107. package/dist/chunk-YA6FWFYW.js.map +0 -1
  108. package/dist/execute-QYLJ5L4T.js +0 -36
  109. package/dist/init-XBERAZ4O.js +0 -24
  110. package/dist/plan-RXOW32KY.js +0 -34
  111. package/dist/pr-3WFYAVA7.js +0 -39
  112. package/dist/pr-review-RK63ZEMM.js +0 -32
  113. package/dist/prd-LRV6IW3E.js +0 -33
  114. package/dist/review-AEFISJZD.js +0 -35
  115. package/dist/routing-EA5NJGNU.js +0 -94
  116. package/dist/routing-EA5NJGNU.js.map +0 -1
  117. package/dist/run-HSC7R33Y.js +0 -55
  118. /package/dist/{agent-PMWCDWOW.js.map → agent-HMZTXCRT.js.map} +0 -0
  119. /package/dist/{analyze-556KCJKZ.js.map → analyze-Q4ONIWRZ.js.map} +0 -0
  120. /package/dist/{apply-WVV7G7WN.js.map → apply-EEFPY2W4.js.map} +0 -0
  121. /package/dist/{bench-UBFGKUHR.js.map → bench-WWWXLTFH.js.map} +0 -0
  122. /package/dist/{chunk-GV2DABNZ.js.map → chunk-34OCSKOB.js.map} +0 -0
  123. /package/dist/{chunk-H3EYBJZU.js.map → chunk-62GVVZQO.js.map} +0 -0
  124. /package/dist/{chunk-OGVWJO4S.js.map → chunk-7F7LGMVO.js.map} +0 -0
  125. /package/dist/{chunk-MXCOSPLJ.js.map → chunk-DGRTJD5R.js.map} +0 -0
  126. /package/dist/{chunk-BSBJXPLC.js.map → chunk-E4I6MMHD.js.map} +0 -0
  127. /package/dist/{chunk-EPFC4LHT.js.map → chunk-EG2MGDP5.js.map} +0 -0
  128. /package/dist/{chunk-4FUWWBTP.js.map → chunk-KMCS76ZW.js.map} +0 -0
  129. /package/dist/{chunk-SW2MF47D.js.map → chunk-KRAGAUKD.js.map} +0 -0
  130. /package/dist/{chunk-OOX3RMD3.js.map → chunk-LGHND47K.js.map} +0 -0
  131. /package/dist/{chunk-VTF4RWS5.js.map → chunk-LI35PIIS.js.map} +0 -0
  132. /package/dist/{chunk-Q2TZA6AC.js.map → chunk-LTH6XYZ3.js.map} +0 -0
  133. /package/dist/{chunk-2H43OVTU.js.map → chunk-OODXFCQV.js.map} +0 -0
  134. /package/dist/{chunk-R42YHLDV.js.map → chunk-PPZ3K4YM.js.map} +0 -0
  135. /package/dist/{chunk-VK65RNFA.js.map → chunk-QJ44VG5G.js.map} +0 -0
  136. /package/dist/{chunk-FKWYD44M.js.map → chunk-QPGEFLMY.js.map} +0 -0
  137. /package/dist/{chunk-NQCR3HSU.js.map → chunk-RNFUVKC3.js.map} +0 -0
  138. /package/dist/{chunk-G5LACAXO.js.map → chunk-RYOUZ6EK.js.map} +0 -0
  139. /package/dist/{chunk-MCLPSL6B.js.map → chunk-SA6HXL4Y.js.map} +0 -0
  140. /package/dist/{chunk-GVOZ6NDO.js.map → chunk-SOM4QA6C.js.map} +0 -0
  141. /package/dist/{chunk-DBEMD34J.js.map → chunk-V7QOIQYC.js.map} +0 -0
  142. /package/dist/{chunk-IAMIBZRF.js.map → chunk-WCCZJL7W.js.map} +0 -0
  143. /package/dist/{chunk-OGN2NX57.js.map → chunk-XUEMATVY.js.map} +0 -0
  144. /package/dist/{chunk-SVDEWOUD.js.map → chunk-XZUEFTUP.js.map} +0 -0
  145. /package/dist/{chunk-WLJVL25O.js.map → chunk-YUNBOUQH.js.map} +0 -0
  146. /package/dist/{chunk-WNROH2YE.js.map → chunk-YVPZUN6X.js.map} +0 -0
  147. /package/dist/{config-UE7IP36J.js.map → config-LEBZZCJP.js.map} +0 -0
  148. /package/dist/{conventions-Z3QC2NSG.js.map → conventions-XOT6YW5L.js.map} +0 -0
  149. /package/dist/{diagnostics-2Z5HFQOO.js.map → diagnostics-HTCWF7F4.js.map} +0 -0
  150. /package/dist/{execute-QYLJ5L4T.js.map → execute-LELOSR5T.js.map} +0 -0
  151. /package/dist/{generate-PB4L7V5N.js.map → generate-JGW5JSKB.js.map} +0 -0
  152. /package/dist/{git-7MUOGVRG.js.map → git-A2IP32PQ.js.map} +0 -0
  153. /package/dist/{init-XBERAZ4O.js.map → init-TO6MUXO4.js.map} +0 -0
  154. /package/dist/{operations-QDJAFZUN.js.map → operations-O4FORFIH.js.map} +0 -0
  155. /package/dist/{permissions-XVXXENVP.js.map → permissions-G4XCAESF.js.map} +0 -0
  156. /package/dist/{plan-RXOW32KY.js.map → plan-OXAT344Q.js.map} +0 -0
  157. /package/dist/{policy-6WQZB6B2.js.map → policy-U5NBFAO5.js.map} +0 -0
  158. /package/dist/{pr-3WFYAVA7.js.map → pr-UZHNKKZC.js.map} +0 -0
  159. /package/dist/{pr-review-RK63ZEMM.js.map → pr-review-Q4ACAWDM.js.map} +0 -0
  160. /package/dist/{prd-LRV6IW3E.js.map → prd-6CYEMLDY.js.map} +0 -0
  161. /package/dist/{ps-CX7756QG.js.map → ps-76L6ZAZL.js.map} +0 -0
  162. /package/dist/{recorder-RK6XZYF3.js.map → recorder-2BAKPBRA.js.map} +0 -0
  163. /package/dist/{registry-POSB35Q4.js.map → registry-XLHHYLTR.js.map} +0 -0
  164. /package/dist/{review-AEFISJZD.js.map → review-LVZT2TMW.js.map} +0 -0
  165. /package/dist/{run-HSC7R33Y.js.map → routing-EK6RBZM4.js.map} +0 -0
  166. /package/dist/{session-publisher-VVXOCJ2S.js.map → run-2RXJCJKT.js.map} +0 -0
  167. /package/dist/{state-manager-TIC73GQY.js.map → session-publisher-KAJQRHNZ.js.map} +0 -0
  168. /package/dist/{usage-WDDNSIHP.js.map → usage-KTMYXAPF.js.map} +0 -0
package/web/public/app.js CHANGED
@@ -4,8 +4,6 @@
4
4
  // todo texto dinâmico entra via textContent (nunca innerHTML com dados
5
5
  // do snapshot).
6
6
  (() => {
7
- 'use strict';
8
-
9
7
  const REFRESH_OPTIONS = [3, 5, 10, 30];
10
8
  const PAUSED = 0;
11
9
  const STORAGE_KEY = 'issue-flow:refresh-seconds';
@@ -26,6 +24,16 @@
26
24
  failed: 'falhou',
27
25
  };
28
26
 
27
+ // O resumo do dashboard conta *execuções* (feminino), então não reaproveita
28
+ // STATUS_LABELS, que qualifica a execução no masculino usado pelo badge.
29
+ const SUMMARY_STATUS_ORDER = ['running', 'idle', 'completed', 'failed'];
30
+ const SUMMARY_STATUS_LABELS = {
31
+ running: ['em execução', 'em execução'],
32
+ idle: ['aguardando', 'aguardando'],
33
+ completed: ['concluída', 'concluídas'],
34
+ failed: ['com falha', 'com falha'],
35
+ };
36
+
29
37
  const PHASE_ICONS = {
30
38
  pending: '○',
31
39
  running: '●',
@@ -71,6 +79,7 @@
71
79
  dashboardMeta: document.getElementById('dashboard-meta'),
72
80
  backToDashboard: document.getElementById('back-to-dashboard'),
73
81
  issueLink: document.getElementById('issue-link'),
82
+ issueHeadline: document.getElementById('issue-headline'),
74
83
  branchLine: document.getElementById('branch-line'),
75
84
  statusBadge: document.getElementById('status-badge'),
76
85
  elapsed: document.getElementById('elapsed'),
@@ -91,6 +100,7 @@
91
100
  configuration: document.getElementById('configuration'),
92
101
  appVersion: document.getElementById('app-version'),
93
102
  appVersionDashboard: document.getElementById('app-version-dashboard'),
103
+ dashboardSummary: document.getElementById('dashboard-summary'),
94
104
  phases: document.getElementById('phases'),
95
105
  nextSteps: document.getElementById('next-steps'),
96
106
  stories: document.getElementById('stories'),
@@ -129,6 +139,7 @@
129
139
  // guardar o nó levaria a uma referência morta.
130
140
  selectedDetail: null,
131
141
  configWritable: false,
142
+ configData: null,
132
143
  // Identifica o processo que serviu os assets atuais. Uma troca significa
133
144
  // que --restart-web colocou código novo no mesmo origin e exige reload.
134
145
  serverInstanceId: null,
@@ -156,6 +167,21 @@
156
167
  return node;
157
168
  }
158
169
 
170
+ // Ponto pulsante único do painel — o mesmo componente em "Executando agora"
171
+ // e no card do dashboard. Sem texto: o rótulo adjacente carrega o significado.
172
+ function liveDot() {
173
+ const dot = el('span', 'live');
174
+ dot.setAttribute('aria-hidden', 'true');
175
+ return dot;
176
+ }
177
+
178
+ // Ícone textual (○ ● ✓ ✗): a cor e o rótulo ao lado já dizem o estado.
179
+ function statusIcon(status, symbol) {
180
+ const icon = el('span', 'item-icon icon-' + status, symbol || PHASE_ICONS[status] || '○');
181
+ icon.setAttribute('aria-hidden', 'true');
182
+ return icon;
183
+ }
184
+
159
185
  function link(href, text, className) {
160
186
  const node = el('a', className, text);
161
187
  node.href = href;
@@ -291,7 +317,8 @@
291
317
  ...story,
292
318
  status: STORY_STATUS_LABELS[status] !== undefined ? status : 'backlog',
293
319
  stage: STORY_STAGE_LABELS[stage] !== undefined ? stage : 'pending',
294
- stageSince: story.stageSince !== null && story.stageSince !== undefined ? story.stageSince : null,
320
+ stageSince:
321
+ story.stageSince !== null && story.stageSince !== undefined ? story.stageSince : null,
295
322
  stageDetail:
296
323
  story.stageDetail !== null && story.stageDetail !== undefined ? story.stageDetail : null,
297
324
  dependencies: list(story.dependencies),
@@ -318,15 +345,34 @@
318
345
  // Troca apenas visibilidade e estado ARIA. Não toca no polling: render()
319
346
  // segue atualizando os dois painéis, então a aba inativa nunca fica defasada.
320
347
 
321
- function setActiveTab(tabId) {
348
+ function setActiveTab(tabId, options) {
322
349
  state.activeTab = tabId;
350
+ let focused = null;
323
351
  for (const tab of els.tabs) {
324
352
  const active = tab.id === tabId;
325
353
  tab.setAttribute('aria-selected', active ? 'true' : 'false');
354
+ tab.tabIndex = active ? 0 : -1;
326
355
  tab.classList.toggle('is-active', active);
327
356
  const panel = document.getElementById(tab.getAttribute('aria-controls'));
328
357
  if (panel) panel.hidden = !active;
358
+ if (active) focused = tab;
329
359
  }
360
+ if (options && options.focus && focused) focused.focus();
361
+ }
362
+
363
+ // Padrão ARIA de tablist: setas movem entre as abas, Home/End vão às pontas.
364
+ // Só a aba ativa fica no fluxo do Tab (roving tabindex).
365
+ function onTabListKeydown(event) {
366
+ const current = els.tabs.indexOf(event.target);
367
+ if (current === -1) return;
368
+ let next = -1;
369
+ if (event.key === 'ArrowRight') next = (current + 1) % els.tabs.length;
370
+ else if (event.key === 'ArrowLeft') next = (current - 1 + els.tabs.length) % els.tabs.length;
371
+ else if (event.key === 'Home') next = 0;
372
+ else if (event.key === 'End') next = els.tabs.length - 1;
373
+ else return;
374
+ event.preventDefault();
375
+ setActiveTab(els.tabs[next].id, { focus: true });
330
376
  }
331
377
 
332
378
  // ---- Tema ------------------------------------------------------------------
@@ -613,9 +659,7 @@
613
659
  }
614
660
 
615
661
  const nextId =
616
- resolved.session && resolved.session.sessionId != null
617
- ? resolved.session.sessionId
618
- : null;
662
+ resolved.session && resolved.session.sessionId != null ? resolved.session.sessionId : null;
619
663
  const sessionChanged = state.detailSessionId !== nextId;
620
664
  if (sessionChanged) {
621
665
  clearDetailState();
@@ -662,6 +706,12 @@
662
706
  state.snapshot = await res.json();
663
707
  render();
664
708
  }
709
+ const configUrl = 'api/config?session=' + encodeURIComponent(state.detailSessionId || '');
710
+ const configRes = await fetch(configUrl, { cache: 'no-store' });
711
+ if (configRes.ok) {
712
+ state.configData = await configRes.json();
713
+ if (state.snapshot) renderConfiguration(state.snapshot);
714
+ }
665
715
  if (state.eventsUrl) {
666
716
  const eventsRes = await fetch(state.eventsUrl, { cache: 'no-store' });
667
717
  if (!eventsRes.ok) throw new Error('HTTP ' + eventsRes.status);
@@ -697,6 +747,9 @@
697
747
  function renderEmptyDetail() {
698
748
  if (state.snapshot) return;
699
749
  document.title = 'issue-flow';
750
+ els.issueLink.hidden = true;
751
+ els.issueHeadline.textContent = 'Nenhuma execução ativa';
752
+ els.branchLine.textContent = '';
700
753
  els.statusBadge.textContent = STATUS_LABELS.idle;
701
754
  els.statusBadge.className = 'badge status-idle';
702
755
  els.elapsed.textContent = '—';
@@ -711,13 +764,29 @@
711
764
  return normalized.slice(0, max - 1).trimEnd() + '…';
712
765
  }
713
766
 
767
+ // Linha secundária do header do dashboard: quantas execuções e em que
768
+ // estado. Substitui a legenda estática que só repetia o h1.
769
+ function renderDashboardSummary(sessions) {
770
+ const counts = {};
771
+ for (const session of sessions) {
772
+ const status = session.status || 'idle';
773
+ counts[status] = (counts[status] || 0) + 1;
774
+ }
775
+ const parts = [];
776
+ for (const status of SUMMARY_STATUS_ORDER) {
777
+ const count = counts[status];
778
+ if (!count) continue;
779
+ const label = SUMMARY_STATUS_LABELS[status];
780
+ parts.push(count + ' ' + (count === 1 ? label[0] : label[1]));
781
+ }
782
+ const total = sessions.length + (sessions.length === 1 ? ' execução' : ' execuções');
783
+ els.dashboardSummary.textContent = parts.length > 0 ? total + ' · ' + parts.join(' · ') : total;
784
+ }
785
+
714
786
  function renderDashboard(sessions) {
715
787
  const active = document.activeElement;
716
788
  const focusedId =
717
- active &&
718
- active.dataset &&
719
- els.dashboard.contains(active) &&
720
- active.dataset.sessionId
789
+ active && active.dataset && els.dashboard.contains(active) && active.dataset.sessionId
721
790
  ? active.dataset.sessionId
722
791
  : null;
723
792
 
@@ -726,10 +795,13 @@
726
795
  if (sessions.length === 0) {
727
796
  els.dashboard.appendChild(el('p', 'empty', 'Nenhuma execução ativa.'));
728
797
  els.dashboardMeta.textContent = '0 execuções';
798
+ els.dashboardSummary.textContent = 'Nenhuma execução ativa';
729
799
  document.title = 'issue-flow';
730
800
  return;
731
801
  }
732
802
 
803
+ renderDashboardSummary(sessions);
804
+
733
805
  for (const session of sessions) {
734
806
  // <button> só aceita phrasing content — mesmos spans do Kanban.
735
807
  const card = el('button', 'dashboard-card');
@@ -753,20 +825,15 @@
753
825
  if (session.issueNumber !== null && session.issueNumber !== undefined) {
754
826
  titleRow.appendChild(el('span', 'dashboard-issue', '#' + session.issueNumber));
755
827
  }
756
- titleRow.appendChild(
757
- el('span', 'dashboard-title', session.issueTitle || 'Sem título'),
758
- );
828
+ titleRow.appendChild(el('span', 'dashboard-title', session.issueTitle || 'Sem título'));
759
829
  card.appendChild(titleRow);
760
830
 
761
831
  const summary = truncateText(session.issueDescription, DESCRIPTION_PREVIEW);
762
832
  card.appendChild(el('span', 'dashboard-summary muted', summary || 'Sem descrição'));
763
833
 
764
834
  const meta = el('span', 'dashboard-meta-row');
765
- meta.appendChild(
766
- el('span', null, 'Fase: ' + (session.currentPhase || '—')),
767
- );
768
- const percent =
769
- typeof session.progressPercent === 'number' ? session.progressPercent : 0;
835
+ meta.appendChild(el('span', null, 'Fase: ' + (session.currentPhase || '—')));
836
+ const percent = typeof session.progressPercent === 'number' ? session.progressPercent : 0;
770
837
  meta.appendChild(el('span', null, percent + '%'));
771
838
  const elapsed =
772
839
  typeof session.elapsedSeconds === 'number'
@@ -802,7 +869,10 @@
802
869
  card.appendChild(progress);
803
870
 
804
871
  if (session.status === 'running') {
805
- card.appendChild(el('span', 'dashboard-live-dot', 'ao vivo'));
872
+ const live = el('span', 'live-label');
873
+ live.appendChild(liveDot());
874
+ live.appendChild(document.createTextNode('ao vivo'));
875
+ card.appendChild(live);
806
876
  }
807
877
 
808
878
  card.addEventListener('click', () => {
@@ -860,8 +930,11 @@
860
930
  document.title = (prefix + issue + ' · issue-flow').replace(/^ · /, '');
861
931
  }
862
932
 
933
+ // O h1 é a execução — número, título, e ao lado status, tempo decorrido e
934
+ // branch. A marca do produto vive só no <title> do documento.
863
935
  function renderHeader(snapshot) {
864
- if (snapshot.issue.number !== null) {
936
+ const hasIssue = snapshot.issue.number !== null && snapshot.issue.number !== undefined;
937
+ if (hasIssue) {
865
938
  els.issueLink.hidden = false;
866
939
  els.issueLink.textContent = '#' + snapshot.issue.number;
867
940
  if (snapshot.issue.url) els.issueLink.href = snapshot.issue.url;
@@ -870,6 +943,10 @@
870
943
  els.issueLink.hidden = true;
871
944
  }
872
945
 
946
+ // session.json antigo pode não ter título; sem issue vinculada, nem número.
947
+ els.issueHeadline.textContent =
948
+ snapshot.issue.title || (hasIssue ? 'Sem título' : 'Execução sem issue vinculada');
949
+
873
950
  const branch = snapshot.git.branch;
874
951
  const base = snapshot.git.baseBranch;
875
952
  const branchMode =
@@ -929,18 +1006,13 @@
929
1006
  clear(els.issueSummary);
930
1007
  const issue = snapshot.issue || {};
931
1008
 
932
- const heading = el('p', 'issue-summary-title');
933
- heading.appendChild(
934
- el('span', 'mono', issue.number !== null && issue.number !== undefined ? '#' + issue.number : '—'),
935
- );
936
- heading.appendChild(document.createTextNode(' ' + (issue.title || 'Sem título')));
937
- els.issueSummary.appendChild(heading);
938
-
1009
+ // Número e título saem no h1 do header (renderHeader); aqui fica só o que
1010
+ // o header não carrega — estado, labels e descrição.
939
1011
  const meta = el('div', 'issue-summary-meta');
940
1012
  const state = issue.state || null;
941
- const stateClass = state === 'open' ? 'state-open' : state === 'closed' ? 'state-closed' : 'state-unknown';
1013
+ const stateClass =
1014
+ state === 'open' ? 'state-open' : state === 'closed' ? 'state-closed' : 'state-unknown';
942
1015
  meta.appendChild(el('span', 'badge ' + stateClass, state || 'estado desconhecido'));
943
- meta.appendChild(el('span', 'muted', 'Prioridade: Não definida'));
944
1016
  els.issueSummary.appendChild(meta);
945
1017
 
946
1018
  const labels = issue.labels || [];
@@ -952,7 +1024,9 @@
952
1024
  els.issueSummary.appendChild(labelRow);
953
1025
  }
954
1026
 
955
- els.issueSummary.appendChild(el('p', 'issue-description', issue.description || 'Sem descrição.'));
1027
+ els.issueSummary.appendChild(
1028
+ el('p', 'issue-description', issue.description || 'Sem descrição.'),
1029
+ );
956
1030
  }
957
1031
 
958
1032
  function metaRow(grid, label, value, className) {
@@ -1005,16 +1079,16 @@
1005
1079
  const messages = {
1006
1080
  idle: 'Nenhuma execução em andamento.',
1007
1081
  completed: 'Execução concluída.',
1008
- failed: 'Execução falhou veja os erros acima.',
1082
+ failed: 'Execução falhou. Veja os erros acima.',
1009
1083
  };
1010
1084
  els.now.appendChild(el('p', 'empty', messages[snapshot.status] || '—'));
1011
1085
  return;
1012
1086
  }
1013
1087
 
1014
1088
  const grid = el('dl', 'now-grid');
1015
- const phase = el('dd');
1089
+ const phase = el('dd', 'now-phase');
1016
1090
  if (snapshot.currentPhase) {
1017
- phase.appendChild(el('span', 'pulse'));
1091
+ phase.appendChild(liveDot());
1018
1092
  phase.appendChild(document.createTextNode(snapshot.currentPhase));
1019
1093
  } else {
1020
1094
  phase.textContent = '—';
@@ -1024,7 +1098,7 @@
1024
1098
 
1025
1099
  const activity = snapshot.currentActivity;
1026
1100
  if (activity) {
1027
- if (activity.story) nowRow(grid, 'Story', activity.story);
1101
+ if (activity.story) nowRow(grid, 'User story', activity.story);
1028
1102
  if (activity.tool) nowRow(grid, 'Ferramenta', activity.tool);
1029
1103
  if (activity.detail) nowRow(grid, 'Detalhe', activity.detail);
1030
1104
  const since = el('dd', null, formatAgo(activity.since));
@@ -1063,6 +1137,7 @@
1063
1137
  env: 'variável de ambiente',
1064
1138
  cli: 'override da execução',
1065
1139
  fallback: 'fallback',
1140
+ recommended: 'política recomendada',
1066
1141
  };
1067
1142
  return labels[source] || source || 'não informado';
1068
1143
  }
@@ -1107,7 +1182,9 @@
1107
1182
  nowRow(
1108
1183
  summary,
1109
1184
  'Harness padrão',
1110
- (config.defaultProvider.value || '—') + ' · ' + configSourceLabel(config.defaultProvider.source),
1185
+ (config.defaultProvider.value || '—') +
1186
+ ' · ' +
1187
+ configSourceLabel(config.defaultProvider.source),
1111
1188
  );
1112
1189
  nowRow(
1113
1190
  summary,
@@ -1120,21 +1197,126 @@
1120
1197
  nowRow(summary, 'Precedência', list(config.precedence).join(' → '));
1121
1198
  els.configuration.appendChild(summary);
1122
1199
 
1200
+ const liveRouting = state.configData && state.configData.routing;
1201
+ if (liveRouting) {
1202
+ const routingSummary = el('dl', 'now-grid config-routing-summary');
1203
+ nowRow(routingSummary, 'Routing', liveRouting.mode + ' · perfil ' + liveRouting.profile);
1204
+ nowRow(
1205
+ routingSummary,
1206
+ 'Política',
1207
+ liveRouting.policy === 'recommended' ? 'recomendada (opt-in)' : 'score adaptativo',
1208
+ );
1209
+ els.configuration.appendChild(routingSummary);
1210
+
1211
+ if (state.configWritable) {
1212
+ const routingForm = el('form', 'config-form config-routing-form');
1213
+ const mode = el('select');
1214
+ mode.setAttribute('aria-label', 'Modo de routing');
1215
+ for (const value of ['off', 'shadow', 'recommend', 'active']) {
1216
+ const option = el('option', null, value);
1217
+ option.value = value;
1218
+ mode.appendChild(option);
1219
+ }
1220
+ mode.value = liveRouting.mode;
1221
+ const profile = el('select');
1222
+ profile.setAttribute('aria-label', 'Perfil de routing');
1223
+ for (const value of ['economy', 'balanced', 'quality', 'speed']) {
1224
+ const option = el('option', null, value);
1225
+ option.value = value;
1226
+ profile.appendChild(option);
1227
+ }
1228
+ profile.value = liveRouting.profile;
1229
+ const saveRouting = el('button', 'config-save', 'Salvar routing');
1230
+ saveRouting.type = 'submit';
1231
+ const applyPolicy = el(
1232
+ 'button',
1233
+ 'config-save config-policy',
1234
+ 'Aplicar política recomendada',
1235
+ );
1236
+ applyPolicy.type = 'button';
1237
+ const routingFeedback = el('span', 'muted');
1238
+ routingForm.appendChild(mode);
1239
+ routingForm.appendChild(profile);
1240
+ routingForm.appendChild(saveRouting);
1241
+ routingForm.appendChild(applyPolicy);
1242
+ routingForm.appendChild(routingFeedback);
1243
+ routingForm.addEventListener('submit', async (event) => {
1244
+ event.preventDefault();
1245
+ await saveRoutingConfig(
1246
+ { mode: mode.value, profile: profile.value },
1247
+ saveRouting,
1248
+ routingFeedback,
1249
+ );
1250
+ });
1251
+ applyPolicy.addEventListener('click', async () => {
1252
+ await saveRoutingConfig({ policy: 'recommended' }, applyPolicy, routingFeedback);
1253
+ });
1254
+ els.configuration.appendChild(routingForm);
1255
+ }
1256
+ }
1257
+
1123
1258
  const phases = el('div', 'config-phase-grid');
1124
1259
  for (const phase of list(config.phases)) {
1125
- const row = el('button', 'config-phase-row');
1126
- row.type = 'button';
1127
- row.appendChild(el('span', 'mono', phase.phase));
1128
- row.appendChild(el('span', null, phase.provider.value || '—'));
1129
- row.appendChild(el('span', null, phase.model.value || 'default do provider'));
1130
- row.appendChild(
1131
- el(
1132
- 'span',
1133
- 'muted',
1134
- configSourceLabel(phase.provider.source) + ' / ' + configSourceLabel(phase.model.source),
1135
- ),
1136
- );
1137
- row.addEventListener('click', () => openDrawer('phase', phase.phase));
1260
+ const row = el(state.configWritable ? 'form' : 'button', 'config-phase-row');
1261
+ if (!state.configWritable) row.type = 'button';
1262
+ if (state.configWritable) {
1263
+ row.classList.add('is-editable');
1264
+ const phaseButton = el('button', 'config-phase-open mono', phase.phase);
1265
+ phaseButton.type = 'button';
1266
+ phaseButton.addEventListener('click', () => openDrawer('phase', phase.phase));
1267
+ row.appendChild(phaseButton);
1268
+ const provider = el('select');
1269
+ provider.setAttribute('aria-label', 'Harness para ' + phase.phase);
1270
+ fillProviderSelect(provider, phase.provider.value || 'claude');
1271
+ const model = el('select');
1272
+ model.setAttribute('aria-label', 'Tier e modelo para ' + phase.phase);
1273
+ fillModelSelect(model, provider.value, phase.model.value || '');
1274
+ provider.addEventListener('change', () => fillModelSelect(model, provider.value, ''));
1275
+ const save = el('button', 'config-save', 'Salvar');
1276
+ save.type = 'submit';
1277
+ const feedback = el('span', 'muted config-phase-feedback');
1278
+ row.appendChild(provider);
1279
+ row.appendChild(model);
1280
+ row.appendChild(save);
1281
+ row.appendChild(feedback);
1282
+ row.addEventListener('submit', async (event) => {
1283
+ event.preventDefault();
1284
+ save.disabled = true;
1285
+ feedback.textContent = 'salvando…';
1286
+ try {
1287
+ const response = await fetch('api/config/agent', {
1288
+ method: 'POST',
1289
+ headers: { 'Content-Type': 'application/json' },
1290
+ body: JSON.stringify({
1291
+ provider: provider.value,
1292
+ model: model.value,
1293
+ phase: phase.phase,
1294
+ }),
1295
+ });
1296
+ const result = await response.json();
1297
+ if (!response.ok) throw new Error(result.error || 'falha ao salvar');
1298
+ feedback.textContent = 'salvo para a próxima execução';
1299
+ } catch (error) {
1300
+ feedback.textContent = error.message || 'falha ao salvar';
1301
+ } finally {
1302
+ save.disabled = false;
1303
+ }
1304
+ });
1305
+ } else {
1306
+ row.appendChild(el('span', 'mono', phase.phase));
1307
+ row.appendChild(el('span', null, phase.provider.value || '—'));
1308
+ row.appendChild(el('span', null, phase.model.value || 'default do provider'));
1309
+ row.appendChild(
1310
+ el(
1311
+ 'span',
1312
+ 'muted',
1313
+ configSourceLabel(phase.provider.source) +
1314
+ ' / ' +
1315
+ configSourceLabel(phase.model.source),
1316
+ ),
1317
+ );
1318
+ row.addEventListener('click', () => openDrawer('phase', phase.phase));
1319
+ }
1138
1320
  phases.appendChild(row);
1139
1321
  }
1140
1322
  els.configuration.appendChild(phases);
@@ -1182,6 +1364,75 @@
1182
1364
  els.configuration.appendChild(form);
1183
1365
  }
1184
1366
 
1367
+ function catalog() {
1368
+ return list(state.configData && state.configData.catalog);
1369
+ }
1370
+
1371
+ function fillProviderSelect(select, selected) {
1372
+ clear(select);
1373
+ const entries = catalog().filter((entry) => {
1374
+ if (!entry.installed) return false;
1375
+ if (entry.state === 'unavailable') return false;
1376
+ if (entry.authentication === 'failed') return false;
1377
+ return (
1378
+ entry.authenticated !== false || entry.state === 'conditional' || entry.state === 'ready'
1379
+ );
1380
+ });
1381
+ const providers = entries.length
1382
+ ? entries.map((entry) => entry.provider)
1383
+ : ['claude', 'codex', 'cursor', 'antigravity'];
1384
+ if (!providers.includes(selected)) providers.push(selected);
1385
+ for (const provider of providers) {
1386
+ const option = el('option', null, provider);
1387
+ option.value = provider;
1388
+ select.appendChild(option);
1389
+ }
1390
+ select.value = selected;
1391
+ }
1392
+
1393
+ function fillModelSelect(select, provider, selected) {
1394
+ clear(select);
1395
+ const defaultOption = el('option', null, 'default do provider');
1396
+ defaultOption.value = '';
1397
+ select.appendChild(defaultOption);
1398
+ const entry = catalog().find((candidate) => candidate.provider === provider);
1399
+ for (const model of list(entry && entry.models)) {
1400
+ if (!model.id) continue;
1401
+ const option = el('option', null, model.tier + ' · ' + model.id);
1402
+ option.value = model.id;
1403
+ select.appendChild(option);
1404
+ }
1405
+ if (
1406
+ selected &&
1407
+ !Array.prototype.some.call(select.options, (option) => option.value === selected)
1408
+ ) {
1409
+ const current = el('option', null, 'configurado · ' + selected);
1410
+ current.value = selected;
1411
+ select.appendChild(current);
1412
+ }
1413
+ select.value = selected || '';
1414
+ }
1415
+
1416
+ async function saveRoutingConfig(values, button, feedback) {
1417
+ button.disabled = true;
1418
+ feedback.textContent = 'salvando…';
1419
+ try {
1420
+ const response = await fetch('api/config/routing', {
1421
+ method: 'POST',
1422
+ headers: { 'Content-Type': 'application/json' },
1423
+ body: JSON.stringify(values),
1424
+ });
1425
+ const result = await response.json();
1426
+ if (!response.ok) throw new Error(result.error || 'falha ao salvar');
1427
+ state.configData.routing = Object.assign({}, state.configData.routing, values);
1428
+ feedback.textContent = 'salvo para execuções futuras';
1429
+ } catch (error) {
1430
+ feedback.textContent = error.message || 'falha ao salvar';
1431
+ } finally {
1432
+ button.disabled = false;
1433
+ }
1434
+ }
1435
+
1185
1436
  function renderPhases(snapshot) {
1186
1437
  clear(els.phases);
1187
1438
  if (snapshot.phases.length === 0) {
@@ -1191,7 +1442,7 @@
1191
1442
  for (const phase of snapshot.phases) {
1192
1443
  const item = el('li');
1193
1444
  item.className = 'detail-row';
1194
- item.appendChild(el('span', 'item-icon icon-' + phase.status, PHASE_ICONS[phase.status] || '○'));
1445
+ item.appendChild(statusIcon(phase.status));
1195
1446
  const main = el('div', 'item-main');
1196
1447
  main.appendChild(el('div', 'item-title', phase.name));
1197
1448
  if (phase.error) main.appendChild(el('div', 'item-error', phase.error));
@@ -1212,19 +1463,17 @@
1212
1463
  }
1213
1464
  }
1214
1465
 
1466
+ // Uma linha dentro do bloco de estado, não uma lista num cartão próprio: os
1467
+ // passos são poucos e curtos, e o rótulo já vem do HTML (.next-steps-label).
1215
1468
  function renderNextSteps(snapshot) {
1216
1469
  clear(els.nextSteps);
1217
1470
  if (snapshot.nextSteps.length === 0) {
1218
1471
  const message =
1219
1472
  snapshot.status === 'completed' ? 'Pipeline concluído.' : 'Nenhum passo pendente.';
1220
- els.nextSteps.appendChild(el('p', 'empty', message));
1473
+ els.nextSteps.appendChild(el('span', 'empty', message));
1221
1474
  return;
1222
1475
  }
1223
- const list = el('ol');
1224
- for (const step of snapshot.nextSteps) {
1225
- list.appendChild(el('li', null, step));
1226
- }
1227
- els.nextSteps.appendChild(list);
1476
+ els.nextSteps.appendChild(el('span', null, snapshot.nextSteps.join(' · ')));
1228
1477
  }
1229
1478
 
1230
1479
  function renderStories(snapshot) {
@@ -1236,11 +1485,12 @@
1236
1485
  for (const story of snapshot.stories) {
1237
1486
  // stage vem com default no schema ('pending'), mas session.json gravado
1238
1487
  // antes da issue 38 pode chegar sem ele.
1239
- const storyStage = story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
1488
+ const storyStage =
1489
+ story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
1240
1490
  const item = el('li', storyStage === 'executing' ? 'story-executing' : null);
1241
1491
  item.classList.add('detail-row');
1242
1492
  const status = story.passes ? 'completed' : 'pending';
1243
- item.appendChild(el('span', 'item-icon icon-' + status, story.passes ? '✓' : '○'));
1493
+ item.appendChild(statusIcon(status, story.passes ? '✓' : '○'));
1244
1494
  const main = el('div', 'item-main');
1245
1495
  const title = el('div', 'item-title');
1246
1496
  title.appendChild(el('span', 'story-id', story.id));
@@ -1249,10 +1499,15 @@
1249
1499
 
1250
1500
  // status/dependencies vêm com default no schema (backlog/[]), mas
1251
1501
  // session.json gravado antes de #29 pode chegar sem eles.
1252
- const storyStatus = story.status !== null && story.status !== undefined ? story.status : 'backlog';
1502
+ const storyStatus =
1503
+ story.status !== null && story.status !== undefined ? story.status : 'backlog';
1253
1504
  const meta = el('div', 'story-meta');
1254
1505
  meta.appendChild(
1255
- el('span', 'badge story-status-' + storyStatus, STORY_STATUS_LABELS[storyStatus] || storyStatus),
1506
+ el(
1507
+ 'span',
1508
+ 'badge story-status-' + storyStatus,
1509
+ STORY_STATUS_LABELS[storyStatus] || storyStatus,
1510
+ ),
1256
1511
  );
1257
1512
  const dependencies = story.dependencies || [];
1258
1513
  if (dependencies.length > 0) {
@@ -1294,9 +1549,7 @@
1294
1549
  card.dataset.storyId = story.id;
1295
1550
 
1296
1551
  const head = el('span', 'kanban-card-head');
1297
- head.appendChild(
1298
- el('span', 'item-icon icon-' + (story.passes ? 'completed' : 'pending'), story.passes ? '✓' : '○'),
1299
- );
1552
+ head.appendChild(statusIcon(story.passes ? 'completed' : 'pending', story.passes ? '✓' : '○'));
1300
1553
  head.appendChild(el('span', 'story-id', story.id));
1301
1554
  card.appendChild(head);
1302
1555
 
@@ -1378,16 +1631,30 @@
1378
1631
  item.appendChild(title);
1379
1632
  const grid = el('dl', 'now-grid execution-grid');
1380
1633
  nowRow(grid, 'Harness', execution.agent?.harness || '—');
1381
- nowRow(grid, 'Modelo', execution.agent?.model?.resolved || execution.agent?.model?.requested || '—');
1634
+ nowRow(
1635
+ grid,
1636
+ 'Modelo',
1637
+ execution.agent?.model?.resolved || execution.agent?.model?.requested || '—',
1638
+ );
1382
1639
  nowRow(grid, 'Início', formatClock(execution.startedAt));
1383
- nowRow(grid, 'Fim', execution.finishedAt ? formatClock(execution.finishedAt) : 'em andamento');
1384
- nowRow(grid, 'Duração', metric(execution.durationMs) !== null ? formatDuration(execution.durationMs / 1000) : '—');
1640
+ nowRow(
1641
+ grid,
1642
+ 'Fim',
1643
+ execution.finishedAt ? formatClock(execution.finishedAt) : 'em andamento',
1644
+ );
1645
+ nowRow(
1646
+ grid,
1647
+ 'Duração',
1648
+ metric(execution.durationMs) !== null ? formatDuration(execution.durationMs / 1000) : '—',
1649
+ );
1385
1650
  nowRow(grid, 'Tokens', formatUsage(execution.usage || {}) || '—');
1386
1651
  nowRow(grid, 'Custo', executionCost(execution));
1387
- if (execution.correctionCycle) nowRow(grid, 'Correção', 'ciclo ' + execution.correctionCycle);
1652
+ if (execution.correctionCycle)
1653
+ nowRow(grid, 'Correção', 'ciclo ' + execution.correctionCycle);
1388
1654
  if (execution.verdict?.status) nowRow(grid, 'Veredito', execution.verdict.status);
1389
1655
  item.appendChild(grid);
1390
- if (execution.failure?.message) item.appendChild(el('p', 'item-error', execution.failure.message));
1656
+ if (execution.failure?.message)
1657
+ item.appendChild(el('p', 'item-error', execution.failure.message));
1391
1658
  timeline.appendChild(item);
1392
1659
  }
1393
1660
  body.appendChild(timeline);
@@ -1427,7 +1694,9 @@
1427
1694
  if (entries.length === 0) return;
1428
1695
  drawerSection('Diagnóstico global persistente', (body) => {
1429
1696
  const details = el('details', 'process-output');
1430
- details.appendChild(el('summary', null, entries.length + ' registro(s) em ~/.issue-flow/logs'));
1697
+ details.appendChild(
1698
+ el('summary', null, entries.length + ' registro(s) em ~/.issue-flow/logs'),
1699
+ );
1431
1700
  const output = el('pre', 'process-output-body');
1432
1701
  output.textContent = entries
1433
1702
  .slice(0, 200)
@@ -1444,7 +1713,8 @@
1444
1713
  const kind = state.selectedDetail.kind;
1445
1714
  const id = state.selectedDetail.id;
1446
1715
  const story = kind === 'story' ? getStoryById(snapshot, id) : null;
1447
- const phase = kind === 'phase' ? list(snapshot.phases).find((entry) => entry.name === id) : null;
1716
+ const phase =
1717
+ kind === 'phase' ? list(snapshot.phases).find((entry) => entry.name === id) : null;
1448
1718
  if ((kind === 'story' && story === null) || (kind === 'phase' && !phase)) {
1449
1719
  closeDrawer();
1450
1720
  return;
@@ -1457,9 +1727,15 @@
1457
1727
  const grid = el('dl', 'now-grid drawer-summary-grid');
1458
1728
  nowRow(grid, 'Início', phase.startedAt ? formatClock(phase.startedAt) : '—');
1459
1729
  nowRow(grid, 'Fim', phase.endedAt ? formatClock(phase.endedAt) : '—');
1460
- nowRow(grid, 'Duração', metric(phase.durationSeconds) !== null ? formatDuration(phase.durationSeconds) : '—');
1730
+ nowRow(
1731
+ grid,
1732
+ 'Duração',
1733
+ metric(phase.durationSeconds) !== null ? formatDuration(phase.durationSeconds) : '—',
1734
+ );
1461
1735
  nowRow(grid, 'Uso total', formatUsage(phase) || '—');
1462
- const configured = list(snapshot.configuration?.phases).find((entry) => entry.phase === phase.name);
1736
+ const configured = list(snapshot.configuration?.phases).find(
1737
+ (entry) => entry.phase === phase.name,
1738
+ );
1463
1739
  if (configured) {
1464
1740
  nowRow(grid, 'Harness efetivo', configured.provider.value || '—');
1465
1741
  nowRow(grid, 'Origem', configSourceLabel(configured.provider.source));
@@ -1499,7 +1775,8 @@
1499
1775
  return;
1500
1776
  }
1501
1777
  const items = el('ul', 'drawer-list');
1502
- for (const criterion of story.acceptanceCriteria) items.appendChild(el('li', null, criterion));
1778
+ for (const criterion of story.acceptanceCriteria)
1779
+ items.appendChild(el('li', null, criterion));
1503
1780
  body.appendChild(items);
1504
1781
  });
1505
1782
 
@@ -1613,9 +1890,7 @@
1613
1890
  function renderLogs(snapshot) {
1614
1891
  clear(els.logs);
1615
1892
  const filter = state.logFilter;
1616
- const entries = snapshot.logs.filter(
1617
- (entry) => filter === 'all' || entry.level === filter,
1618
- );
1893
+ const entries = snapshot.logs.filter((entry) => filter === 'all' || entry.level === filter);
1619
1894
  if (entries.length === 0) {
1620
1895
  els.logs.appendChild(el('p', 'empty', 'Nenhum log para exibir.'));
1621
1896
  return;
@@ -1659,9 +1934,20 @@
1659
1934
  case 'agent:activity':
1660
1935
  return 'Atividade recebida de ' + event.provider;
1661
1936
  case 'agent:result':
1662
- return event.provider + (event.success ? ' concluiu a tentativa' : ' falhou: ' + (event.failureKind || 'desconhecida'));
1937
+ return (
1938
+ event.provider +
1939
+ (event.success
1940
+ ? ' concluiu a tentativa'
1941
+ : ' falhou: ' + (event.failureKind || 'desconhecida'))
1942
+ );
1663
1943
  case 'failover':
1664
- return 'Failover de ' + event.from + ' para ' + event.to + (event.reason ? ': ' + event.reason : '');
1944
+ return (
1945
+ 'Failover de ' +
1946
+ event.from +
1947
+ ' para ' +
1948
+ event.to +
1949
+ (event.reason ? ': ' + event.reason : '')
1950
+ );
1665
1951
  case 'correction:cycle':
1666
1952
  return 'Ciclo de correção ' + event.cycle + '/' + event.maxCycles;
1667
1953
  case 'log':
@@ -1696,7 +1982,7 @@
1696
1982
 
1697
1983
  function renderMeta(snapshot) {
1698
1984
  const parts = [];
1699
- if (snapshot.sessionId) parts.push('sessão ' + snapshot.sessionId);
1985
+ if (snapshot.sessionId) parts.push('execução ' + snapshot.sessionId);
1700
1986
  if (snapshot.updatedAt) parts.push('atualizado ' + formatClock(snapshot.updatedAt));
1701
1987
  parts.push('somente leitura');
1702
1988
  els.sessionMeta.textContent = parts.join(' · ');
@@ -1745,6 +2031,8 @@
1745
2031
  }
1746
2032
 
1747
2033
  async function init() {
2034
+ const tablist = document.querySelector('[role="tablist"]');
2035
+ if (tablist) tablist.addEventListener('keydown', onTabListKeydown);
1748
2036
  for (const tab of els.tabs) {
1749
2037
  tab.addEventListener('click', () => setActiveTab(tab.id));
1750
2038
  }
@@ -1791,7 +2079,10 @@
1791
2079
  serverInstanceChanged(healthRes);
1792
2080
  const health = await healthRes.json();
1793
2081
  renderMonitorVersion(health.version);
1794
- state.configWritable = list(health.capabilities).includes('config:agent:write');
2082
+ const capabilities = list(health.capabilities);
2083
+ state.configWritable =
2084
+ capabilities.includes('config:agent:write') &&
2085
+ capabilities.includes('config:routing:write');
1795
2086
  const suggested = Number(health.refreshSeconds);
1796
2087
  if (stored === null && Number.isFinite(suggested) && suggested > 0) {
1797
2088
  state.refreshSeconds = suggested;