issue-flow 0.12.0 → 0.14.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 (197) hide show
  1. package/README.md +79 -274
  2. package/dist/agent-OX3KPHE4.js +30 -0
  3. package/dist/{analyze-OEET7KGE.js → analyze-WIND2SEB.js} +31 -16
  4. package/dist/analyze-WIND2SEB.js.map +1 -0
  5. package/dist/{apply-ZIA3C52S.js → apply-JNOI2LR4.js} +18 -7
  6. package/dist/{apply-ZIA3C52S.js.map → apply-JNOI2LR4.js.map} +1 -1
  7. package/dist/bench-B4VZON7E.js +910 -0
  8. package/dist/bench-B4VZON7E.js.map +1 -0
  9. package/dist/{chunk-FADTCQGM.js → chunk-2IQ6M4ZE.js} +94 -32
  10. package/dist/chunk-2IQ6M4ZE.js.map +1 -0
  11. package/dist/chunk-3BWZO4YI.js +141 -0
  12. package/dist/chunk-3BWZO4YI.js.map +1 -0
  13. package/dist/chunk-3WFZETIM.js +163 -0
  14. package/dist/chunk-3WFZETIM.js.map +1 -0
  15. package/dist/chunk-67YIU2VB.js +362 -0
  16. package/dist/chunk-67YIU2VB.js.map +1 -0
  17. package/dist/{chunk-QCPGHBSZ.js → chunk-6BENHR2I.js} +63 -21
  18. package/dist/chunk-6BENHR2I.js.map +1 -0
  19. package/dist/{chunk-BYZDPR7N.js → chunk-6QYMSNVA.js} +194 -235
  20. package/dist/chunk-6QYMSNVA.js.map +1 -0
  21. package/dist/{chunk-Y5ZDJ4PJ.js → chunk-76MMPJ53.js} +20 -10
  22. package/dist/chunk-76MMPJ53.js.map +1 -0
  23. package/dist/chunk-7Z4Y3VKM.js +26 -0
  24. package/dist/chunk-7Z4Y3VKM.js.map +1 -0
  25. package/dist/{chunk-3QHK4YQA.js → chunk-B72RQZBV.js} +346 -119
  26. package/dist/chunk-B72RQZBV.js.map +1 -0
  27. package/dist/chunk-BEZ5BA5U.js +625 -0
  28. package/dist/chunk-BEZ5BA5U.js.map +1 -0
  29. package/dist/chunk-BRCNQXYQ.js +74 -0
  30. package/dist/chunk-BRCNQXYQ.js.map +1 -0
  31. package/dist/chunk-CGKLE6WR.js +1203 -0
  32. package/dist/chunk-CGKLE6WR.js.map +1 -0
  33. package/dist/chunk-CRGIQFUQ.js +83 -0
  34. package/dist/chunk-CRGIQFUQ.js.map +1 -0
  35. package/dist/{chunk-NS6R37II.js → chunk-F6TOYSYY.js} +141 -12
  36. package/dist/chunk-F6TOYSYY.js.map +1 -0
  37. package/dist/chunk-G5EK2XRN.js +25 -0
  38. package/dist/chunk-G5EK2XRN.js.map +1 -0
  39. package/dist/chunk-HAKDLR6N.js +134 -0
  40. package/dist/chunk-HAKDLR6N.js.map +1 -0
  41. package/dist/chunk-J3SOTUVI.js +298 -0
  42. package/dist/chunk-J3SOTUVI.js.map +1 -0
  43. package/dist/chunk-JN6D3MY6.js +30 -0
  44. package/dist/chunk-JN6D3MY6.js.map +1 -0
  45. package/dist/{chunk-LAFNLLQP.js → chunk-KMRFRXIT.js} +5 -5
  46. package/dist/chunk-KT7JKOXB.js +72 -0
  47. package/dist/chunk-KT7JKOXB.js.map +1 -0
  48. package/dist/chunk-L6HSKTQE.js +77 -0
  49. package/dist/chunk-L6HSKTQE.js.map +1 -0
  50. package/dist/chunk-LBGHI4S2.js +1264 -0
  51. package/dist/chunk-LBGHI4S2.js.map +1 -0
  52. package/dist/chunk-LEW4QM74.js +180 -0
  53. package/dist/chunk-LEW4QM74.js.map +1 -0
  54. package/dist/{chunk-FJP3UQGD.js → chunk-LFEQIZLB.js} +115 -20
  55. package/dist/chunk-LFEQIZLB.js.map +1 -0
  56. package/dist/chunk-OOVG2KBY.js +77 -0
  57. package/dist/chunk-OOVG2KBY.js.map +1 -0
  58. package/dist/{chunk-56KS3TFQ.js → chunk-OR43364P.js} +115 -160
  59. package/dist/chunk-OR43364P.js.map +1 -0
  60. package/dist/chunk-P3LRUNC6.js +16 -0
  61. package/dist/chunk-P3LRUNC6.js.map +1 -0
  62. package/dist/{chunk-TSOLWK7Y.js → chunk-PBTA3QQX.js} +8 -7
  63. package/dist/{chunk-TSOLWK7Y.js.map → chunk-PBTA3QQX.js.map} +1 -1
  64. package/dist/{chunk-MJULIKQU.js → chunk-PHW2YV6N.js} +23 -47
  65. package/dist/chunk-PHW2YV6N.js.map +1 -0
  66. package/dist/{chunk-73ATPIYX.js → chunk-PZTWU277.js} +36 -71
  67. package/dist/chunk-PZTWU277.js.map +1 -0
  68. package/dist/{chunk-M5UCXTIT.js → chunk-QD4VOILZ.js} +22 -18
  69. package/dist/chunk-QD4VOILZ.js.map +1 -0
  70. package/dist/chunk-QJ5UZAH5.js +2718 -0
  71. package/dist/chunk-QJ5UZAH5.js.map +1 -0
  72. package/dist/chunk-QK7WUO7A.js +283 -0
  73. package/dist/chunk-QK7WUO7A.js.map +1 -0
  74. package/dist/{chunk-A4SLHOW7.js → chunk-QZVKRDJY.js} +27 -19
  75. package/dist/chunk-QZVKRDJY.js.map +1 -0
  76. package/dist/chunk-REO3PXXO.js +71 -0
  77. package/dist/chunk-REO3PXXO.js.map +1 -0
  78. package/dist/{chunk-EZATO3DW.js → chunk-RVO7L4FX.js} +594 -17
  79. package/dist/chunk-RVO7L4FX.js.map +1 -0
  80. package/dist/{chunk-XQUSIEKA.js → chunk-SZMHCEYV.js} +63 -14
  81. package/dist/chunk-SZMHCEYV.js.map +1 -0
  82. package/dist/chunk-TYRNTSPJ.js +243 -0
  83. package/dist/chunk-TYRNTSPJ.js.map +1 -0
  84. package/dist/chunk-UJ3L2MS7.js +238 -0
  85. package/dist/chunk-UJ3L2MS7.js.map +1 -0
  86. package/dist/chunk-V7MO5EDS.js +120 -0
  87. package/dist/chunk-V7MO5EDS.js.map +1 -0
  88. package/dist/chunk-WB2X37MO.js +415 -0
  89. package/dist/chunk-WB2X37MO.js.map +1 -0
  90. package/dist/{chunk-NTMMF5UD.js → chunk-X44FKNKP.js} +525 -252
  91. package/dist/chunk-X44FKNKP.js.map +1 -0
  92. package/dist/chunk-XEYH6ZZI.js +289 -0
  93. package/dist/chunk-XEYH6ZZI.js.map +1 -0
  94. package/dist/cli.js +267 -44
  95. package/dist/cli.js.map +1 -1
  96. package/dist/{config-PRJGWW7J.js → config-HMFMCY2M.js} +31 -5
  97. package/dist/contract-JH2WBIO6.js +8 -0
  98. package/dist/conventions-NJF2OPVQ.js +146 -0
  99. package/dist/conventions-NJF2OPVQ.js.map +1 -0
  100. package/dist/diagnostics-THH42ZMP.js +23 -0
  101. package/dist/execute-KFNNZZHI.js +35 -0
  102. package/dist/{generate-EGB4KV2B.js → generate-FW5C6NP7.js} +37 -20
  103. package/dist/{generate-EGB4KV2B.js.map → generate-FW5C6NP7.js.map} +1 -1
  104. package/dist/git-W72QQ6GT.js +33 -0
  105. package/dist/init-HTN4V67Q.js +24 -0
  106. package/dist/{operations-BYT626KQ.js → operations-BCFOBH42.js} +27 -15
  107. package/dist/{operations-BYT626KQ.js.map → operations-BCFOBH42.js.map} +1 -1
  108. package/dist/permissions-4AKNQZHK.js +18 -0
  109. package/dist/plan-SIZLGA4V.js +34 -0
  110. package/dist/{policy-6S4GFC76.js → policy-DT6Q6ZVM.js} +20 -6
  111. package/dist/policy-DT6Q6ZVM.js.map +1 -0
  112. package/dist/pr-OMWCGUOW.js +39 -0
  113. package/dist/pr-review-N24CZPKR.js +32 -0
  114. package/dist/pr-review-N24CZPKR.js.map +1 -0
  115. package/dist/prd-5OFTZAHJ.js +33 -0
  116. package/dist/prd-5OFTZAHJ.js.map +1 -0
  117. package/dist/ps-RFORTCUD.js +93 -0
  118. package/dist/ps-RFORTCUD.js.map +1 -0
  119. package/dist/recorder-KCCPMKQG.js +32 -0
  120. package/dist/recorder-KCCPMKQG.js.map +1 -0
  121. package/dist/registry-GLPXOMFG.js +15 -0
  122. package/dist/registry-GLPXOMFG.js.map +1 -0
  123. package/dist/{resume-PPCNQVVV.js → resume-M5RLKP4I.js} +55 -34
  124. package/dist/{resume-PPCNQVVV.js.map → resume-M5RLKP4I.js.map} +1 -1
  125. package/dist/review-YM4JAABY.js +35 -0
  126. package/dist/review-YM4JAABY.js.map +1 -0
  127. package/dist/routing-NNT5WKGX.js +94 -0
  128. package/dist/routing-NNT5WKGX.js.map +1 -0
  129. package/dist/run-PZUKHPQK.js +54 -0
  130. package/dist/run-PZUKHPQK.js.map +1 -0
  131. package/dist/session-publisher-T7EI3CIP.js +11 -0
  132. package/dist/session-publisher-T7EI3CIP.js.map +1 -0
  133. package/dist/{state-manager-2EDVDBAR.js → state-manager-GQH32C44.js} +4 -2
  134. package/dist/state-manager-GQH32C44.js.map +1 -0
  135. package/dist/usage-V2CHE5PY.js +177 -0
  136. package/dist/usage-V2CHE5PY.js.map +1 -0
  137. package/dist/{web-EF5WIIGG.js → web-EA3KGNFN.js} +45 -30
  138. package/dist/web-EA3KGNFN.js.map +1 -0
  139. package/package.json +6 -2
  140. package/prompts/execute.md +7 -8
  141. package/prompts/plan.md +4 -5
  142. package/prompts/pr.md +2 -2
  143. package/web/AGENTS.md +152 -13
  144. package/web/public/app.css +380 -109
  145. package/web/public/app.js +525 -25
  146. package/web/public/index.html +68 -1
  147. package/dist/analyze-OEET7KGE.js.map +0 -1
  148. package/dist/chunk-3QHK4YQA.js.map +0 -1
  149. package/dist/chunk-42PJFMAR.js +0 -605
  150. package/dist/chunk-42PJFMAR.js.map +0 -1
  151. package/dist/chunk-4JSOYE4G.js +0 -368
  152. package/dist/chunk-4JSOYE4G.js.map +0 -1
  153. package/dist/chunk-56KS3TFQ.js.map +0 -1
  154. package/dist/chunk-73ATPIYX.js.map +0 -1
  155. package/dist/chunk-A4SLHOW7.js.map +0 -1
  156. package/dist/chunk-BAUPQXPE.js +0 -173
  157. package/dist/chunk-BAUPQXPE.js.map +0 -1
  158. package/dist/chunk-BYZDPR7N.js.map +0 -1
  159. package/dist/chunk-CD5ULRVC.js +0 -615
  160. package/dist/chunk-CD5ULRVC.js.map +0 -1
  161. package/dist/chunk-DIDUVL6O.js +0 -1365
  162. package/dist/chunk-DIDUVL6O.js.map +0 -1
  163. package/dist/chunk-EZATO3DW.js.map +0 -1
  164. package/dist/chunk-FADTCQGM.js.map +0 -1
  165. package/dist/chunk-FJP3UQGD.js.map +0 -1
  166. package/dist/chunk-LWRSMTTZ.js +0 -18
  167. package/dist/chunk-LWRSMTTZ.js.map +0 -1
  168. package/dist/chunk-M5UCXTIT.js.map +0 -1
  169. package/dist/chunk-M66SJHQQ.js +0 -206
  170. package/dist/chunk-M66SJHQQ.js.map +0 -1
  171. package/dist/chunk-MJULIKQU.js.map +0 -1
  172. package/dist/chunk-NS6R37II.js.map +0 -1
  173. package/dist/chunk-NTMMF5UD.js.map +0 -1
  174. package/dist/chunk-QCPGHBSZ.js.map +0 -1
  175. package/dist/chunk-XQUSIEKA.js.map +0 -1
  176. package/dist/chunk-Y5ZDJ4PJ.js.map +0 -1
  177. package/dist/execute-XBNVL753.js +0 -18
  178. package/dist/init-AJHCXJBP.js +0 -10
  179. package/dist/plan-QOZ3RIO6.js +0 -21
  180. package/dist/policy-6S4GFC76.js.map +0 -1
  181. package/dist/pr-EBBHD2ZX.js +0 -26
  182. package/dist/pr-review-VVMW5PO4.js +0 -20
  183. package/dist/prd-JQSZF7PR.js +0 -21
  184. package/dist/review-3F6MARWH.js +0 -19
  185. package/dist/run-RXE5GDQO.js +0 -36
  186. package/dist/web-EF5WIIGG.js.map +0 -1
  187. /package/dist/{config-PRJGWW7J.js.map → agent-OX3KPHE4.js.map} +0 -0
  188. /package/dist/{chunk-LAFNLLQP.js.map → chunk-KMRFRXIT.js.map} +0 -0
  189. /package/dist/{execute-XBNVL753.js.map → config-HMFMCY2M.js.map} +0 -0
  190. /package/dist/{init-AJHCXJBP.js.map → contract-JH2WBIO6.js.map} +0 -0
  191. /package/dist/{plan-QOZ3RIO6.js.map → diagnostics-THH42ZMP.js.map} +0 -0
  192. /package/dist/{pr-EBBHD2ZX.js.map → execute-KFNNZZHI.js.map} +0 -0
  193. /package/dist/{pr-review-VVMW5PO4.js.map → git-W72QQ6GT.js.map} +0 -0
  194. /package/dist/{prd-JQSZF7PR.js.map → init-HTN4V67Q.js.map} +0 -0
  195. /package/dist/{review-3F6MARWH.js.map → permissions-4AKNQZHK.js.map} +0 -0
  196. /package/dist/{run-RXE5GDQO.js.map → plan-SIZLGA4V.js.map} +0 -0
  197. /package/dist/{state-manager-2EDVDBAR.js.map → pr-OMWCGUOW.js.map} +0 -0
