issue-flow 0.17.0 → 0.19.1

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 (239) hide show
  1. package/README.md +34 -100
  2. package/dist/agent-EWKA7357.js +32 -0
  3. package/dist/{analyze-6M3KEOX7.js → analyze-Z7ODC27F.js} +42 -41
  4. package/dist/analyze-Z7ODC27F.js.map +1 -0
  5. package/dist/{apply-ADB3ZK7W.js → apply-VNSWQS6O.js} +22 -24
  6. package/dist/apply-VNSWQS6O.js.map +1 -0
  7. package/dist/artifacts-AMJQJFFV.js +82 -0
  8. package/dist/artifacts-AMJQJFFV.js.map +1 -0
  9. package/dist/{bench-FM7I7EIV.js → bench-ZJEIACUK.js} +42 -42
  10. package/dist/{chunk-RYOUZ6EK.js → chunk-23VHVVKW.js} +3 -3
  11. package/dist/chunk-2EYLOS5I.js +99 -0
  12. package/dist/chunk-2EYLOS5I.js.map +1 -0
  13. package/dist/{chunk-ANI2NTR4.js → chunk-2EZW4X57.js} +28 -25
  14. package/dist/chunk-2EZW4X57.js.map +1 -0
  15. package/dist/{chunk-E6KXCV3T.js → chunk-2Q6PG3MT.js} +15 -15
  16. package/dist/chunk-2Q6PG3MT.js.map +1 -0
  17. package/dist/{chunk-C5TL7T4V.js → chunk-2YM2VH6R.js} +2 -2
  18. package/dist/{chunk-LPWPFIIB.js → chunk-2YYP7AE6.js} +14 -16
  19. package/dist/{chunk-LPWPFIIB.js.map → chunk-2YYP7AE6.js.map} +1 -1
  20. package/dist/chunk-3JD25HHJ.js +278 -0
  21. package/dist/chunk-3JD25HHJ.js.map +1 -0
  22. package/dist/{chunk-ISZ54SVE.js → chunk-46S7APLQ.js} +41 -17
  23. package/dist/chunk-46S7APLQ.js.map +1 -0
  24. package/dist/chunk-6AOZCWN2.js +21 -0
  25. package/dist/chunk-6AOZCWN2.js.map +1 -0
  26. package/dist/{chunk-QPGEFLMY.js → chunk-7CMVL4X6.js} +7 -7
  27. package/dist/chunk-7MJGYPHF.js +1571 -0
  28. package/dist/chunk-7MJGYPHF.js.map +1 -0
  29. package/dist/chunk-7ZZUSMBJ.js +35 -0
  30. package/dist/chunk-7ZZUSMBJ.js.map +1 -0
  31. package/dist/{chunk-DUS5ZNKL.js → chunk-DIMDAYZS.js} +7 -6
  32. package/dist/{chunk-DGRTJD5R.js → chunk-DXH4TQRB.js} +7 -7
  33. package/dist/{chunk-CRGIQFUQ.js → chunk-E2H3JV2E.js} +3 -1
  34. package/dist/chunk-E2H3JV2E.js.map +1 -0
  35. package/dist/{chunk-YUNBOUQH.js → chunk-ERAG5IQB.js} +8 -8
  36. package/dist/{chunk-D6ZBGJ3L.js → chunk-F2NFJ27Z.js} +130 -36
  37. package/dist/chunk-F2NFJ27Z.js.map +1 -0
  38. package/dist/{chunk-SOM4QA6C.js → chunk-GD7JZMSJ.js} +93 -18
  39. package/dist/chunk-GD7JZMSJ.js.map +1 -0
  40. package/dist/{chunk-YII4U7ZD.js → chunk-HEHHQU4G.js} +70 -81
  41. package/dist/chunk-HEHHQU4G.js.map +1 -0
  42. package/dist/{chunk-KZ5EY5XX.js → chunk-HHXWAWU6.js} +159 -114
  43. package/dist/chunk-HHXWAWU6.js.map +1 -0
  44. package/dist/{chunk-SGTTR4XW.js → chunk-HWWB22HC.js} +152 -100
  45. package/dist/chunk-HWWB22HC.js.map +1 -0
  46. package/dist/chunk-IMB6FA2F.js +673 -0
  47. package/dist/chunk-IMB6FA2F.js.map +1 -0
  48. package/dist/{chunk-6YQT3SSM.js → chunk-IQYXXFRC.js} +22 -6
  49. package/dist/chunk-IQYXXFRC.js.map +1 -0
  50. package/dist/chunk-J6VJ5M2U.js +1201 -0
  51. package/dist/chunk-J6VJ5M2U.js.map +1 -0
  52. package/dist/{chunk-B3TSS7BT.js → chunk-KDQRNVBZ.js} +3 -3
  53. package/dist/{chunk-A4H7P7T7.js → chunk-LRWIP7QS.js} +41 -26
  54. package/dist/chunk-LRWIP7QS.js.map +1 -0
  55. package/dist/{chunk-DV7HUC4Z.js → chunk-LYC57OXF.js} +20 -117
  56. package/dist/chunk-LYC57OXF.js.map +1 -0
  57. package/dist/{chunk-HAKDLR6N.js → chunk-MAJ6QDRL.js} +65 -78
  58. package/dist/chunk-MAJ6QDRL.js.map +1 -0
  59. package/dist/{chunk-HJBF4WD2.js → chunk-MHA7Q3KU.js} +288 -108
  60. package/dist/chunk-MHA7Q3KU.js.map +1 -0
  61. package/dist/{chunk-UK4E2FUA.js → chunk-MQTVMB7W.js} +103 -76
  62. package/dist/chunk-MQTVMB7W.js.map +1 -0
  63. package/dist/chunk-NP47HRX2.js +136 -0
  64. package/dist/chunk-NP47HRX2.js.map +1 -0
  65. package/dist/{chunk-MI5TREN3.js → chunk-NYDAHE3Y.js} +52 -46
  66. package/dist/chunk-NYDAHE3Y.js.map +1 -0
  67. package/dist/{chunk-UWDCIFTP.js → chunk-P7ZYOC23.js} +41 -57
  68. package/dist/chunk-P7ZYOC23.js.map +1 -0
  69. package/dist/{chunk-3R27MJ5R.js → chunk-PFWHHYEO.js} +3 -3
  70. package/dist/{chunk-2SUFAE6X.js → chunk-PHPAK2L6.js} +44 -33
  71. package/dist/{chunk-2SUFAE6X.js.map → chunk-PHPAK2L6.js.map} +1 -1
  72. package/dist/{chunk-PPZ3K4YM.js → chunk-PRGPSNGA.js} +2 -2
  73. package/dist/{chunk-32JSYXEK.js → chunk-RGFMQFX7.js} +9 -23
  74. package/dist/{chunk-32JSYXEK.js.map → chunk-RGFMQFX7.js.map} +1 -1
  75. package/dist/{chunk-YVPZUN6X.js → chunk-TJVX42KY.js} +335 -38
  76. package/dist/chunk-TJVX42KY.js.map +1 -0
  77. package/dist/{chunk-RCZJQUTC.js → chunk-UDRAD7MF.js} +60 -27
  78. package/dist/chunk-UDRAD7MF.js.map +1 -0
  79. package/dist/{chunk-HY5HLFDU.js → chunk-UHDXXLYH.js} +148 -135
  80. package/dist/chunk-UHDXXLYH.js.map +1 -0
  81. package/dist/{chunk-RNFUVKC3.js → chunk-UPAAZU4W.js} +30 -13
  82. package/dist/chunk-UPAAZU4W.js.map +1 -0
  83. package/dist/{chunk-L6HSKTQE.js → chunk-W7LZLJAF.js} +4 -4
  84. package/dist/{chunk-HLAPOUEM.js → chunk-WB2FVIUS.js} +44 -12
  85. package/dist/chunk-WB2FVIUS.js.map +1 -0
  86. package/dist/{chunk-VE3IYNDN.js → chunk-WPHC6JBR.js} +9 -9
  87. package/dist/chunk-WPHC6JBR.js.map +1 -0
  88. package/dist/{chunk-KZXQWC7U.js → chunk-XHCP6ZNO.js} +201 -64
  89. package/dist/chunk-XHCP6ZNO.js.map +1 -0
  90. package/dist/{chunk-7F7LGMVO.js → chunk-XKR3VD2Z.js} +12 -12
  91. package/dist/{chunk-7F7LGMVO.js.map → chunk-XKR3VD2Z.js.map} +1 -1
  92. package/dist/{chunk-ICM463EO.js → chunk-XNK5UN2D.js} +5 -649
  93. package/dist/chunk-XNK5UN2D.js.map +1 -0
  94. package/dist/{chunk-Z5O2XBWA.js → chunk-XQLNZW7I.js} +40 -30
  95. package/dist/chunk-XQLNZW7I.js.map +1 -0
  96. package/dist/{chunk-QMIQR7EE.js → chunk-ZAYATXJD.js} +32 -316
  97. package/dist/chunk-ZAYATXJD.js.map +1 -0
  98. package/dist/cli.js +128 -60
  99. package/dist/cli.js.map +1 -1
  100. package/dist/compat-YPDZB34U.js +24 -0
  101. package/dist/{config-KGZUPAUP.js → config-56B4OHZE.js} +20 -17
  102. package/dist/{contract-JH2WBIO6.js → contract-HWLFFL3F.js} +2 -2
  103. package/dist/{conventions-RYN7OXJN.js → conventions-DW2CM5I3.js} +20 -16
  104. package/dist/{conventions-RYN7OXJN.js.map → conventions-DW2CM5I3.js.map} +1 -1
  105. package/dist/db-5NU77PJA.js +264 -0
  106. package/dist/db-5NU77PJA.js.map +1 -0
  107. package/dist/diagnostics-UCTS2VUF.js +25 -0
  108. package/dist/execute-R7AVOLNG.js +38 -0
  109. package/dist/{generate-CX6KNHZS.js → generate-O7GUXLN4.js} +46 -43
  110. package/dist/generate-O7GUXLN4.js.map +1 -0
  111. package/dist/{git-A2IP32PQ.js → git-LCG45YXD.js} +2 -3
  112. package/dist/history-6GSURU3H.js +101 -0
  113. package/dist/history-6GSURU3H.js.map +1 -0
  114. package/dist/init-DD6DBRC5.js +28 -0
  115. package/dist/{operations-CAKIT2BO.js → operations-HU5ZFEBA.js} +78 -52
  116. package/dist/operations-HU5ZFEBA.js.map +1 -0
  117. package/dist/{permissions-G4XCAESF.js → permissions-NNNSRC4P.js} +5 -6
  118. package/dist/plan-XR6B7EM3.js +38 -0
  119. package/dist/{policy-WX6O2JID.js → policy-RFJOJQL6.js} +17 -15
  120. package/dist/{policy-WX6O2JID.js.map → policy-RFJOJQL6.js.map} +1 -1
  121. package/dist/pr-OSCNJCOO.js +42 -0
  122. package/dist/pr-review-IKIYHFBO.js +33 -0
  123. package/dist/prd-HFDZJJQR.js +37 -0
  124. package/dist/{ps-76L6ZAZL.js → ps-AKRHGPSR.js} +26 -15
  125. package/dist/ps-AKRHGPSR.js.map +1 -0
  126. package/dist/{recorder-N7U66HCO.js → recorder-T7426CKV.js} +11 -7
  127. package/dist/registry-DWRKD65A.js +17 -0
  128. package/dist/repository-TABBRW2C.js +80 -0
  129. package/dist/resume-GDXKK5XP.js +278 -0
  130. package/dist/resume-GDXKK5XP.js.map +1 -0
  131. package/dist/review-JJHMQA2O.js +38 -0
  132. package/dist/routing-FPNUVE7T.js +35 -0
  133. package/dist/run-RLCURLLL.js +61 -0
  134. package/dist/{session-publisher-KAJQRHNZ.js → session-publisher-Q5TNEZPX.js} +3 -3
  135. package/dist/session-publisher-Q5TNEZPX.js.map +1 -0
  136. package/dist/{state-manager-UPHO2EA2.js → state-manager-IGNTD4AB.js} +9 -4
  137. package/dist/state-manager-IGNTD4AB.js.map +1 -0
  138. package/dist/{usage-U7QNGCPK.js → usage-XW27AL5K.js} +69 -63
  139. package/dist/usage-XW27AL5K.js.map +1 -0
  140. package/dist/web-NCDBVZ5Y.js +205 -0
  141. package/dist/web-NCDBVZ5Y.js.map +1 -0
  142. package/package.json +20 -7
  143. package/prompts/analyze.md +16 -19
  144. package/prompts/execute.md +73 -144
  145. package/prompts/generate.md +23 -10
  146. package/prompts/plan.md +32 -60
  147. package/prompts/pr-review.md +58 -40
  148. package/prompts/pr.md +50 -16
  149. package/prompts/prd.md +26 -19
  150. package/prompts/review.md +41 -18
  151. package/web/AGENTS.md +115 -7
  152. package/web/public/app.css +393 -245
  153. package/web/public/app.js +212 -143
  154. package/web/public/index.html +106 -78
  155. package/dist/agent-TB6W7PT7.js +0 -30
  156. package/dist/analyze-6M3KEOX7.js.map +0 -1
  157. package/dist/apply-ADB3ZK7W.js.map +0 -1
  158. package/dist/chunk-6YQT3SSM.js.map +0 -1
  159. package/dist/chunk-A4H7P7T7.js.map +0 -1
  160. package/dist/chunk-ANI2NTR4.js.map +0 -1
  161. package/dist/chunk-CRGIQFUQ.js.map +0 -1
  162. package/dist/chunk-D6ZBGJ3L.js.map +0 -1
  163. package/dist/chunk-DV7HUC4Z.js.map +0 -1
  164. package/dist/chunk-E6KXCV3T.js.map +0 -1
  165. package/dist/chunk-HAKDLR6N.js.map +0 -1
  166. package/dist/chunk-HJBF4WD2.js.map +0 -1
  167. package/dist/chunk-HLAPOUEM.js.map +0 -1
  168. package/dist/chunk-HY5HLFDU.js.map +0 -1
  169. package/dist/chunk-ICM463EO.js.map +0 -1
  170. package/dist/chunk-ISZ54SVE.js.map +0 -1
  171. package/dist/chunk-KZ5EY5XX.js.map +0 -1
  172. package/dist/chunk-KZXQWC7U.js.map +0 -1
  173. package/dist/chunk-MI5TREN3.js.map +0 -1
  174. package/dist/chunk-QMIQR7EE.js.map +0 -1
  175. package/dist/chunk-RCZJQUTC.js.map +0 -1
  176. package/dist/chunk-RNFUVKC3.js.map +0 -1
  177. package/dist/chunk-SGTTR4XW.js.map +0 -1
  178. package/dist/chunk-SOM4QA6C.js.map +0 -1
  179. package/dist/chunk-UK4E2FUA.js.map +0 -1
  180. package/dist/chunk-UWDCIFTP.js.map +0 -1
  181. package/dist/chunk-VE3IYNDN.js.map +0 -1
  182. package/dist/chunk-WCCZJL7W.js +0 -141
  183. package/dist/chunk-WCCZJL7W.js.map +0 -1
  184. package/dist/chunk-XEYH6ZZI.js +0 -289
  185. package/dist/chunk-XEYH6ZZI.js.map +0 -1
  186. package/dist/chunk-XZUEFTUP.js +0 -243
  187. package/dist/chunk-XZUEFTUP.js.map +0 -1
  188. package/dist/chunk-YII4U7ZD.js.map +0 -1
  189. package/dist/chunk-YVPZUN6X.js.map +0 -1
  190. package/dist/chunk-Z5O2XBWA.js.map +0 -1
  191. package/dist/diagnostics-HTCWF7F4.js +0 -23
  192. package/dist/execute-QL3ZQIAP.js +0 -37
  193. package/dist/generate-CX6KNHZS.js.map +0 -1
  194. package/dist/init-PDVQDRS6.js +0 -24
  195. package/dist/operations-CAKIT2BO.js.map +0 -1
  196. package/dist/plan-PRORGS3L.js +0 -34
  197. package/dist/pr-27RMQI5F.js +0 -39
  198. package/dist/pr-review-VNESMAMT.js +0 -32
  199. package/dist/prd-ZNC4T5BS.js +0 -33
  200. package/dist/ps-76L6ZAZL.js.map +0 -1
  201. package/dist/registry-XLHHYLTR.js +0 -15
  202. package/dist/resume-VLWIE264.js +0 -244
  203. package/dist/resume-VLWIE264.js.map +0 -1
  204. package/dist/review-7CREJDKB.js +0 -35
  205. package/dist/routing-VCDYHSXA.js +0 -33
  206. package/dist/run-3MPORLF5.js +0 -56
  207. package/dist/usage-U7QNGCPK.js.map +0 -1
  208. package/dist/web-HUNDHCJ4.js +0 -172
  209. package/dist/web-HUNDHCJ4.js.map +0 -1
  210. /package/dist/{agent-TB6W7PT7.js.map → agent-EWKA7357.js.map} +0 -0
  211. /package/dist/{bench-FM7I7EIV.js.map → bench-ZJEIACUK.js.map} +0 -0
  212. /package/dist/{chunk-RYOUZ6EK.js.map → chunk-23VHVVKW.js.map} +0 -0
  213. /package/dist/{chunk-C5TL7T4V.js.map → chunk-2YM2VH6R.js.map} +0 -0
  214. /package/dist/{chunk-QPGEFLMY.js.map → chunk-7CMVL4X6.js.map} +0 -0
  215. /package/dist/{chunk-DUS5ZNKL.js.map → chunk-DIMDAYZS.js.map} +0 -0
  216. /package/dist/{chunk-DGRTJD5R.js.map → chunk-DXH4TQRB.js.map} +0 -0
  217. /package/dist/{chunk-YUNBOUQH.js.map → chunk-ERAG5IQB.js.map} +0 -0
  218. /package/dist/{chunk-B3TSS7BT.js.map → chunk-KDQRNVBZ.js.map} +0 -0
  219. /package/dist/{chunk-3R27MJ5R.js.map → chunk-PFWHHYEO.js.map} +0 -0
  220. /package/dist/{chunk-PPZ3K4YM.js.map → chunk-PRGPSNGA.js.map} +0 -0
  221. /package/dist/{chunk-L6HSKTQE.js.map → chunk-W7LZLJAF.js.map} +0 -0
  222. /package/dist/{config-KGZUPAUP.js.map → compat-YPDZB34U.js.map} +0 -0
  223. /package/dist/{contract-JH2WBIO6.js.map → config-56B4OHZE.js.map} +0 -0
  224. /package/dist/{diagnostics-HTCWF7F4.js.map → contract-HWLFFL3F.js.map} +0 -0
  225. /package/dist/{execute-QL3ZQIAP.js.map → diagnostics-UCTS2VUF.js.map} +0 -0
  226. /package/dist/{git-A2IP32PQ.js.map → execute-R7AVOLNG.js.map} +0 -0
  227. /package/dist/{init-PDVQDRS6.js.map → git-LCG45YXD.js.map} +0 -0
  228. /package/dist/{permissions-G4XCAESF.js.map → init-DD6DBRC5.js.map} +0 -0
  229. /package/dist/{plan-PRORGS3L.js.map → permissions-NNNSRC4P.js.map} +0 -0
  230. /package/dist/{pr-27RMQI5F.js.map → plan-XR6B7EM3.js.map} +0 -0
  231. /package/dist/{pr-review-VNESMAMT.js.map → pr-OSCNJCOO.js.map} +0 -0
  232. /package/dist/{prd-ZNC4T5BS.js.map → pr-review-IKIYHFBO.js.map} +0 -0
  233. /package/dist/{recorder-N7U66HCO.js.map → prd-HFDZJJQR.js.map} +0 -0
  234. /package/dist/{registry-XLHHYLTR.js.map → recorder-T7426CKV.js.map} +0 -0
  235. /package/dist/{review-7CREJDKB.js.map → registry-DWRKD65A.js.map} +0 -0
  236. /package/dist/{routing-VCDYHSXA.js.map → repository-TABBRW2C.js.map} +0 -0
  237. /package/dist/{run-3MPORLF5.js.map → review-JJHMQA2O.js.map} +0 -0
  238. /package/dist/{session-publisher-KAJQRHNZ.js.map → routing-FPNUVE7T.js.map} +0 -0
  239. /package/dist/{state-manager-UPHO2EA2.js.map → run-RLCURLLL.js.map} +0 -0