package/web/public/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Interface do monitoramento web do issue-flow.
2
2
  // JS puro, sem framework e sem recursos externos — funciona offline.
3
- // Consome GET api/sessions, GET api/status (ETag/304) e GET api/health;
3
+ // Consome GET api/sessions, GET api/status (ETag/304), GET api/events e GET api/health;
4
4
  // todo texto dinâmico entra via textContent (nunca innerHTML com dados
5
5
  // do snapshot).
6
6
  (() => {
@@ -9,6 +9,12 @@
9
9
  const REFRESH_OPTIONS = [3, 5, 10, 30];
10
10
  const PAUSED = 0;
11
11
  const STORAGE_KEY = 'issue-flow:refresh-seconds';
12
+ const THEME_STORAGE_KEY = 'issue-flow:theme';
13
+ // Esta chave também é lida pelo <script> inline do <head>
14
+ // do index.html, que aplica o tema antes do primeiro paint. A duplicação
15
+ // é deliberada: aquele script roda antes deste arquivo existir na página e
16
+ // não pode depender de nenhum símbolo daqui. Mudou o formato do valor,
17
+ // mude nos dois lugares.
12
18
  const MAX_BACKOFF_MS = 60000;
13
19
  const ALERT_PREVIEW = 3;
14
20
  const DESCRIPTION_PREVIEW = 140;
@@ -71,6 +77,8 @@
71
77
  estimate: document.getElementById('estimate'),
72
78
  refreshSelect: document.getElementById('refresh-select'),
73
79
  refreshSelectDashboard: document.getElementById('refresh-select-dashboard'),
80
+ themeSelect: document.getElementById('theme-select'),
81
+ themeSelectDashboard: document.getElementById('theme-select-dashboard'),
74
82
  alerts: document.getElementById('alerts'),
75
83
  alertsBody: document.getElementById('alerts-body'),
76
84
  issueSummary: document.getElementById('issue-summary'),
@@ -79,6 +87,8 @@
79
87
  progressPercent: document.getElementById('progress-percent'),
80
88
  progressCounters: document.getElementById('progress-counters'),
81
89
  now: document.getElementById('now'),
90
+ resilience: document.getElementById('resilience'),
91
+ configuration: document.getElementById('configuration'),
82
92
  phases: document.getElementById('phases'),
83
93
  nextSteps: document.getElementById('next-steps'),
84
94
  stories: document.getElementById('stories'),
@@ -86,6 +96,8 @@
86
96
  pullRequests: document.getElementById('pull-requests'),
87
97
  logFilter: document.getElementById('log-filter'),
88
98
  logs: document.getElementById('logs'),
99
+ historyFilter: document.getElementById('history-filter'),
100
+ history: document.getElementById('history'),
89
101
  sessionMeta: document.getElementById('session-meta'),
90
102
  tabs: Array.prototype.slice.call(document.querySelectorAll('[role="tab"]')),
91
103
  kanban: document.getElementById('kanban'),
@@ -105,9 +117,16 @@
105
117
  polling: false,
106
118
  logFilter: 'all',
107
119
  activeTab: 'tab-execution',
120
+ // 'system' | 'light' | 'dark'. Valor inicial em initTheme(), lido da
121
+ // preferência guardada em localStorage por readStoredTheme().
122
+ theme: 'system',
123
+ historyFilter: 'all',
124
+ events: [],
125
+ diagnostics: [],
108
126
  // Só o id: o card que abriu o drawer é destruído no próximo render, então
109
127
  // guardar o nó levaria a uma referência morta.
110
- selectedStoryId: null,
128
+ selectedDetail: null,
129
+ configWritable: false,
111
130
  // Multi-sessão (#35): lista de /api/sessions e seleção explícita do usuário.
112
131
  // selectedSessionId null = modo automático (1 sessão → detalhe; 2+ → dashboard).
113
132
  sessions: [],
@@ -116,6 +135,7 @@
116
135
  detailSessionId: null,
117
136
  viewMode: 'detail', // 'detail' | 'dashboard'
118
137
  statusUrl: 'api/status',
138
+ eventsUrl: null,
119
139
  // Se selectSession/clearSessionSelection chega durante um poll, reexecuta.
120
140
  pollAgain: false,
121
141
  };
@@ -186,8 +206,9 @@
186
206
  }
187
207
 
188
208
  // Custos abaixo de um centavo perderiam todo o significado com 2 casas.
189
- function formatCost(value) {
190
- return '~$' + (Math.abs(value) < 0.01 ? value.toFixed(4) : value.toFixed(2));
209
+ function formatCost(value, approximate) {
210
+ const prefix = approximate === false ? '$' : '~$';
211
+ return prefix + (Math.abs(value) < 0.01 ? value.toFixed(4) : value.toFixed(2));
191
212
  }
192
213
 
193
214
  // Ex.: '12.4k in / 3.1k out · 88.0k cache · ~$0.42'. Segmentos sem dado são
@@ -301,6 +322,88 @@
301
322
  }
302
323
  }
303
324
 
325
+ // ---- Tema ------------------------------------------------------------------
326
+ // Três estados: 'system' segue o SO (o @media decide), 'light'/'dark' forçam.
327
+ // Quem pinta é o CSS; aqui só se define (ou remove) o data-theme da raiz.
328
+
329
+ // Par ler/gravar tolerante a exceção, na mesma forma de readStoredRefresh()
330
+ // e storeRefresh(): armazenamento bloqueado (janela privada, cookies de
331
+ // terceiros desligados) não pode derrubar o painel.
332
+ function readStoredTheme() {
333
+ let raw = null;
334
+ try {
335
+ raw = window.localStorage.getItem(THEME_STORAGE_KEY);
336
+ } catch (err) {
337
+ // localStorage indisponível (ex.: bloqueado) — segue no modo sistema.
338
+ }
339
+ return raw === 'light' || raw === 'dark' ? raw : 'system';
340
+ }
341
+
342
+ function storeTheme(value) {
343
+ try {
344
+ window.localStorage.setItem(THEME_STORAGE_KEY, value);
345
+ } catch (err) {
346
+ // Persistência é conveniência; falha é ignorada. Sem gravar, a escolha
347
+ // continua valendo nesta aba — só não sobrevive ao reload.
348
+ }
349
+ }
350
+
351
+ function applyTheme(theme) {
352
+ // 'system' **remove** o atributo em vez de gravar 'system': é a ausência
353
+ // do data-theme que devolve a decisão ao @media (prefers-color-scheme).
354
+ if (theme === 'light' || theme === 'dark') {
355
+ document.documentElement.setAttribute('data-theme', theme);
356
+ } else {
357
+ document.documentElement.removeAttribute('data-theme');
358
+ }
359
+ }
360
+
361
+ // Os dois headers têm o seu seletor; mudar num precisa refletir no outro.
362
+ // As três opções são estáticas no HTML — aqui só se sincroniza o .value.
363
+ function syncThemeSelects() {
364
+ if (els.themeSelect) els.themeSelect.value = state.theme;
365
+ if (els.themeSelectDashboard) els.themeSelectDashboard.value = state.theme;
366
+ }
367
+
368
+ // Preferência do SO. Só se observa no modo 'system': é ali que a troca de
369
+ // claro para escuro no SO precisa chegar ao painel com a página aberta.
370
+ const systemThemeQuery =
371
+ typeof window.matchMedia === 'function'
372
+ ? window.matchMedia('(prefers-color-scheme: dark)')
373
+ : null;
374
+
375
+ function onSystemThemeChange() {
376
+ // Sincroniza o lado JS do tema com a preferência nova do SO: a raiz (que
377
+ // no modo sistema segue sem data-theme, para o @media decidir) e o .value
378
+ // dos dois seletores. O repaint das cores é do próprio @media, que o
379
+ // navegador reavalia sozinho — por isso não há nada a pintar aqui.
380
+ applyTheme(state.theme);
381
+ syncThemeSelects();
382
+ }
383
+
384
+ // Anexa no modo 'system' e desanexa nos modos forçados: com tema forçado a
385
+ // preferência do SO não vale, e continuar ouvindo seria ruído.
386
+ function watchSystemTheme(enabled) {
387
+ if (!systemThemeQuery) return;
388
+ if (enabled) systemThemeQuery.addEventListener('change', onSystemThemeChange);
389
+ else systemThemeQuery.removeEventListener('change', onSystemThemeChange);
390
+ }
391
+
392
+ function setTheme(theme) {
393
+ state.theme = theme === 'light' || theme === 'dark' ? theme : 'system';
394
+ applyTheme(state.theme);
395
+ syncThemeSelects();
396
+ watchSystemTheme(state.theme === 'system');
397
+ }
398
+
399
+ function initTheme() {
400
+ // O <script> inline do <head> já leu a mesma chave e aplicou o tema antes
401
+ // do primeiro paint; esta é a segunda metade da duplicação deliberada
402
+ // documentada em THEME_STORAGE_KEY — traz a preferência para o state e,
403
+ // com ela, para os dois seletores.
404
+ setTheme(readStoredTheme());
405
+ }
406
+
304
407
  // ---- Polling: intervalo configurável, aba oculta e backoff ----------------