package/web/public/app.js CHANGED
@@ -24,6 +24,16 @@
24
24
  failed: 'falhou',
25
25
  };
26
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
+
27
37
  const PHASE_ICONS = {
28
38
  pending: '○',
29
39
  running: '●',
@@ -69,6 +79,7 @@
69
79
  dashboardMeta: document.getElementById('dashboard-meta'),
70
80
  backToDashboard: document.getElementById('back-to-dashboard'),
71
81
  issueLink: document.getElementById('issue-link'),
82
+ issueHeadline: document.getElementById('issue-headline'),
72
83
  branchLine: document.getElementById('branch-line'),
73
84
  statusBadge: document.getElementById('status-badge'),
74
85
  elapsed: document.getElementById('elapsed'),
@@ -89,6 +100,7 @@
89
100
  configuration: document.getElementById('configuration'),
90
101
  appVersion: document.getElementById('app-version'),
91
102
  appVersionDashboard: document.getElementById('app-version-dashboard'),
103
+ dashboardSummary: document.getElementById('dashboard-summary'),
92
104
  phases: document.getElementById('phases'),
93
105
  nextSteps: document.getElementById('next-steps'),
94
106
  stories: document.getElementById('stories'),
@@ -155,6 +167,21 @@
155
167
  return node;
156
168
  }