305
408
 
306
409
  function readStoredRefresh() {
@@ -372,9 +475,6 @@
372
475
  if (!stillThere) {
373
476
  state.selectedSessionId = null;
374
477
  state.etag = null;
375
- } else if (sessions.length === 1) {
376
- // Seleção explícita deixa de fazer sentido com uma única sessão.
377
- state.selectedSessionId = null;
378
478
  }
379
479
  }
380
480
 
@@ -398,11 +498,11 @@
398
498
  state.viewMode = mode;
399
499
  els.viewDashboard.hidden = mode !== 'dashboard';
400
500
  els.viewDetail.hidden = mode !== 'detail';
401
- // Back quando o usuário escolheu um card e ainda dashboard para voltar.
501
+ // Back quando o usuário escolheu um card e existe uma lista para voltar.
402
502
  const showBack =
403
- mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 2;
503
+ mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 1;
404
504
  els.backToDashboard.hidden = !showBack;
405
- if (mode === 'dashboard' && state.selectedStoryId !== null) {
505
+ if (mode === 'dashboard' && state.selectedDetail !== null) {
406
506
  closeDrawer();
407
507
  }
408
508
  }
@@ -414,10 +514,18 @@
414
514
  return session.statusUrl.replace(/^\//, '');
415
515
  }
416
516
 
517
+ function eventsUrlFor(session) {
518
+ if (!session || !session.eventsUrl) return null;
519
+ return session.eventsUrl.replace(/^\//, '');
520
+ }
521
+
417
522
  function clearDetailState() {
418
523
  state.etag = null;
419
524
  state.snapshot = null;
420
525
  state.detailSessionId = null;
526
+ state.events = [];
527
+ state.diagnostics = [];
528
+ state.eventsUrl = null;
421
529
  }
422
530
 
423
531
  function requestPoll() {
@@ -491,6 +599,7 @@
491
599
  state.statusUrl = nextUrl;
492
600
  state.etag = null;
493
601
  }
602
+ state.eventsUrl = eventsUrlFor(resolved.session);
494
603
 
495
604
  // Evita flash da sessão anterior enquanto o status novo carrega.
496
605
  if (sessionChanged || !state.snapshot) {
@@ -516,6 +625,21 @@
516
625
  state.snapshot = await res.json();
517
626
  render();
518
627
  }
628
+ if (state.eventsUrl) {
629
+ const eventsRes = await fetch(state.eventsUrl, { cache: 'no-store' });
630
+ if (!eventsRes.ok) throw new Error('HTTP ' + eventsRes.status);
631
+ const entries = await eventsRes.json();
632
+ state.events = Array.isArray(entries) ? entries : [];
633
+ const diagnosticUrl =
634
+ 'api/diagnostics?session=' + encodeURIComponent(state.detailSessionId || '');
635
+ const diagnosticsRes = await fetch(diagnosticUrl, { cache: 'no-store' });
636
+ if (diagnosticsRes.ok) {
637
+ const diagnostics = await diagnosticsRes.json();
638
+ state.diagnostics = Array.isArray(diagnostics) ? diagnostics : [];
639
+ }
640
+ renderHistory();
641
+ renderDrawer(state.snapshot);
642
+ }
519
643
  setViewMode('detail');
520
644
  state.failures = 0;
521
645
  els.banner.hidden = true;
@@ -624,7 +748,13 @@
624
748
  meta.appendChild(el('span', null, 'correção ' + session.correctionCycle));
625
749
  }
626
750
  if (session.updatedAt) {
627
- meta.appendChild(el('span', null, 'atividade ' + formatAgo(session.updatedAt)));
751
+ meta.appendChild(
752
+ el('span', null, 'atividade ' + formatAgo(session.lastActivityAt || session.updatedAt)),
753
+ );
754
+ }
755
+ if (session.provider) meta.appendChild(el('span', null, 'provider ' + session.provider));
756
+ if (typeof session.attempt === 'number' && session.attempt > 0) {
757
+ meta.appendChild(el('span', null, 'tentativa ' + session.attempt));
628
758
  }
629
759
  card.appendChild(meta);
630
760
 
@@ -668,6 +798,8 @@
668
798
  renderRepository(snapshot);
669
799
  renderProgress(snapshot);
670
800
  renderNow(snapshot);
801
+ renderResilience(snapshot);
802
+ renderConfiguration(snapshot);
671
803
  renderPhases(snapshot);
672
804
  renderNextSteps(snapshot);
673
805
  renderStories(snapshot);
@@ -676,6 +808,7 @@
676
808
  renderDrawer(snapshot);
677
809
  renderGit(snapshot);
678
810
  renderLogs(snapshot);
811
+ renderHistory();
679
812
  renderMeta(snapshot);
680
813
  renderTimers();
681
814
  renderTitle(snapshot);
@@ -702,7 +835,15 @@
702
835
 
703
836
  const branch = snapshot.git.branch;
704
837
  const base = snapshot.git.baseBranch;
705
- els.branchLine.textContent = branch ? (base ? branch + ' ← ' + base : branch) : '';
838
+ const branchMode =
839
+ snapshot.git.branchCreated === false
840
+ ? 'branch atual · não criada pelo Issue Flow'
841
+ : snapshot.git.branchCreated === true
842
+ ? 'criada pelo Issue Flow'
843
+ : 'origem da branch não informada';
844
+ els.branchLine.textContent = branch
845
+ ? (base ? branch + ' ← ' + base : branch) + ' · ' + branchMode
846
+ : '';
706
847
 
707
848
  els.statusBadge.textContent = STATUS_LABELS[snapshot.status] || snapshot.status;
708
849
  els.statusBadge.className = 'badge status-' + snapshot.status;
@@ -857,6 +998,125 @@
857
998
  els.now.appendChild(grid);
858
999
  }
859
1000
 
1001
+ function renderResilience(snapshot) {
1002
+ clear(els.resilience);
1003
+ const resilience = snapshot.resilience || {};
1004
+ const grid = el('dl', 'now-grid');
1005
+ nowRow(grid, 'Tentativa', resilience.attempt > 0 ? String(resilience.attempt) : '—');
1006
+ nowRow(grid, 'Provider', resilience.provider || '—');
1007
+ nowRow(grid, 'Modelo', resilience.model || '—');
1008
+ nowRow(grid, 'Última falha', resilience.lastFailureKind || '—');
1009
+ nowRow(
1010
+ grid,
1011
+ 'Cooldown',
1012
+ resilience.cooldownUntil ? formatClock(resilience.cooldownUntil) : '—',
1013
+ );
1014
+ const activity = resilience.lastActivityAt
1015
+ ? formatClock(resilience.lastActivityAt) + ' (' + formatAgo(resilience.lastActivityAt) + ')'
1016
+ : '—';
1017
+ nowRow(grid, 'Última atividade', activity);
1018
+ els.resilience.appendChild(grid);
1019
+ }
1020
+
1021
+ function configSourceLabel(source) {
1022
+ const labels = {
1023
+ default: 'default do Issue Flow',
1024
+ global: 'configuração global',
1025
+ project: 'configuração do projeto',
1026
+ env: 'variável de ambiente',
1027
+ cli: 'override da execução',
1028
+ fallback: 'fallback',
1029
+ };
1030
+ return labels[source] || source || 'não informado';
1031
+ }
1032
+
1033
+ function renderConfiguration(snapshot) {
1034
+ clear(els.configuration);
1035
+ const config = snapshot.configuration;
1036
+ if (!config) {
1037
+ els.configuration.appendChild(el('p', 'empty', 'Configuração não capturada nesta execução.'));
1038
+ return;
1039
+ }
1040
+
1041
+ const summary = el('dl', 'now-grid');
1042
+ nowRow(
1043
+ summary,
1044
+ 'Harness padrão',
1045
+ (config.defaultProvider.value || '—') + ' · ' + configSourceLabel(config.defaultProvider.source),
1046
+ );
1047
+ nowRow(
1048
+ summary,
1049
+ 'Modelo padrão',
1050
+ (config.defaultModel.value || 'default do provider') +
1051
+ ' · ' +
1052
+ configSourceLabel(config.defaultModel.source),
1053
+ );
1054
+ nowRow(summary, 'Fallbacks', list(config.fallbacks).join(' → ') || 'nenhum configurado');
1055
+ nowRow(summary, 'Precedência', list(config.precedence).join(' → '));
1056
+ els.configuration.appendChild(summary);
1057
+
1058
+ const phases = el('div', 'config-phase-grid');
1059
+ for (const phase of list(config.phases)) {
1060
+ const row = el('button', 'config-phase-row');
1061
+ row.type = 'button';
1062
+ row.appendChild(el('span', 'mono', phase.phase));
1063
+ row.appendChild(el('span', null, phase.provider.value || '—'));
1064
+ row.appendChild(el('span', null, phase.model.value || 'default do provider'));
1065
+ row.appendChild(
1066
+ el(
1067
+ 'span',
1068
+ 'muted',
1069
+ configSourceLabel(phase.provider.source) + ' / ' + configSourceLabel(phase.model.source),
1070
+ ),
1071
+ );
1072
+ row.addEventListener('click', () => openDrawer('phase', phase.phase));
1073
+ phases.appendChild(row);
1074
+ }
1075
+ els.configuration.appendChild(phases);
1076
+
1077
+ const form = el('form', 'config-form');
1078
+ const provider = el('select');
1079
+ provider.setAttribute('aria-label', 'Harness padrão para execuções futuras');
1080
+ for (const id of ['claude', 'codex', 'cursor', 'antigravity']) {
1081
+ const option = el('option', null, id);
1082
+ option.value = id;
1083
+ provider.appendChild(option);
1084
+ }
1085
+ provider.value = config.defaultProvider.value || 'claude';
1086
+ const model = el('input');
1087
+ model.type = 'text';
1088
+ model.placeholder = 'modelo (vazio = default)';
1089
+ model.value = config.defaultModel.value || '';
1090
+ const save = el('button', 'config-save', 'Salvar preferência global');
1091
+ save.type = 'submit';
1092
+ const feedback = el('span', 'muted');
1093
+ form.appendChild(provider);
1094
+ form.appendChild(model);
1095
+ form.appendChild(save);
1096
+ form.appendChild(feedback);
1097
+ form.addEventListener('submit', async (event) => {
1098
+ event.preventDefault();
1099
+ save.disabled = true;
1100
+ feedback.textContent = 'salvando…';
1101
+ try {
1102
+ const response = await fetch('api/config/agent', {
1103
+ method: 'POST',
1104
+ headers: { 'Content-Type': 'application/json' },
1105
+ body: JSON.stringify({ provider: provider.value, model: model.value }),
1106
+ });
1107
+ const result = await response.json();
1108
+ if (!response.ok) throw new Error(result.error || 'falha ao salvar');
1109
+ feedback.textContent = 'salvo para execuções futuras';
1110
+ } catch (error) {
1111
+ feedback.textContent = error.message || 'falha ao salvar';
1112
+ } finally {
1113
+ save.disabled = false;
1114
+ }
1115
+ });
1116
+ form.hidden = !state.configWritable;
1117
+ els.configuration.appendChild(form);
1118
+ }
1119
+
860
1120
  function renderPhases(snapshot) {
861
1121
  clear(els.phases);
862
1122
  if (snapshot.phases.length === 0) {
@@ -865,6 +1125,7 @@
865
1125
  }
866
1126
  for (const phase of snapshot.phases) {
867
1127
  const item = el('li');
1128
+ item.className = 'detail-row';
868
1129
  item.appendChild(el('span', 'item-icon icon-' + phase.status, PHASE_ICONS[phase.status] || '○'));
869
1130
  const main = el('div', 'item-main');
870
1131
  main.appendChild(el('div', 'item-title', phase.name));
@@ -876,6 +1137,12 @@
876
1137
  formatUsage(phase),
877
1138
  ]);