157
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
+
158
185
  function link(href, text, className) {
159
186
  const node = el('a', className, text);
160
187
  node.href = href;
@@ -176,17 +203,17 @@
176
203
  function formatDuration(totalSeconds) {
177
204
  if (totalSeconds === null || totalSeconds === undefined || totalSeconds < 0) return '—';
178
205
  const seconds = Math.round(totalSeconds);
179
- if (seconds < 60) return seconds + 's';
206
+ if (seconds < 60) return `${seconds}s`;
180
207
  const minutes = Math.floor(seconds / 60);
181
- if (minutes < 60) return minutes + 'min ' + String(seconds % 60).padStart(2, '0') + 's';
208
+ if (minutes < 60) return `${minutes}min ${String(seconds % 60).padStart(2, '0')}s`;
182
209
  const hours = Math.floor(minutes / 60);
183
- return hours + 'h ' + String(minutes % 60).padStart(2, '0') + 'min';
210
+ return `${hours}h ${String(minutes % 60).padStart(2, '0')}min`;
184
211
  }
185
212
 
186
213
  function formatAgo(iso) {
187
214
  const ms = parseIso(iso);
188
215
  if (ms === null) return '';
189
- return '' + formatDuration((Date.now() - ms) / 1000);
216
+ return `${formatDuration((Date.now() - ms) / 1000)}`;
190
217
  }
191
218
 
192
219
  function formatClock(iso) {
@@ -206,8 +233,8 @@
206
233
  // 1523 → 1.5k, 2400000 → 2.4M (mesma regra do resumo no terminal).
207
234
  function compactTokens(value) {
208
235
  const abs = Math.abs(value);
209
- if (abs >= 1000000) return (value / 1000000).toFixed(1) + 'M';
210
- if (abs >= 1000) return (value / 1000).toFixed(1) + 'k';
236
+ if (abs >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
237
+ if (abs >= 1000) return `${(value / 1000).toFixed(1)}k`;
211
238
  return String(Math.round(value));
212
239
  }
213
240
 
@@ -226,14 +253,14 @@
226
253
  const input = metric(usage.inputTokens);
227
254
  const output = metric(usage.outputTokens);
228
255
  const io = [];
229
- if (input !== null) io.push(compactTokens(input) + ' in');
230
- if (output !== null) io.push(compactTokens(output) + ' out');
256
+ if (input !== null) io.push(`${compactTokens(input)} in`);
257
+ if (output !== null) io.push(`${compactTokens(output)} out`);
231
258
  if (io.length > 0) segments.push(io.join(' / '));
232
259
 
233
260
  const cacheRead = metric(usage.cacheReadTokens);
234
261
  const cacheCreation = metric(usage.cacheCreationTokens);
235
262
  if (cacheRead !== null || cacheCreation !== null) {
236
- segments.push(compactTokens((cacheRead || 0) + (cacheCreation || 0)) + ' cache');
263
+ segments.push(`${compactTokens((cacheRead || 0) + (cacheCreation || 0))} cache`);
237
264
  }
238
265
 
239
266
  const cost = metric(usage.costUsd);
@@ -262,7 +289,7 @@
262
289
 
263
290
  // URL do repositório derivada da URL da issue (…/issues/N → raiz do repo).
264
291
  function repoUrl(snapshot) {
265
- const url = snapshot.issue && snapshot.issue.url;
292
+ const url = snapshot.issue?.url;
266
293
  if (!url) return null;
267
294
  const match = url.match(/^(https?:\/\/\S+?)\/issues\/\d+\/?$/);
268
295
  return match ? match[1] : null;
@@ -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?.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 ------------------------------------------------------------------
@@ -340,7 +386,7 @@
340
386
  let raw = null;
341
387
  try {
342
388
  raw = window.localStorage.getItem(THEME_STORAGE_KEY);
343
- } catch (err) {
389
+ } catch (_err) {
344
390
  // localStorage indisponível (ex.: bloqueado) — segue no modo sistema.
345
391
  }
346
392
  return raw === 'light' || raw === 'dark' ? raw : 'system';
@@ -349,7 +395,7 @@
349
395
  function storeTheme(value) {
350
396
  try {
351
397
  window.localStorage.setItem(THEME_STORAGE_KEY, value);
352
- } catch (err) {
398
+ } catch (_err) {
353
399
  // Persistência é conveniência; falha é ignorada. Sem gravar, a escolha
354
400
  // continua valendo nesta aba — só não sobrevive ao reload.
355
401
  }
@@ -417,7 +463,7 @@
417
463
  let raw = null;
418
464
  try {
419
465
  raw = window.localStorage.getItem(STORAGE_KEY);
420
- } catch (err) {
466
+ } catch (_err) {
421
467
  // localStorage indisponível (ex.: bloqueado) — segue com o default.
422
468
  }
423
469
  if (raw === null) return null;
@@ -428,7 +474,7 @@
428
474
  function storeRefresh(value) {
429
475
  try {
430
476
  window.localStorage.setItem(STORAGE_KEY, String(value));
431
- } catch (err) {
477
+ } catch (_err) {
432
478
  // Persistência é conveniência; falha é ignorada.
433
479
  }
434
480
  }
@@ -442,7 +488,7 @@
442
488
  }
443
489
  clear(select);
444
490
  for (const value of values) {
445
- const option = el('option', null, value + 's');
491
+ const option = el('option', null, `${value}s`);
446
492
  option.value = String(value);
447
493
  select.appendChild(option);
448
494
  }
@@ -515,14 +561,14 @@
515
561
  }
516
562
 
517
563
  function statusUrlFor(session) {
518
- if (!session || !session.statusUrl) return 'api/status';
564
+ if (!session?.statusUrl) return 'api/status';
519
565
  // statusUrl vem com barra inicial (/api/status?session=…); o fetch relativo
520
566
  // ao path do painel precisa da forma sem a barra absoluta do host.
521
567
  return session.statusUrl.replace(/^\//, '');
522
568
  }
523
569
 
524
570
  function eventsUrlFor(session) {
525
- if (!session || !session.eventsUrl) return null;
571
+ if (!session?.eventsUrl) return null;
526
572
  return session.eventsUrl.replace(/^\//, '');
527
573
  }
528
574
 
@@ -540,7 +586,7 @@
540
586
  // então é ela que explica o que está na tela. As duas aparecem juntas no card
541
587
  // de configuração quando divergem.
542
588
  function renderMonitorVersion(version) {
543
- const label = typeof version === 'string' && version !== '' ? 'v' + version : null;
589
+ const label = typeof version === 'string' && version !== '' ? `v${version}` : null;
544
590
  state.monitorVersion = label === null ? null : version;
545
591
  for (const node of [els.appVersion, els.appVersionDashboard]) {
546
592
  if (!node) continue;
@@ -597,7 +643,7 @@
597
643
  try {
598
644
  const sessionsRes = await fetch('api/sessions', { cache: 'no-store' });
599
645
  if (serverInstanceChanged(sessionsRes)) return;
600
- if (!sessionsRes.ok) throw new Error('HTTP ' + sessionsRes.status);
646
+ if (!sessionsRes.ok) throw new Error(`HTTP ${sessionsRes.status}`);
601
647
  const sessions = await sessionsRes.json();
602
648
  if (!Array.isArray(sessions)) throw new Error('sessions payload invalid');
603
649
 
@@ -655,12 +701,12 @@
655
701
  return;
656
702
  }
657
703
  if (res.status !== 304) {
658
- if (!res.ok) throw new Error('HTTP ' + res.status);
704
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
659
705
  state.etag = res.headers.get('ETag');
660
706
  state.snapshot = await res.json();
661
707
  render();
662
708
  }
663
- const configUrl = 'api/config?session=' + encodeURIComponent(state.detailSessionId || '');
709
+ const configUrl = `api/config?session=${encodeURIComponent(state.detailSessionId || '')}`;
664
710
  const configRes = await fetch(configUrl, { cache: 'no-store' });
665
711
  if (configRes.ok) {
666
712
  state.configData = await configRes.json();
@@ -668,11 +714,10 @@
668
714
  }
669
715
  if (state.eventsUrl) {
670
716
  const eventsRes = await fetch(state.eventsUrl, { cache: 'no-store' });
671
- if (!eventsRes.ok) throw new Error('HTTP ' + eventsRes.status);
717
+ if (!eventsRes.ok) throw new Error(`HTTP ${eventsRes.status}`);
672
718
  const entries = await eventsRes.json();
673
719
  state.events = Array.isArray(entries) ? entries : [];
674
- const diagnosticUrl =
675
- 'api/diagnostics?session=' + encodeURIComponent(state.detailSessionId || '');
720
+ const diagnosticUrl = `api/diagnostics?session=${encodeURIComponent(state.detailSessionId || '')}`;
676
721
  const diagnosticsRes = await fetch(diagnosticUrl, { cache: 'no-store' });
677
722
  if (diagnosticsRes.ok) {
678
723
  const diagnostics = await diagnosticsRes.json();
@@ -684,7 +729,7 @@
684
729
  setViewMode('detail');
685
730
  state.failures = 0;
686
731
  els.banner.hidden = true;
687
- } catch (err) {
732
+ } catch (_err) {
688
733
  state.failures += 1;
689
734
  els.banner.hidden = false;
690
735
  } finally {
@@ -701,6 +746,9 @@
701
746
  function renderEmptyDetail() {
702
747
  if (state.snapshot) return;
703
748
  document.title = 'issue-flow';
749
+ els.issueLink.hidden = true;
750
+ els.issueHeadline.textContent = 'Nenhuma execução ativa';
751
+ els.branchLine.textContent = '';
704
752
  els.statusBadge.textContent = STATUS_LABELS.idle;
705
753
  els.statusBadge.className = 'badge status-idle';
706
754
  els.elapsed.textContent = '—';
@@ -712,13 +760,32 @@
712
760
  if (!text) return '';
713
761
  const normalized = String(text).replace(/\s+/g, ' ').trim();
714
762
  if (normalized.length <= max) return normalized;
715
- return normalized.slice(0, max - 1).trimEnd() + '…';
763
+ return `${normalized.slice(0, max - 1).trimEnd()}…`;
764
+ }
765
+
766
+ // Linha secundária do header do dashboard: quantas execuções e em que
767
+ // estado. Substitui a legenda estática que só repetia o h1.
768
+ function renderDashboardSummary(sessions) {
769
+ const counts = {};
770
+ for (const session of sessions) {
771
+ const status = session.status || 'idle';
772
+ counts[status] = (counts[status] || 0) + 1;
773
+ }
774
+ const parts = [];
775
+ for (const status of SUMMARY_STATUS_ORDER) {
776
+ const count = counts[status];
777
+ if (!count) continue;
778
+ const label = SUMMARY_STATUS_LABELS[status];
779
+ parts.push(`${count} ${count === 1 ? label[0] : label[1]}`);
780
+ }
781
+ const total = sessions.length + (sessions.length === 1 ? ' execução' : ' execuções');
782
+ els.dashboardSummary.textContent = parts.length > 0 ? `${total} · ${parts.join(' · ')}` : total;
716
783
  }
717
784
 
718
785
  function renderDashboard(sessions) {
719
786
  const active = document.activeElement;
720
787
  const focusedId =
721
- active && active.dataset && els.dashboard.contains(active) && active.dataset.sessionId
788
+ active?.dataset && els.dashboard.contains(active) && active.dataset.sessionId
722
789
  ? active.dataset.sessionId
723
790
  : null;
724
791
 
@@ -727,10 +794,13 @@
727
794
  if (sessions.length === 0) {
728
795
  els.dashboard.appendChild(el('p', 'empty', 'Nenhuma execução ativa.'));
729
796
  els.dashboardMeta.textContent = '0 execuções';
797
+ els.dashboardSummary.textContent = 'Nenhuma execução ativa';
730
798
  document.title = 'issue-flow';
731
799
  return;
732
800
  }
733
801
 
802
+ renderDashboardSummary(sessions);
803
+
734
804
  for (const session of sessions) {
735
805
  // <button> só aceita phrasing content — mesmos spans do Kanban.
736
806
  const card = el('button', 'dashboard-card');
@@ -744,7 +814,7 @@
744
814
  'dashboard-project',
745
815
  session.repositoryName || 'Projeto desconhecido',
746
816
  );
747
- const badge = el('span', 'badge status-' + (session.status || 'idle'));
817
+ const badge = el('span', `badge status-${session.status || 'idle'}`);
748
818
  badge.textContent = STATUS_LABELS[session.status] || session.status || '—';
749
819
  head.appendChild(project);
750
820
  head.appendChild(badge);
@@ -752,7 +822,7 @@
752
822
 
753
823
  const titleRow = el('span', 'dashboard-title-row');
754
824
  if (session.issueNumber !== null && session.issueNumber !== undefined) {
755
- titleRow.appendChild(el('span', 'dashboard-issue', '#' + session.issueNumber));
825
+ titleRow.appendChild(el('span', 'dashboard-issue', `#${session.issueNumber}`));
756
826
  }
757
827
  titleRow.appendChild(el('span', 'dashboard-title', session.issueTitle || 'Sem título'));
758
828
  card.appendChild(titleRow);
@@ -761,9 +831,9 @@
761
831
  card.appendChild(el('span', 'dashboard-summary muted', summary || 'Sem descrição'));
762
832
 
763
833
  const meta = el('span', 'dashboard-meta-row');
764
- meta.appendChild(el('span', null, 'Fase: ' + (session.currentPhase || '—')));
834
+ meta.appendChild(el('span', null, `Fase: ${session.currentPhase || '—'}`));
765
835
  const percent = typeof session.progressPercent === 'number' ? session.progressPercent : 0;
766
- meta.appendChild(el('span', null, percent + '%'));
836
+ meta.appendChild(el('span', null, `${percent}%`));
767
837
  const elapsed =
768
838
  typeof session.elapsedSeconds === 'number'
769
839
  ? formatDuration(session.elapsedSeconds)
@@ -775,30 +845,33 @@
775
845
  // A card that only shows a percentage cannot tell a run that is
776
846
  // progressing from one that has been retrying for twenty minutes.
777
847
  if (typeof session.retries === 'number' && session.retries > 0) {
778
- meta.appendChild(el('span', null, session.retries + ' retry(s)'));
848
+ meta.appendChild(el('span', null, `${session.retries} retry(s)`));
779
849
  }
780
850
  if (typeof session.correctionCycle === 'number' && session.correctionCycle > 0) {
781
- meta.appendChild(el('span', null, 'correção ' + session.correctionCycle));
851
+ meta.appendChild(el('span', null, `correção ${session.correctionCycle}`));
782
852
  }
783
853
  if (session.updatedAt) {
784
854
  meta.appendChild(
785
- el('span', null, 'atividade ' + formatAgo(session.lastActivityAt || session.updatedAt)),
855
+ el('span', null, `atividade ${formatAgo(session.lastActivityAt || session.updatedAt)}`),
786
856
  );
787
857
  }
788
- if (session.provider) meta.appendChild(el('span', null, 'provider ' + session.provider));
858
+ if (session.provider) meta.appendChild(el('span', null, `provider ${session.provider}`));
789
859
  if (typeof session.attempt === 'number' && session.attempt > 0) {
790
- meta.appendChild(el('span', null, 'tentativa ' + session.attempt));
860
+ meta.appendChild(el('span', null, `tentativa ${session.attempt}`));
791
861
  }
792
862
  card.appendChild(meta);
793
863
 
794
864
  const progress = el('span', 'dashboard-progress');
795
865
  const bar = el('span', 'dashboard-progress-bar');
796
- bar.style.width = Math.max(0, Math.min(100, percent)) + '%';
866
+ bar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
797
867
  progress.appendChild(bar);
798
868
  card.appendChild(progress);
799
869
 
800
870
  if (session.status === 'running') {
801
- card.appendChild(el('span', 'dashboard-live-dot', 'ao vivo'));
871
+ const live = el('span', 'live-label');
872
+ live.appendChild(liveDot());
873
+ live.appendChild(document.createTextNode('ao vivo'));
874
+ card.appendChild(live);
802
875
  }
803
876
 
804
877
  card.addEventListener('click', () => {
@@ -810,11 +883,11 @@
810
883
 
811
884
  els.dashboardMeta.textContent =
812
885
  sessions.length + (sessions.length === 1 ? ' execução' : ' execuções');
813
- document.title = sessions.length + ' execuções · issue-flow';
886
+ document.title = `${sessions.length} execuções · issue-flow`;
814
887
 
815
888
  if (focusedId) {
816
889
  const card = els.dashboard.querySelector(
817
- '[data-session-id="' + focusedId.replace(/"/g, '') + '"]',
890
+ `[data-session-id="${focusedId.replace(/"/g, '')}"]`,
818
891
  );
819
892
  if (card) card.focus();
820
893
  }
@@ -848,24 +921,31 @@
848
921
  }
849
922
 
850
923
  function renderTitle(snapshot) {
851
- const issue = snapshot.issue.number !== null ? '#' + snapshot.issue.number : '';
924
+ const issue = snapshot.issue.number !== null ? `#${snapshot.issue.number}` : '';
852
925
  let prefix = '';
853
- if (snapshot.status === 'running') prefix = snapshot.progress.percent + '% · ';
926
+ if (snapshot.status === 'running') prefix = `${snapshot.progress.percent}% · `;
854
927
  else if (snapshot.status === 'completed') prefix = '✓ ';
855
928
  else if (snapshot.status === 'failed') prefix = '✗ ';
856
- document.title = (prefix + issue + ' · issue-flow').replace(/^ · /, '');
929
+ document.title = `${prefix + issue} · issue-flow`.replace(/^ · /, '');
857
930
  }
858
931
 
932
+ // O h1 é a execução — número, título, e ao lado status, tempo decorrido e
933
+ // branch. A marca do produto vive só no <title> do documento.
859
934
  function renderHeader(snapshot) {
860
- if (snapshot.issue.number !== null) {
935
+ const hasIssue = snapshot.issue.number !== null && snapshot.issue.number !== undefined;
936
+ if (hasIssue) {
861
937
  els.issueLink.hidden = false;
862
- els.issueLink.textContent = '#' + snapshot.issue.number;
938
+ els.issueLink.textContent = `#${snapshot.issue.number}`;
863
939
  if (snapshot.issue.url) els.issueLink.href = snapshot.issue.url;
864
940
  else els.issueLink.removeAttribute('href');
865
941
  } else {
866
942
  els.issueLink.hidden = true;
867
943
  }
868
944
 
945
+ // session.json antigo pode não ter título; sem issue vinculada, nem número.
946
+ els.issueHeadline.textContent =
947
+ snapshot.issue.title || (hasIssue ? 'Sem título' : 'Execução sem issue vinculada');
948
+
869
949
  const branch = snapshot.git.branch;
870
950
  const base = snapshot.git.baseBranch;
871
951
  const branchMode =
@@ -875,11 +955,11 @@
875
955
  ? 'criada pelo Issue Flow'
876
956
  : 'origem da branch não informada';
877
957
  els.branchLine.textContent = branch
878
- ? (base ? branch + ' ' + base : branch) + ' · ' + branchMode
958
+ ? `${base ? `${branch}${base}` : branch} · ${branchMode}`
879
959
  : '';
880
960
 
881
961
  els.statusBadge.textContent = STATUS_LABELS[snapshot.status] || snapshot.status;
882
- els.statusBadge.className = 'badge status-' + snapshot.status;
962
+ els.statusBadge.className = `badge status-${snapshot.status}`;
883
963
  }
884
964
 
885
965
  function renderAlerts(snapshot) {
@@ -892,8 +972,8 @@
892
972
  if (!any) return;
893
973
 
894
974
  const counts = [];
895
- if (errors.length > 0) counts.push(errors.length + ' erro(s)');
896
- if (warnings.length > 0) counts.push(warnings.length + ' aviso(s)');
975
+ if (errors.length > 0) counts.push(`${errors.length} erro(s)`);
976
+ if (warnings.length > 0) counts.push(`${warnings.length} aviso(s)`);
897
977
  if (counts.length > 0) {
898
978
  els.alertsBody.appendChild(el('p', 'alert-count', counts.join(' · ')));
899
979
  }
@@ -906,13 +986,13 @@
906
986
  }
907
987
  for (const log of errors.slice(-ALERT_PREVIEW)) {
908
988
  const entry = el('div', 'alert-entry level-error');
909
- entry.appendChild(el('span', 'mono', formatClock(log.at) + ' '));
989
+ entry.appendChild(el('span', 'mono', `${formatClock(log.at)} `));
910
990
  entry.appendChild(document.createTextNode(log.message));
911
991
  els.alertsBody.appendChild(entry);
912
992
  }
913
993
  for (const log of warnings.slice(-ALERT_PREVIEW)) {
914
994
  const entry = el('div', 'alert-entry level-warn');
915
- entry.appendChild(el('span', 'mono', formatClock(log.at) + ' '));
995
+ entry.appendChild(el('span', 'mono', `${formatClock(log.at)} `));
916
996
  entry.appendChild(document.createTextNode(log.message));
917
997
  els.alertsBody.appendChild(entry);
918
998
  }
@@ -925,23 +1005,13 @@
925
1005
  clear(els.issueSummary);
926
1006
  const issue = snapshot.issue || {};
927
1007
 
928
- const heading = el('p', 'issue-summary-title');
929
- heading.appendChild(
930
- el(
931
- 'span',
932
- 'mono',
933
- issue.number !== null && issue.number !== undefined ? '#' + issue.number : '—',
934
- ),
935
- );
936
- heading.appendChild(document.createTextNode(' ' + (issue.title || 'Sem título')));
937
- els.issueSummary.appendChild(heading);
938
-
1008
+ // Número e título saem no h1 do header (renderHeader); aqui fica só o que
1009
+ // o header não carrega — estado, labels e descrição.
939
1010
  const meta = el('div', 'issue-summary-meta');
940
1011
  const state = issue.state || null;
941
1012
  const stateClass =
942
1013
  state === 'open' ? 'state-open' : state === 'closed' ? 'state-closed' : 'state-unknown';
943
- meta.appendChild(el('span', 'badge ' + stateClass, state || 'estado desconhecido'));
944
- meta.appendChild(el('span', 'muted', 'Prioridade: Não definida'));
1014
+ meta.appendChild(el('span', `badge ${stateClass}`, state || 'estado desconhecido'));
945
1015
  els.issueSummary.appendChild(meta);
946
1016
 
947
1017
  const labels = issue.labels || [];
@@ -983,7 +1053,7 @@
983
1053
  function renderProgress(snapshot) {
984
1054
  const progress = snapshot.progress;
985
1055
  els.progressBar.value = progress.percent;
986
- els.progressPercent.textContent = progress.percent + '%';
1056
+ els.progressPercent.textContent = `${progress.percent}%`;
987
1057
  const counters =
988
1058
  'Fases ' +
989
1059
  progress.phasesCompleted +
@@ -994,7 +1064,7 @@
994
1064
  '/' +
995
1065
  progress.storiesTotal;
996
1066
  const totals = formatTotals(snapshot.metrics);
997
- els.progressCounters.textContent = totals ? counters + ' · ' + totals : counters;
1067
+ els.progressCounters.textContent = totals ? `${counters} · ${totals}` : counters;
998
1068
  }
999
1069
 
1000
1070
  function nowRow(grid, label, value) {
@@ -1008,16 +1078,16 @@
1008
1078
  const messages = {
1009
1079
  idle: 'Nenhuma execução em andamento.',
1010
1080
  completed: 'Execução concluída.',
1011
- failed: 'Execução falhou veja os erros acima.',
1081
+ failed: 'Execução falhou. Veja os erros acima.',
1012
1082
  };
1013
1083
  els.now.appendChild(el('p', 'empty', messages[snapshot.status] || '—'));
1014
1084
  return;
1015
1085
  }
1016
1086
 
1017
1087
  const grid = el('dl', 'now-grid');
1018
- const phase = el('dd');
1088
+ const phase = el('dd', 'now-phase');
1019
1089
  if (snapshot.currentPhase) {
1020
- phase.appendChild(el('span', 'pulse'));
1090
+ phase.appendChild(liveDot());
1021
1091
  phase.appendChild(document.createTextNode(snapshot.currentPhase));
1022
1092
  } else {
1023
1093
  phase.textContent = '—';
@@ -1027,7 +1097,7 @@
1027
1097
 
1028
1098
  const activity = snapshot.currentActivity;
1029
1099
  if (activity) {
1030
- if (activity.story) nowRow(grid, 'Story', activity.story);
1100
+ if (activity.story) nowRow(grid, 'User story', activity.story);
1031
1101
  if (activity.tool) nowRow(grid, 'Ferramenta', activity.tool);
1032
1102
  if (activity.detail) nowRow(grid, 'Detalhe', activity.detail);
1033
1103
  const since = el('dd', null, formatAgo(activity.since));
@@ -1052,7 +1122,7 @@
1052
1122
  resilience.cooldownUntil ? formatClock(resilience.cooldownUntil) : '—',
1053
1123
  );
1054
1124
  const activity = resilience.lastActivityAt
1055
- ? formatClock(resilience.lastActivityAt) + ' (' + formatAgo(resilience.lastActivityAt) + ')'
1125
+ ? `${formatClock(resilience.lastActivityAt)} (${formatAgo(resilience.lastActivityAt)})`
1056
1126
  : '—';
1057
1127
  nowRow(grid, 'Última atividade', activity);
1058
1128
  els.resilience.appendChild(grid);
@@ -1078,16 +1148,16 @@
1078
1148
  // estar em versões diferentes; este é o único lugar onde as duas aparecem
1079
1149
  // lado a lado, então a divergência é dita aqui.
1080
1150
  const env = snapshot.environment;
1081
- const runVersion = env && env.cliVersion ? env.cliVersion : null;
1151
+ const runVersion = env?.cliVersion ? env.cliVersion : null;
1082
1152
  const runtime = el('dl', 'now-grid');
1083
- const runtimeBits = [runVersion ? 'v' + runVersion : 'versão não registrada'];
1084
- if (env && env.node) runtimeBits.push(env.node);
1085
- if (env && env.platform) runtimeBits.push(env.platform);
1153
+ const runtimeBits = [runVersion ? `v${runVersion}` : 'versão não registrada'];
1154
+ if (env?.node) runtimeBits.push(env.node);
1155
+ if (env?.platform) runtimeBits.push(env.platform);
1086
1156
  nowRow(runtime, 'Issue Flow (execução)', runtimeBits.join(' · '));
1087
1157
  nowRow(
1088
1158
  runtime,
1089
1159
  'Monitor (este painel)',
1090
- state.monitorVersion ? 'v' + state.monitorVersion : '—',
1160
+ state.monitorVersion ? `v${state.monitorVersion}` : '—',
1091
1161
  );
1092
1162
  els.configuration.appendChild(runtime);
1093
1163
  if (runVersion !== null && state.monitorVersion && runVersion !== state.monitorVersion) {
@@ -1126,10 +1196,10 @@
1126
1196
  nowRow(summary, 'Precedência', list(config.precedence).join(' → '));
1127
1197
  els.configuration.appendChild(summary);
1128
1198
 
1129
- const liveRouting = state.configData && state.configData.routing;
1199
+ const liveRouting = state.configData?.routing;
1130
1200
  if (liveRouting) {
1131
1201
  const routingSummary = el('dl', 'now-grid config-routing-summary');
1132
- nowRow(routingSummary, 'Routing', liveRouting.mode + ' · perfil ' + liveRouting.profile);
1202
+ nowRow(routingSummary, 'Routing', `${liveRouting.mode} · perfil ${liveRouting.profile}`);
1133
1203
  nowRow(
1134
1204
  routingSummary,
1135
1205
  'Política',
@@ -1195,10 +1265,10 @@
1195
1265
  phaseButton.addEventListener('click', () => openDrawer('phase', phase.phase));
1196
1266
  row.appendChild(phaseButton);
1197
1267
  const provider = el('select');
1198
- provider.setAttribute('aria-label', 'Harness para ' + phase.phase);
1268
+ provider.setAttribute('aria-label', `Harness para ${phase.phase}`);
1199
1269
  fillProviderSelect(provider, phase.provider.value || 'claude');
1200
1270
  const model = el('select');
1201
- model.setAttribute('aria-label', 'Tier e modelo para ' + phase.phase);
1271
+ model.setAttribute('aria-label', `Tier e modelo para ${phase.phase}`);
1202
1272
  fillModelSelect(model, provider.value, phase.model.value || '');
1203
1273
  provider.addEventListener('change', () => fillModelSelect(model, provider.value, ''));
1204
1274
  const save = el('button', 'config-save', 'Salvar');
@@ -1294,12 +1364,19 @@
1294
1364
  }
1295
1365
 
1296
1366
  function catalog() {
1297
- return list(state.configData && state.configData.catalog);
1367
+ return list(state.configData?.catalog);
1298
1368
  }
1299
1369
 
1300
1370
  function fillProviderSelect(select, selected) {
1301
1371
  clear(select);
1302
- const entries = catalog().filter((entry) => entry.installed && entry.authenticated);
1372
+ const entries = catalog().filter((entry) => {
1373
+ if (!entry.installed) return false;
1374
+ if (entry.state === 'unavailable') return false;
1375
+ if (entry.authentication === 'failed') return false;
1376
+ return (
1377
+ entry.authenticated !== false || entry.state === 'conditional' || entry.state === 'ready'
1378
+ );
1379
+ });
1303
1380
  const providers = entries.length
1304
1381
  ? entries.map((entry) => entry.provider)
1305
1382
  : ['claude', 'codex', 'cursor', 'antigravity'];
@@ -1318,9 +1395,9 @@
1318
1395
  defaultOption.value = '';
1319
1396
  select.appendChild(defaultOption);
1320
1397
  const entry = catalog().find((candidate) => candidate.provider === provider);
1321
- for (const model of list(entry && entry.models)) {
1398
+ for (const model of list(entry?.models)) {
1322
1399
  if (!model.id) continue;
1323
- const option = el('option', null, model.tier + ' · ' + model.id);
1400
+ const option = el('option', null, `${model.tier} · ${model.id}`);
1324
1401
  option.value = model.id;
1325
1402
  select.appendChild(option);
1326
1403
  }
@@ -1328,7 +1405,7 @@
1328
1405
  selected &&
1329
1406
  !Array.prototype.some.call(select.options, (option) => option.value === selected)
1330
1407
  ) {
1331
- const current = el('option', null, 'configurado · ' + selected);
1408
+ const current = el('option', null, `configurado · ${selected}`);
1332
1409
  current.value = selected;
1333
1410
  select.appendChild(current);
1334
1411
  }
@@ -1364,9 +1441,7 @@
1364
1441
  for (const phase of snapshot.phases) {
1365
1442
  const item = el('li');
1366
1443
  item.className = 'detail-row';
1367
- item.appendChild(
1368
- el('span', 'item-icon icon-' + phase.status, PHASE_ICONS[phase.status] || '○'),
1369
- );
1444
+ item.appendChild(statusIcon(phase.status));
1370
1445
  const main = el('div', 'item-main');
1371
1446
  main.appendChild(el('div', 'item-title', phase.name));
1372
1447
  if (phase.error) main.appendChild(el('div', 'item-error', phase.error));
@@ -1387,19 +1462,17 @@
1387
1462
  }
1388
1463
  }
1389
1464
 
1465
+ // Uma linha dentro do bloco de estado, não uma lista num cartão próprio: os
1466
+ // passos são poucos e curtos, e o rótulo já vem do HTML (.next-steps-label).
1390
1467
  function renderNextSteps(snapshot) {
1391
1468
  clear(els.nextSteps);
1392
1469
  if (snapshot.nextSteps.length === 0) {
1393
1470
  const message =
1394
1471
  snapshot.status === 'completed' ? 'Pipeline concluído.' : 'Nenhum passo pendente.';
1395
- els.nextSteps.appendChild(el('p', 'empty', message));
1472
+ els.nextSteps.appendChild(el('span', 'empty', message));
1396
1473
  return;
1397
1474
  }
1398
- const list = el('ol');
1399
- for (const step of snapshot.nextSteps) {
1400
- list.appendChild(el('li', null, step));
1401
- }
1402
- els.nextSteps.appendChild(list);
1475
+ els.nextSteps.appendChild(el('span', null, snapshot.nextSteps.join(' · ')));
1403
1476
  }
1404
1477
 
1405
1478
  function renderStories(snapshot) {
@@ -1416,7 +1489,7 @@
1416
1489
  const item = el('li', storyStage === 'executing' ? 'story-executing' : null);
1417
1490
  item.classList.add('detail-row');
1418
1491
  const status = story.passes ? 'completed' : 'pending';
1419
- item.appendChild(el('span', 'item-icon icon-' + status, story.passes ? '✓' : '○'));
1492
+ item.appendChild(statusIcon(status, story.passes ? '✓' : '○'));
1420
1493
  const main = el('div', 'item-main');
1421
1494
  const title = el('div', 'item-title');
1422
1495
  title.appendChild(el('span', 'story-id', story.id));
@@ -1431,13 +1504,13 @@
1431
1504
  meta.appendChild(
1432
1505
  el(
1433
1506
  'span',
1434
- 'badge story-status-' + storyStatus,
1507
+ `badge story-status-${storyStatus}`,
1435
1508
  STORY_STATUS_LABELS[storyStatus] || storyStatus,
1436
1509
  ),
1437
1510
  );
1438
1511
  const dependencies = story.dependencies || [];
1439
1512
  if (dependencies.length > 0) {
1440
- meta.appendChild(el('span', 'muted story-deps', 'depende de: ' + dependencies.join(', ')));
1513
+ meta.appendChild(el('span', 'muted story-deps', `depende de: ${dependencies.join(', ')}`));
1441
1514
  }
1442
1515
  main.appendChild(meta);
1443
1516
 
@@ -1445,11 +1518,11 @@
1445
1518
  const duration = metric(story.durationSeconds);
1446
1519
  const stageLabel = STORY_STAGE_LABELS[storyStage] || storyStage;
1447
1520
  const stageText = story.stageSince
1448
- ? stageLabel + ' ' + formatAgo(story.stageSince)
1521
+ ? `${stageLabel} ${formatAgo(story.stageSince)}`
1449
1522
  : stageLabel;
1450
1523
  const side = itemSideText([
1451
1524
  stageText,
1452
- story.completedAt ? 'concluída ' + formatClock(story.completedAt) : '',
1525
+ story.completedAt ? `concluída ${formatClock(story.completedAt)}` : '',
1453
1526
  duration !== null ? formatDuration(duration) : '',
1454
1527
  formatUsage(story),
1455
1528
  ]);
@@ -1475,20 +1548,14 @@
1475
1548
  card.dataset.storyId = story.id;
1476
1549
 
1477
1550
  const head = el('span', 'kanban-card-head');
1478
- head.appendChild(
1479
- el(
1480
- 'span',
1481
- 'item-icon icon-' + (story.passes ? 'completed' : 'pending'),
1482
- story.passes ? '✓' : '○',
1483
- ),
1484
- );
1551
+ head.appendChild(statusIcon(story.passes ? 'completed' : 'pending', story.passes ? '✓' : '○'));
1485
1552
  head.appendChild(el('span', 'story-id', story.id));
1486
1553
  card.appendChild(head);
1487
1554
 
1488
1555
  card.appendChild(el('span', 'kanban-card-title', story.title));
1489
1556
  if (story.description) card.appendChild(el('span', 'kanban-card-desc', story.description));
1490
1557
  card.appendChild(
1491
- el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
1558
+ el('span', `badge story-status-${story.status}`, STORY_STATUS_LABELS[story.status]),
1492
1559
  );
1493
1560
  card.addEventListener('click', () => openDrawer('story', story.id));
1494
1561
  return card;
@@ -1533,10 +1600,10 @@
1533
1600
  if (cost.status === 'reported' || cost.status === 'estimated') {
1534
1601
  return formatCost(cost.amount, cost.status === 'estimated');
1535
1602
  }
1536
- return cost.reason ? 'não informado (' + cost.reason + ')' : 'não informado';
1603
+ return cost.reason ? `não informado (${cost.reason})` : 'não informado';
1537
1604
  }
1538
1605
 
1539
- function renderExecutionHistory(snapshot, executions) {
1606
+ function renderExecutionHistory(_snapshot, executions) {
1540
1607
  drawerSection('Tentativas, revisões e correções', (body) => {
1541
1608
  if (executions.length === 0) {
1542
1609
  body.appendChild(el('p', 'empty', 'Nenhuma invocação associada.'));
@@ -1549,7 +1616,7 @@
1549
1616
  title.appendChild(
1550
1617
  el(
1551
1618
  'span',
1552
- 'badge status-' + (execution.status === 'completed' ? 'completed' : execution.status),
1619
+ `badge status-${execution.status === 'completed' ? 'completed' : execution.status}`,
1553
1620
  execution.status,
1554
1621
  ),
1555
1622
  );
@@ -1557,7 +1624,7 @@
1557
1624
  el(
1558
1625
  'strong',
1559
1626
  null,
1560
- execution.purpose + ' · tentativa ' + execution.attempt + ' · ' + execution.trigger,
1627
+ `${execution.purpose} · tentativa ${execution.attempt} · ${execution.trigger}`,
1561
1628
  ),
1562
1629
  );
1563
1630
  item.appendChild(title);
@@ -1582,7 +1649,7 @@
1582
1649
  nowRow(grid, 'Tokens', formatUsage(execution.usage || {}) || '—');
1583
1650
  nowRow(grid, 'Custo', executionCost(execution));
1584
1651
  if (execution.correctionCycle)
1585
- nowRow(grid, 'Correção', 'ciclo ' + execution.correctionCycle);
1652
+ nowRow(grid, 'Correção', `ciclo ${execution.correctionCycle}`);
1586
1653
  if (execution.verdict?.status) nowRow(grid, 'Veredito', execution.verdict.status);
1587
1654
  item.appendChild(grid);
1588
1655
  if (execution.failure?.message)
@@ -1600,7 +1667,7 @@
1600
1667
  );
1601
1668
  drawerSection('Saída do processo', (body) => {
1602
1669
  const details = el('details', 'process-output');
1603
- const summary = el('summary', null, logs.length + ' linha(s) sanitizada(s)');
1670
+ const summary = el('summary', null, `${logs.length} linha(s) sanitizada(s)`);
1604
1671
  details.appendChild(summary);
1605
1672
  if (logs.length === 0) {
1606
1673
  details.appendChild(el('p', 'empty', 'Nenhuma saída capturada.'));
@@ -1608,7 +1675,7 @@
1608
1675
  const output = el('pre', 'process-output-body');
1609
1676
  output.textContent = logs
1610
1677
  .slice(-200)
1611
- .map((entry) => formatClock(entry.at) + ' ' + entry.message)
1678
+ .map((entry) => `${formatClock(entry.at)} ${entry.message}`)
1612
1679
  .join('\n');
1613
1680
  details.appendChild(output);
1614
1681
  }
@@ -1627,12 +1694,12 @@
1627
1694
  drawerSection('Diagnóstico global persistente', (body) => {
1628
1695
  const details = el('details', 'process-output');
1629
1696
  details.appendChild(
1630
- el('summary', null, entries.length + ' registro(s) em ~/.issue-flow/logs'),
1697
+ el('summary', null, `${entries.length} registro(s) em ~/.issue-flow/logs`),
1631
1698
  );
1632
1699
  const output = el('pre', 'process-output-body');
1633
1700
  output.textContent = entries
1634
1701
  .slice(0, 200)
1635
- .map((entry) => formatClock(entry.timestamp) + ' ' + entry.level + ' ' + entry.message)
1702
+ .map((entry) => `${formatClock(entry.timestamp)} ${entry.level} ${entry.message}`)
1636
1703
  .join('\n');
1637
1704
  details.appendChild(output);
1638
1705
  body.appendChild(details);
@@ -1654,8 +1721,8 @@
1654
1721
 
1655
1722
  clear(els.drawerBody);
1656
1723
  if (kind === 'phase') {
1657
- els.drawerTitle.textContent = 'Fase · ' + phase.name;
1658
- els.drawerBody.appendChild(el('span', 'badge status-' + phase.status, phase.status));
1724
+ els.drawerTitle.textContent = `Fase · ${phase.name}`;
1725
+ els.drawerBody.appendChild(el('span', `badge status-${phase.status}`, phase.status));
1659
1726
  const grid = el('dl', 'now-grid drawer-summary-grid');
1660
1727
  nowRow(grid, 'Início', phase.startedAt ? formatClock(phase.startedAt) : '—');
1661
1728
  nowRow(grid, 'Fim', phase.endedAt ? formatClock(phase.endedAt) : '—');
@@ -1682,13 +1749,13 @@
1682
1749
  return;
1683
1750
  }
1684
1751
 
1685
- els.drawerTitle.textContent = story.id + ' · ' + story.title;
1752
+ els.drawerTitle.textContent = `${story.id} · ${story.title}`;
1686
1753
  els.drawerBody.appendChild(
1687
- el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
1754
+ el('span', `badge story-status-${story.status}`, STORY_STATUS_LABELS[story.status]),
1688
1755
  );
1689
1756
 
1690
1757
  const timing = itemSideText([
1691
- story.completedAt ? 'concluída ' + formatClock(story.completedAt) : '',
1758
+ story.completedAt ? `concluída ${formatClock(story.completedAt)}` : '',
1692
1759
  metric(story.durationSeconds) !== null ? formatDuration(story.durationSeconds) : '',
1693
1760
  ]);
1694
1761
  if (timing) els.drawerBody.appendChild(el('p', 'muted drawer-timing', timing));
@@ -1736,7 +1803,7 @@
1736
1803
  item.appendChild(
1737
1804
  document.createTextNode(
1738
1805
  (STORY_STAGE_LABELS[entry.stage] || entry.stage || '') +
1739
- (entry.detail ? ' · ' + entry.detail : ''),
1806
+ (entry.detail ? ` · ${entry.detail}` : ''),
1740
1807
  ),
1741
1808
  );
1742
1809
  entries.appendChild(item);
@@ -1777,7 +1844,7 @@
1777
1844
  // referência guardada na abertura.
1778
1845
  const card =
1779
1846
  selected && selected.kind === 'story'
1780
- ? els.kanban.querySelector('[data-story-id="' + selected.id + '"]')
1847
+ ? els.kanban.querySelector(`[data-story-id="${selected.id}"]`)
1781
1848
  : null;
1782
1849
  if (card) card.focus();
1783
1850
  }
@@ -1792,7 +1859,7 @@
1792
1859
  for (const commit of snapshot.git.commits) {
1793
1860
  const item = el('li');
1794
1861
  const hash = repo
1795
- ? link(repo + '/commit/' + commit.hash, commit.hash, 'mono commit-hash')
1862
+ ? link(`${repo}/commit/${commit.hash}`, commit.hash, 'mono commit-hash')
1796
1863
  : el('span', 'mono commit-hash', commit.hash);
1797
1864
  item.appendChild(hash);
1798
1865
  const subject = el('div', 'item-main item-title', commit.subject);
@@ -1812,7 +1879,7 @@
1812
1879
  } else {
1813
1880
  for (const pr of snapshot.pullRequests) {
1814
1881
  const item = el('li');
1815
- item.appendChild(link(pr.url, '#' + pr.number));
1882
+ item.appendChild(link(pr.url, `#${pr.number}`));
1816
1883
  item.appendChild(el('div', 'item-main item-title', pr.title));
1817
1884
  els.pullRequests.appendChild(item);
1818
1885
  }
@@ -1829,7 +1896,7 @@
1829
1896
  }
1830
1897
  // Mais recentes primeiro: monitoramento lê o topo, sem gerenciar scroll.
1831
1898
  for (const entry of entries.slice().reverse()) {
1832
- const item = el('li', 'level-' + entry.level);
1899
+ const item = el('li', `level-${entry.level}`);
1833
1900
  item.appendChild(el('span', 'log-time', formatClock(entry.at)));
1834
1901
  item.appendChild(el('span', 'log-level', entry.level));
1835
1902
  item.appendChild(el('span', 'log-message', entry.message));
@@ -1850,27 +1917,27 @@
1850
1917
  case 'session:start':
1851
1918
  return 'Sessão iniciada';
1852
1919
  case 'session:end':
1853
- return 'Sessão encerrada: ' + event.status;
1920
+ return `Sessão encerrada: ${event.status}`;
1854
1921
  case 'phase:start':
1855
- return 'Fase iniciada: ' + event.phase;
1922
+ return `Fase iniciada: ${event.phase}`;
1856
1923
  case 'phase:end':
1857
- return 'Fase encerrada: ' + event.phase + (event.success ? ' (ok)' : ' (falhou)');
1924
+ return `Fase encerrada: ${event.phase}${event.success ? ' (ok)' : ' (falhou)'}`;
1858
1925
  case 'iteration:start':
1859
- return 'Iteração ' + event.iteration + ' iniciada';
1926
+ return `Iteração ${event.iteration} iniciada`;
1860
1927
  case 'iteration:end':
1861
- return 'Iteração ' + event.iteration + ' encerrada';
1928
+ return `Iteração ${event.iteration} encerrada`;
1862
1929
  case 'retry':
1863
- return 'Retry ' + event.attempt + (event.kind ? ': ' + event.kind : '');
1930
+ return `Retry ${event.attempt}${event.kind ? `: ${event.kind}` : ''}`;
1864
1931
  case 'agent:attempt':
1865
- return 'Tentativa ' + event.attempt + ' com ' + event.provider;
1932
+ return `Tentativa ${event.attempt} com ${event.provider}`;
1866
1933
  case 'agent:activity':
1867
- return 'Atividade recebida de ' + event.provider;
1934
+ return `Atividade recebida de ${event.provider}`;
1868
1935
  case 'agent:result':
1869
1936
  return (
1870
1937
  event.provider +
1871
1938
  (event.success
1872
1939
  ? ' concluiu a tentativa'
1873
- : ' falhou: ' + (event.failureKind || 'desconhecida'))
1940
+ : ` falhou: ${event.failureKind || 'desconhecida'}`)
1874
1941
  );
1875
1942
  case 'failover':
1876
1943
  return (
@@ -1878,10 +1945,10 @@
1878
1945
  event.from +
1879
1946
  ' para ' +
1880
1947
  event.to +
1881
- (event.reason ? ': ' + event.reason : '')
1948
+ (event.reason ? `: ${event.reason}` : '')
1882
1949
  );
1883
1950
  case 'correction:cycle':
1884
- return 'Ciclo de correção ' + event.cycle + '/' + event.maxCycles;
1951
+ return `Ciclo de correção ${event.cycle}/${event.maxCycles}`;
1885
1952
  case 'log':
1886
1953
  return event.message;
1887
1954
  default:
@@ -1892,7 +1959,7 @@
1892
1959
  function renderHistory() {
1893
1960
  clear(els.history);
1894
1961
  const entries = state.events.filter((entry) => {
1895
- const type = entry && entry.event && entry.event.type;
1962
+ const type = entry?.event?.type;
1896
1963
  if (!type) return false;
1897
1964
  if (state.historyFilter === 'resilience') return RESILIENCE_EVENTS.has(type);
1898
1965
  if (state.historyFilter === 'pipeline') return !RESILIENCE_EVENTS.has(type);
@@ -1914,8 +1981,8 @@
1914
1981
 
1915
1982
  function renderMeta(snapshot) {
1916
1983
  const parts = [];
1917
- if (snapshot.sessionId) parts.push('sessão ' + snapshot.sessionId);
1918
- if (snapshot.updatedAt) parts.push('atualizado ' + formatClock(snapshot.updatedAt));
1984
+ if (snapshot.sessionId) parts.push(`execução ${snapshot.sessionId}`);
1985
+ if (snapshot.updatedAt) parts.push(`atualizado ${formatClock(snapshot.updatedAt)}`);
1919
1986
  parts.push('somente leitura');
1920
1987
  els.sessionMeta.textContent = parts.join(' · ');
1921
1988
  }
@@ -1938,7 +2005,7 @@
1938
2005
  const estimate = snapshot.estimatedRemainingSeconds;
1939
2006
  if (snapshot.status === 'running' && estimate !== null) {
1940
2007
  els.estimate.hidden = false;
1941
- els.estimate.textContent = '~' + formatDuration(estimate) + ' restantes (estimativa)';
2008
+ els.estimate.textContent = `~${formatDuration(estimate)} restantes (estimativa)`;
1942
2009
  } else {
1943
2010
  els.estimate.hidden = true;
1944
2011
  }
@@ -1963,6 +2030,8 @@
1963
2030
  }
1964
2031
 
1965
2032
  async function init() {
2033
+ const tablist = document.querySelector('[role="tablist"]');
2034
+ if (tablist) tablist.addEventListener('keydown', onTabListKeydown);
1966
2035
  for (const tab of els.tabs) {
1967
2036
  tab.addEventListener('click', () => setActiveTab(tab.id));
1968
2037
  }
@@ -2017,7 +2086,7 @@
2017
2086
  if (stored === null && Number.isFinite(suggested) && suggested > 0) {
2018
2087
  state.refreshSeconds = suggested;
2019
2088
  }
2020
- } catch (err) {
2089
+ } catch (_err) {
2021
2090
  // Sem /api/health segue o default local (5s).
2022
2091
  }
2023
2092
  if (stored !== null) state.refreshSeconds = stored;