878
1139
  if (side) item.appendChild(el('span', 'item-side', side));
1140
+ item.tabIndex = 0;
1141
+ item.setAttribute('role', 'button');
1142
+ item.addEventListener('click', () => openDrawer('phase', phase.name));
1143
+ item.addEventListener('keydown', (event) => {
1144
+ if (event.key === 'Enter' || event.key === ' ') openDrawer('phase', phase.name);
1145
+ });
879
1146
  els.phases.appendChild(item);
880
1147
  }
881
1148
  }
@@ -906,6 +1173,7 @@
906
1173
  // antes da issue 38 pode chegar sem ele.
907
1174
  const storyStage = story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
908
1175
  const item = el('li', storyStage === 'executing' ? 'story-executing' : null);
1176
+ item.classList.add('detail-row');
909
1177
  const status = story.passes ? 'completed' : 'pending';
910
1178
  item.appendChild(el('span', 'item-icon icon-' + status, story.passes ? '✓' : '○'));
911
1179
  const main = el('div', 'item-main');
@@ -940,6 +1208,12 @@
940
1208
  formatUsage(story),
941
1209
  ]);
942
1210
  if (side) item.appendChild(el('span', 'item-side', side));
1211
+ item.tabIndex = 0;
1212
+ item.setAttribute('role', 'button');
1213
+ item.addEventListener('click', () => openDrawer('story', story.id));
1214
+ item.addEventListener('keydown', (event) => {
1215
+ if (event.key === 'Enter' || event.key === ' ') openDrawer('story', story.id);
1216
+ });
943
1217
  els.stories.appendChild(item);
944
1218
  }
945
1219
  }
@@ -966,7 +1240,7 @@
966
1240
  card.appendChild(
967
1241
  el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
968
1242
  );
969
- card.addEventListener('click', () => openDrawer(story.id));
1243
+ card.addEventListener('click', () => openDrawer('story', story.id));
970
1244
  return card;
971
1245
  }
972
1246
 
@@ -997,18 +1271,145 @@
997
1271
  if (event.key === 'Escape') closeDrawer();
998
1272
  }
999
1273
 
1000
- // Reidrata a partir do id a cada render: o drawer aberto sobrevive ao poll.
1274
+ function executionsFor(snapshot, kind, id) {
1275
+ return list(snapshot.executions).filter((execution) => {
1276
+ if (kind === 'phase') return execution.purpose === id;
1277
+ return list(execution.storyIds).includes(id);
1278
+ });
1279
+ }
1280
+
1281
+ function executionCost(execution) {
1282
+ const cost = execution.cost || {};
1283
+ if (cost.status === 'reported' || cost.status === 'estimated') {
1284
+ return formatCost(cost.amount, cost.status === 'estimated');
1285
+ }
1286
+ return cost.reason ? 'não informado (' + cost.reason + ')' : 'não informado';
1287
+ }
1288
+
1289
+ function renderExecutionHistory(snapshot, executions) {
1290
+ drawerSection('Tentativas, revisões e correções', (body) => {
1291
+ if (executions.length === 0) {
1292
+ body.appendChild(el('p', 'empty', 'Nenhuma invocação associada.'));
1293
+ return;
1294
+ }
1295
+ const timeline = el('ol', 'execution-timeline');
1296
+ for (const execution of executions.slice().reverse()) {
1297
+ const item = el('li', 'execution-entry');
1298
+ const title = el('div', 'execution-entry-head');
1299
+ title.appendChild(
1300
+ el(
1301
+ 'span',
1302
+ 'badge status-' + (execution.status === 'completed' ? 'completed' : execution.status),
1303
+ execution.status,
1304
+ ),
1305
+ );
1306
+ title.appendChild(
1307
+ el(
1308
+ 'strong',
1309
+ null,
1310
+ execution.purpose + ' · tentativa ' + execution.attempt + ' · ' + execution.trigger,
1311
+ ),
1312
+ );
1313
+ item.appendChild(title);
1314
+ const grid = el('dl', 'now-grid execution-grid');
1315
+ nowRow(grid, 'Harness', execution.agent?.harness || '—');
1316
+ nowRow(grid, 'Modelo', execution.agent?.model?.resolved || execution.agent?.model?.requested || '—');
1317
+ nowRow(grid, 'Início', formatClock(execution.startedAt));
1318
+ nowRow(grid, 'Fim', execution.finishedAt ? formatClock(execution.finishedAt) : 'em andamento');
1319
+ nowRow(grid, 'Duração', metric(execution.durationMs) !== null ? formatDuration(execution.durationMs / 1000) : '—');
1320
+ nowRow(grid, 'Tokens', formatUsage(execution.usage || {}) || '—');
1321
+ nowRow(grid, 'Custo', executionCost(execution));
1322
+ if (execution.correctionCycle) nowRow(grid, 'Correção', 'ciclo ' + execution.correctionCycle);
1323
+ if (execution.verdict?.status) nowRow(grid, 'Veredito', execution.verdict.status);
1324
+ item.appendChild(grid);
1325
+ if (execution.failure?.message) item.appendChild(el('p', 'item-error', execution.failure.message));
1326
+ timeline.appendChild(item);
1327
+ }
1328
+ body.appendChild(timeline);
1329
+ });
1330
+ }
1331
+
1332
+ function renderProcessLogs(snapshot, executions, phase) {
1333
+ const ids = new Set(executions.map((execution) => execution.id));
1334
+ const logs = list(snapshot.processLogs).filter(
1335
+ (entry) => ids.has(entry.executionId) || (ids.size === 0 && entry.phase === phase),
1336
+ );
1337
+ drawerSection('Saída do processo', (body) => {
1338
+ const details = el('details', 'process-output');
1339
+ const summary = el('summary', null, logs.length + ' linha(s) sanitizada(s)');
1340
+ details.appendChild(summary);
1341
+ if (logs.length === 0) {
1342
+ details.appendChild(el('p', 'empty', 'Nenhuma saída capturada.'));
1343
+ } else {
1344
+ const output = el('pre', 'process-output-body');
1345
+ output.textContent = logs
1346
+ .slice(-200)
1347
+ .map((entry) => formatClock(entry.at) + ' ' + entry.message)
1348
+ .join('\n');
1349
+ details.appendChild(output);
1350
+ }
1351
+ body.appendChild(details);
1352
+ });
1353
+ }
1354
+
1355
+ function renderGlobalDiagnostics(kind, id, executions) {
1356
+ const ids = new Set(executions.map((execution) => execution.id));
1357
+ const entries = state.diagnostics.filter((entry) => {
1358
+ if (ids.has(entry.executionId)) return true;
1359
+ if (kind === 'phase') return entry.phase === id;
1360
+ return typeof entry.story === 'string' && entry.story.split(',').includes(id);
1361
+ });
1362
+ if (entries.length === 0) return;
1363
+ drawerSection('Diagnóstico global persistente', (body) => {
1364
+ const details = el('details', 'process-output');
1365
+ details.appendChild(el('summary', null, entries.length + ' registro(s) em ~/.issue-flow/logs'));
1366
+ const output = el('pre', 'process-output-body');
1367
+ output.textContent = entries
1368
+ .slice(0, 200)
1369
+ .map((entry) => formatClock(entry.timestamp) + ' ' + entry.level + ' ' + entry.message)
1370
+ .join('\n');
1371
+ details.appendChild(output);
1372
+ body.appendChild(details);
1373
+ });
1374
+ }
1375
+
1376
+ // Reidrata a partir de kind/id a cada render: o drawer sobrevive ao poll.
1001
1377
  function renderDrawer(snapshot) {
1002
- if (state.selectedStoryId === null) return;
1003
- const story = getStoryById(snapshot, state.selectedStoryId);
1004
- // A story saiu do plano: manter o drawer aberto exibiria dados obsoletos.
1005
- if (story === null) {
1378
+ if (state.selectedDetail === null) return;
1379
+ const kind = state.selectedDetail.kind;
1380
+ const id = state.selectedDetail.id;
1381
+ const story = kind === 'story' ? getStoryById(snapshot, id) : null;
1382
+ const phase = kind === 'phase' ? list(snapshot.phases).find((entry) => entry.name === id) : null;
1383
+ if ((kind === 'story' && story === null) || (kind === 'phase' && !phase)) {
1006
1384
  closeDrawer();
1007
1385
  return;
1008
1386
  }
1009
- els.drawerTitle.textContent = story.id + ' · ' + story.title;
1010
1387
 
1011
1388
  clear(els.drawerBody);
1389
+ if (kind === 'phase') {
1390
+ els.drawerTitle.textContent = 'Fase · ' + phase.name;
1391
+ els.drawerBody.appendChild(el('span', 'badge status-' + phase.status, phase.status));
1392
+ const grid = el('dl', 'now-grid drawer-summary-grid');
1393
+ nowRow(grid, 'Início', phase.startedAt ? formatClock(phase.startedAt) : '—');
1394
+ nowRow(grid, 'Fim', phase.endedAt ? formatClock(phase.endedAt) : '—');
1395
+ nowRow(grid, 'Duração', metric(phase.durationSeconds) !== null ? formatDuration(phase.durationSeconds) : '—');
1396
+ nowRow(grid, 'Uso total', formatUsage(phase) || '—');
1397
+ const configured = list(snapshot.configuration?.phases).find((entry) => entry.phase === phase.name);
1398
+ if (configured) {
1399
+ nowRow(grid, 'Harness efetivo', configured.provider.value || '—');
1400
+ nowRow(grid, 'Origem', configSourceLabel(configured.provider.source));
1401
+ nowRow(grid, 'Modelo efetivo', configured.model.value || 'default do provider');
1402
+ }
1403
+ els.drawerBody.appendChild(grid);
1404
+ if (phase.error) els.drawerBody.appendChild(el('p', 'item-error', phase.error));
1405
+ const executions = executionsFor(snapshot, kind, id);
1406
+ renderExecutionHistory(snapshot, executions);
1407
+ renderProcessLogs(snapshot, executions, phase.name);
1408
+ renderGlobalDiagnostics(kind, id, executions);
1409
+ return;
1410
+ }
1411
+
1412
+ els.drawerTitle.textContent = story.id + ' · ' + story.title;
1012
1413
  els.drawerBody.appendChild(
1013
1414
  el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
1014
1415
  );
@@ -1058,12 +1459,21 @@
1058
1459
  for (const entry of history) {
1059
1460
  const item = el('li');
1060
1461
  if (entry.at) item.appendChild(el('span', 'log-time', formatClock(entry.at)));
1061
- item.appendChild(document.createTextNode(text(entry.message)));
1462
+ item.appendChild(
1463
+ document.createTextNode(
1464
+ (STORY_STAGE_LABELS[entry.stage] || entry.stage || '') +
1465
+ (entry.detail ? ' · ' + entry.detail : ''),
1466
+ ),
1467
+ );
1062
1468
  entries.appendChild(item);
1063
1469
  }
1064
1470
  body.appendChild(entries);
1065
1471
  });
1066
1472
  }
1473
+ const executions = executionsFor(snapshot, kind, id);
1474
+ renderExecutionHistory(snapshot, executions);
1475
+ renderProcessLogs(snapshot, executions, 'execute');
1476
+ renderGlobalDiagnostics(kind, id, executions);
1067
1477
  }
1068
1478
 
1069
1479
  function drawerSection(title, fill) {
@@ -1073,8 +1483,8 @@
1073
1483
  els.drawerBody.appendChild(section);
1074
1484
  }
1075
1485
 
1076
- function openDrawer(id) {
1077
- state.selectedStoryId = id;
1486
+ function openDrawer(kind, id) {
1487
+ state.selectedDetail = { kind, id };
1078
1488
  els.drawer.hidden = false;
1079
1489
  els.drawerOverlay.hidden = false;
1080
1490
  document.addEventListener('keydown', onDrawerKeydown);
@@ -1083,15 +1493,18 @@
1083
1493
  }
1084
1494
 
1085
1495
  function closeDrawer() {
1086
- const id = state.selectedStoryId;
1087
- state.selectedStoryId = null;
1496
+ const selected = state.selectedDetail;
1497
+ state.selectedDetail = null;
1088
1498
  els.drawer.hidden = true;
1089
1499
  els.drawerOverlay.hidden = true;
1090
1500
  document.removeEventListener('keydown', onDrawerKeydown);
1091
1501
  clear(els.drawerBody);
1092
1502
  // O card é recriado a cada render, então o foco volta pelo id, não por uma
1093
1503
  // referência guardada na abertura.
1094
- const card = id ? els.kanban.querySelector('[data-story-id="' + id + '"]') : null;
1504
+ const card =
1505
+ selected && selected.kind === 'story'
1506
+ ? els.kanban.querySelector('[data-story-id="' + selected.id + '"]')
1507
+ : null;
1095
1508
  if (card) card.focus();
1096
1509
  }
1097
1510
 
@@ -1109,6 +1522,11 @@
1109
1522
  : el('span', 'mono commit-hash', commit.hash);
1110
1523
  item.appendChild(hash);
1111
1524
  const subject = el('div', 'item-main item-title', commit.subject);
1525
+ const meta = itemSideText([
1526
+ commit.storyId || '',
1527
+ commit.committedAt ? formatClock(commit.committedAt) : '',
1528
+ ]);
1529
+ if (meta) subject.appendChild(el('span', 'muted commit-meta', meta));
1112
1530
  item.appendChild(subject);
1113
1531
  els.commits.appendChild(item);
1114
1532
  }
@@ -1147,6 +1565,70 @@
1147
1565
  }
1148
1566
  }
1149
1567
 
1568
+ const RESILIENCE_EVENTS = new Set([
1569
+ 'retry',
1570
+ 'agent:attempt',
1571
+ 'agent:activity',
1572
+ 'agent:result',
1573
+ 'failover',
1574
+ ]);
1575
+
1576
+ function historyMessage(event) {
1577
+ switch (event.type) {
1578
+ case 'session:start':
1579
+ return 'Sessão iniciada';
1580
+ case 'session:end':
1581
+ return 'Sessão encerrada: ' + event.status;
1582
+ case 'phase:start':
1583
+ return 'Fase iniciada: ' + event.phase;
1584
+ case 'phase:end':
1585
+ return 'Fase encerrada: ' + event.phase + (event.success ? ' (ok)' : ' (falhou)');
1586
+ case 'iteration:start':
1587
+ return 'Iteração ' + event.iteration + ' iniciada';
1588
+ case 'iteration:end':
1589
+ return 'Iteração ' + event.iteration + ' encerrada';
1590
+ case 'retry':
1591
+ return 'Retry ' + event.attempt + (event.kind ? ': ' + event.kind : '');
1592
+ case 'agent:attempt':
1593
+ return 'Tentativa ' + event.attempt + ' com ' + event.provider;
1594
+ case 'agent:activity':
1595
+ return 'Atividade recebida de ' + event.provider;
1596
+ case 'agent:result':
1597
+ return event.provider + (event.success ? ' concluiu a tentativa' : ' falhou: ' + (event.failureKind || 'desconhecida'));
1598
+ case 'failover':
1599
+ return 'Failover de ' + event.from + ' para ' + event.to + (event.reason ? ': ' + event.reason : '');
1600
+ case 'correction:cycle':
1601
+ return 'Ciclo de correção ' + event.cycle + '/' + event.maxCycles;
1602
+ case 'log':
1603
+ return event.message;
1604
+ default:
1605
+ return event.type;
1606
+ }
1607
+ }
1608
+
1609
+ function renderHistory() {
1610
+ clear(els.history);
1611
+ const entries = state.events.filter((entry) => {
1612
+ const type = entry && entry.event && entry.event.type;
1613
+ if (!type) return false;
1614
+ if (state.historyFilter === 'resilience') return RESILIENCE_EVENTS.has(type);
1615
+ if (state.historyFilter === 'pipeline') return !RESILIENCE_EVENTS.has(type);
1616
+ return true;
1617
+ });
1618
+ if (entries.length === 0) {
1619
+ els.history.appendChild(el('p', 'empty', 'Nenhum evento para exibir.'));
1620
+ return;
1621
+ }
1622
+ for (const entry of entries.slice().reverse()) {
1623
+ const event = entry.event;
1624
+ const item = el('li', 'history-entry');
1625
+ item.appendChild(el('span', 'log-time', formatClock(event.at)));
1626
+ item.appendChild(el('span', 'history-type mono', event.type));
1627
+ item.appendChild(el('span', 'history-message', historyMessage(event)));
1628
+ els.history.appendChild(item);
1629
+ }
1630
+ }
1631
+
1150
1632
  function renderMeta(snapshot) {
1151
1633
  const parts = [];
1152
1634
  if (snapshot.sessionId) parts.push('sessão ' + snapshot.sessionId);
@@ -1184,6 +1666,11 @@
1184
1666
 
1185
1667
  // ---- Inicialização --------------------------------------------------------
1186
1668
 
1669
+ function onThemeChange(select) {
1670
+ setTheme(select.value);
1671
+ storeTheme(state.theme);
1672
+ }
1673
+
1187
1674
  function onRefreshChange(select) {
1188
1675
  state.refreshSeconds = Number(select.value);
1189
1676
  storeRefresh(state.refreshSeconds);
@@ -1209,10 +1696,22 @@
1209
1696
  );
1210
1697
  }
1211
1698
 
1699
+ els.themeSelect.addEventListener('change', () => onThemeChange(els.themeSelect));
1700
+ if (els.themeSelectDashboard) {
1701
+ els.themeSelectDashboard.addEventListener('change', () =>
1702
+ onThemeChange(els.themeSelectDashboard),
1703
+ );
1704
+ }
1705
+ initTheme();
1706
+
1212
1707
  els.logFilter.addEventListener('change', () => {
1213
1708
  state.logFilter = els.logFilter.value;
1214
1709
  if (state.snapshot) renderLogs(state.snapshot);
1215
1710
  });
1711
+ els.historyFilter.addEventListener('change', () => {
1712
+ state.historyFilter = els.historyFilter.value;
1713
+ renderHistory();
1714
+ });
1216
1715
 
1217
1716
  document.addEventListener('visibilitychange', () => {
1218
1717
  if (document.hidden) clearTimer();
@@ -1227,6 +1726,7 @@
1227
1726
  } else {
1228
1727
  try {
1229
1728
  const health = await fetch('api/health', { cache: 'no-store' }).then((r) => r.json());
1729
+ state.configWritable = list(health.capabilities).includes('config:agent:write');
1230
1730
  const suggested = Number(health.refreshSeconds);
1231
1731
  if (Number.isFinite(suggested) && suggested > 0) state.refreshSeconds = suggested;
1232
1732
  } catch (err) {