issue-flow 0.13.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 (162) hide show
  1. package/dist/agent-OX3KPHE4.js +30 -0
  2. package/dist/{analyze-WS47UYGN.js → analyze-WIND2SEB.js} +21 -18
  3. package/dist/{analyze-WS47UYGN.js.map → analyze-WIND2SEB.js.map} +1 -1
  4. package/dist/{apply-OS62UWRN.js → apply-JNOI2LR4.js} +14 -11
  5. package/dist/{apply-OS62UWRN.js.map → apply-JNOI2LR4.js.map} +1 -1
  6. package/dist/{bench-SA3BOME5.js → bench-B4VZON7E.js} +30 -26
  7. package/dist/{bench-SA3BOME5.js.map → bench-B4VZON7E.js.map} +1 -1
  8. package/dist/{chunk-PIVGJM6K.js → chunk-2IQ6M4ZE.js} +11 -11
  9. package/dist/{chunk-QCXPNSEL.js → chunk-3BWZO4YI.js} +2 -2
  10. package/dist/chunk-3WFZETIM.js +163 -0
  11. package/dist/chunk-3WFZETIM.js.map +1 -0
  12. package/dist/{chunk-6FDFRNXZ.js → chunk-67YIU2VB.js} +25 -20
  13. package/dist/chunk-67YIU2VB.js.map +1 -0
  14. package/dist/{chunk-TM67EHSV.js → chunk-6BENHR2I.js} +19 -19
  15. package/dist/chunk-6BENHR2I.js.map +1 -0
  16. package/dist/{chunk-IYV6KNVF.js → chunk-6QYMSNVA.js} +66 -240
  17. package/dist/chunk-6QYMSNVA.js.map +1 -0
  18. package/dist/{chunk-IV5SS4JN.js → chunk-76MMPJ53.js} +9 -7
  19. package/dist/{chunk-IV5SS4JN.js.map → chunk-76MMPJ53.js.map} +1 -1
  20. package/dist/{chunk-LZ6GFQAG.js → chunk-B72RQZBV.js} +35 -25
  21. package/dist/chunk-B72RQZBV.js.map +1 -0
  22. package/dist/{chunk-KQHB7ZT3.js → chunk-BEZ5BA5U.js} +82 -16
  23. package/dist/chunk-BEZ5BA5U.js.map +1 -0
  24. package/dist/{chunk-BQDQTIM7.js → chunk-BRCNQXYQ.js} +4 -4
  25. package/dist/{chunk-ARFVHC2F.js → chunk-CGKLE6WR.js} +115 -23
  26. package/dist/chunk-CGKLE6WR.js.map +1 -0
  27. package/dist/{chunk-BHJCGWQX.js → chunk-F6TOYSYY.js} +9 -9
  28. package/dist/chunk-G5EK2XRN.js +25 -0
  29. package/dist/chunk-G5EK2XRN.js.map +1 -0
  30. package/dist/{chunk-TX5WIVYP.js → chunk-HAKDLR6N.js} +6 -1
  31. package/dist/chunk-HAKDLR6N.js.map +1 -0
  32. package/dist/{chunk-DIG5XKQQ.js → chunk-J3SOTUVI.js} +2 -2
  33. package/dist/{chunk-SNTTOQAD.js → chunk-JN6D3MY6.js} +2 -2
  34. package/dist/{chunk-MIVHK5YI.js → chunk-KMRFRXIT.js} +3 -3
  35. package/dist/{chunk-5TB6KLC2.js → chunk-KT7JKOXB.js} +8 -8
  36. package/dist/{chunk-NEZZ6KDR.js → chunk-LBGHI4S2.js} +49 -2
  37. package/dist/chunk-LBGHI4S2.js.map +1 -0
  38. package/dist/{chunk-3XLCNVMD.js → chunk-LEW4QM74.js} +2 -2
  39. package/dist/{chunk-IHIEXUTC.js → chunk-LFEQIZLB.js} +85 -10
  40. package/dist/chunk-LFEQIZLB.js.map +1 -0
  41. package/dist/{chunk-TVN2JVOF.js → chunk-OOVG2KBY.js} +3 -3
  42. package/dist/{chunk-7KD66LIM.js → chunk-OR43364P.js} +3 -3
  43. package/dist/{chunk-J62QSVP6.js → chunk-PBTA3QQX.js} +8 -7
  44. package/dist/{chunk-J62QSVP6.js.map → chunk-PBTA3QQX.js.map} +1 -1
  45. package/dist/{chunk-664Y3NDX.js → chunk-PHW2YV6N.js} +8 -8
  46. package/dist/{chunk-PV4OX5TC.js → chunk-PZTWU277.js} +11 -11
  47. package/dist/{chunk-HJGGNLWI.js → chunk-QD4VOILZ.js} +9 -9
  48. package/dist/{chunk-CNBYCMSU.js → chunk-QJ5UZAH5.js} +27 -18
  49. package/dist/{chunk-CNBYCMSU.js.map → chunk-QJ5UZAH5.js.map} +1 -1
  50. package/dist/{chunk-HQAHNIAR.js → chunk-QK7WUO7A.js} +14 -14
  51. package/dist/{chunk-UJ7XWK7J.js → chunk-QZVKRDJY.js} +16 -14
  52. package/dist/{chunk-UJ7XWK7J.js.map → chunk-QZVKRDJY.js.map} +1 -1
  53. package/dist/{chunk-FZDR7CGH.js → chunk-REO3PXXO.js} +3 -3
  54. package/dist/{chunk-UOCPC2Y2.js → chunk-RVO7L4FX.js} +4 -4
  55. package/dist/{chunk-QXDNBYQF.js → chunk-SZMHCEYV.js} +21 -10
  56. package/dist/chunk-SZMHCEYV.js.map +1 -0
  57. package/dist/chunk-TYRNTSPJ.js +243 -0
  58. package/dist/chunk-TYRNTSPJ.js.map +1 -0
  59. package/dist/{agent-BXRIEKVM.js → chunk-UJ3L2MS7.js} +22 -22
  60. package/dist/chunk-UJ3L2MS7.js.map +1 -0
  61. package/dist/{chunk-37N2RJW2.js → chunk-V7MO5EDS.js} +3 -3
  62. package/dist/{chunk-HEF2RMKN.js → chunk-WB2X37MO.js} +18 -16
  63. package/dist/{chunk-HEF2RMKN.js.map → chunk-WB2X37MO.js.map} +1 -1
  64. package/dist/{chunk-OPNGPUKJ.js → chunk-X44FKNKP.js} +133 -42
  65. package/dist/chunk-X44FKNKP.js.map +1 -0
  66. package/dist/cli.js +43 -40
  67. package/dist/cli.js.map +1 -1
  68. package/dist/{config-MEM2IWWN.js → config-HMFMCY2M.js} +12 -9
  69. package/dist/{conventions-WV4NDXGH.js → conventions-NJF2OPVQ.js} +14 -11
  70. package/dist/{conventions-WV4NDXGH.js.map → conventions-NJF2OPVQ.js.map} +1 -1
  71. package/dist/diagnostics-THH42ZMP.js +23 -0
  72. package/dist/execute-KFNNZZHI.js +35 -0
  73. package/dist/{generate-MVQI7A45.js → generate-FW5C6NP7.js} +23 -20
  74. package/dist/{generate-MVQI7A45.js.map → generate-FW5C6NP7.js.map} +1 -1
  75. package/dist/{git-YRVRUN3C.js → git-W72QQ6GT.js} +4 -2
  76. package/dist/init-HTN4V67Q.js +24 -0
  77. package/dist/{operations-LU6JA3BX.js → operations-BCFOBH42.js} +17 -14
  78. package/dist/{operations-LU6JA3BX.js.map → operations-BCFOBH42.js.map} +1 -1
  79. package/dist/{permissions-7NMDFU27.js → permissions-4AKNQZHK.js} +4 -4
  80. package/dist/plan-SIZLGA4V.js +34 -0
  81. package/dist/{policy-JSYEK5B2.js → policy-DT6Q6ZVM.js} +13 -10
  82. package/dist/{policy-JSYEK5B2.js.map → policy-DT6Q6ZVM.js.map} +1 -1
  83. package/dist/pr-OMWCGUOW.js +39 -0
  84. package/dist/pr-review-N24CZPKR.js +32 -0
  85. package/dist/prd-5OFTZAHJ.js +33 -0
  86. package/dist/{ps-TNE44R5V.js → ps-RFORTCUD.js} +11 -8
  87. package/dist/{ps-TNE44R5V.js.map → ps-RFORTCUD.js.map} +1 -1
  88. package/dist/{recorder-XGF55WXI.js → recorder-KCCPMKQG.js} +5 -4
  89. package/dist/registry-GLPXOMFG.js +15 -0
  90. package/dist/{resume-QMEPMSZW.js → resume-M5RLKP4I.js} +44 -40
  91. package/dist/{resume-QMEPMSZW.js.map → resume-M5RLKP4I.js.map} +1 -1
  92. package/dist/review-YM4JAABY.js +35 -0
  93. package/dist/{routing-2ZUMZPJB.js → routing-NNT5WKGX.js} +13 -10
  94. package/dist/{routing-2ZUMZPJB.js.map → routing-NNT5WKGX.js.map} +1 -1
  95. package/dist/run-PZUKHPQK.js +54 -0
  96. package/dist/run-PZUKHPQK.js.map +1 -0
  97. package/dist/session-publisher-T7EI3CIP.js +11 -0
  98. package/dist/session-publisher-T7EI3CIP.js.map +1 -0
  99. package/dist/{state-manager-X3PT3FGT.js → state-manager-GQH32C44.js} +3 -3
  100. package/dist/state-manager-GQH32C44.js.map +1 -0
  101. package/dist/{usage-VCFLDA5Y.js → usage-V2CHE5PY.js} +13 -10
  102. package/dist/{usage-VCFLDA5Y.js.map → usage-V2CHE5PY.js.map} +1 -1
  103. package/dist/{web-XVHLX62B.js → web-EA3KGNFN.js} +18 -14
  104. package/dist/{web-XVHLX62B.js.map → web-EA3KGNFN.js.map} +1 -1
  105. package/package.json +1 -1
  106. package/web/AGENTS.md +150 -11
  107. package/web/public/app.css +343 -110
  108. package/web/public/app.js +408 -18
  109. package/web/public/index.html +44 -1
  110. package/dist/agent-BXRIEKVM.js.map +0 -1
  111. package/dist/chunk-6FDFRNXZ.js.map +0 -1
  112. package/dist/chunk-ARFVHC2F.js.map +0 -1
  113. package/dist/chunk-IHIEXUTC.js.map +0 -1
  114. package/dist/chunk-IYV6KNVF.js.map +0 -1
  115. package/dist/chunk-KQHB7ZT3.js.map +0 -1
  116. package/dist/chunk-LZ6GFQAG.js.map +0 -1
  117. package/dist/chunk-NEZZ6KDR.js.map +0 -1
  118. package/dist/chunk-OPNGPUKJ.js.map +0 -1
  119. package/dist/chunk-QXDNBYQF.js.map +0 -1
  120. package/dist/chunk-TM67EHSV.js.map +0 -1
  121. package/dist/chunk-TX5WIVYP.js.map +0 -1
  122. package/dist/execute-NITIHHCK.js +0 -31
  123. package/dist/init-OKWEC3EV.js +0 -21
  124. package/dist/plan-LEJJF3DE.js +0 -31
  125. package/dist/pr-HPVUZ7S3.js +0 -36
  126. package/dist/pr-review-R2QZBT3F.js +0 -29
  127. package/dist/prd-5O6GYCC6.js +0 -30
  128. package/dist/registry-LGPE53QD.js +0 -15
  129. package/dist/review-YKWD6SNN.js +0 -32
  130. package/dist/run-YU4TR62R.js +0 -50
  131. /package/dist/{config-MEM2IWWN.js.map → agent-OX3KPHE4.js.map} +0 -0
  132. /package/dist/{chunk-PIVGJM6K.js.map → chunk-2IQ6M4ZE.js.map} +0 -0
  133. /package/dist/{chunk-QCXPNSEL.js.map → chunk-3BWZO4YI.js.map} +0 -0
  134. /package/dist/{chunk-BQDQTIM7.js.map → chunk-BRCNQXYQ.js.map} +0 -0
  135. /package/dist/{chunk-BHJCGWQX.js.map → chunk-F6TOYSYY.js.map} +0 -0
  136. /package/dist/{chunk-DIG5XKQQ.js.map → chunk-J3SOTUVI.js.map} +0 -0
  137. /package/dist/{chunk-SNTTOQAD.js.map → chunk-JN6D3MY6.js.map} +0 -0
  138. /package/dist/{chunk-MIVHK5YI.js.map → chunk-KMRFRXIT.js.map} +0 -0
  139. /package/dist/{chunk-5TB6KLC2.js.map → chunk-KT7JKOXB.js.map} +0 -0
  140. /package/dist/{chunk-3XLCNVMD.js.map → chunk-LEW4QM74.js.map} +0 -0
  141. /package/dist/{chunk-TVN2JVOF.js.map → chunk-OOVG2KBY.js.map} +0 -0
  142. /package/dist/{chunk-7KD66LIM.js.map → chunk-OR43364P.js.map} +0 -0
  143. /package/dist/{chunk-664Y3NDX.js.map → chunk-PHW2YV6N.js.map} +0 -0
  144. /package/dist/{chunk-PV4OX5TC.js.map → chunk-PZTWU277.js.map} +0 -0
  145. /package/dist/{chunk-HJGGNLWI.js.map → chunk-QD4VOILZ.js.map} +0 -0
  146. /package/dist/{chunk-HQAHNIAR.js.map → chunk-QK7WUO7A.js.map} +0 -0
  147. /package/dist/{chunk-FZDR7CGH.js.map → chunk-REO3PXXO.js.map} +0 -0
  148. /package/dist/{chunk-UOCPC2Y2.js.map → chunk-RVO7L4FX.js.map} +0 -0
  149. /package/dist/{chunk-37N2RJW2.js.map → chunk-V7MO5EDS.js.map} +0 -0
  150. /package/dist/{execute-NITIHHCK.js.map → config-HMFMCY2M.js.map} +0 -0
  151. /package/dist/{git-YRVRUN3C.js.map → diagnostics-THH42ZMP.js.map} +0 -0
  152. /package/dist/{init-OKWEC3EV.js.map → execute-KFNNZZHI.js.map} +0 -0
  153. /package/dist/{permissions-7NMDFU27.js.map → git-W72QQ6GT.js.map} +0 -0
  154. /package/dist/{plan-LEJJF3DE.js.map → init-HTN4V67Q.js.map} +0 -0
  155. /package/dist/{pr-HPVUZ7S3.js.map → permissions-4AKNQZHK.js.map} +0 -0
  156. /package/dist/{pr-review-R2QZBT3F.js.map → plan-SIZLGA4V.js.map} +0 -0
  157. /package/dist/{prd-5O6GYCC6.js.map → pr-OMWCGUOW.js.map} +0 -0
  158. /package/dist/{recorder-XGF55WXI.js.map → pr-review-N24CZPKR.js.map} +0 -0
  159. /package/dist/{registry-LGPE53QD.js.map → prd-5OFTZAHJ.js.map} +0 -0
  160. /package/dist/{review-YKWD6SNN.js.map → recorder-KCCPMKQG.js.map} +0 -0
  161. /package/dist/{run-YU4TR62R.js.map → registry-GLPXOMFG.js.map} +0 -0
  162. /package/dist/{state-manager-X3PT3FGT.js.map → review-YM4JAABY.js.map} +0 -0
package/web/public/app.js CHANGED
@@ -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'),
@@ -80,6 +88,7 @@
80
88
  progressCounters: document.getElementById('progress-counters'),
81
89
  now: document.getElementById('now'),
82
90
  resilience: document.getElementById('resilience'),
91
+ configuration: document.getElementById('configuration'),
83
92
  phases: document.getElementById('phases'),
84
93
  nextSteps: document.getElementById('next-steps'),
85
94
  stories: document.getElementById('stories'),
@@ -108,11 +117,16 @@
108
117
  polling: false,
109
118
  logFilter: 'all',
110
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',
111
123
  historyFilter: 'all',
112
124
  events: [],
125
+ diagnostics: [],
113
126
  // Só o id: o card que abriu o drawer é destruído no próximo render, então
114
127
  // guardar o nó levaria a uma referência morta.
115
- selectedStoryId: null,
128
+ selectedDetail: null,
129
+ configWritable: false,
116
130
  // Multi-sessão (#35): lista de /api/sessions e seleção explícita do usuário.
117
131
  // selectedSessionId null = modo automático (1 sessão → detalhe; 2+ → dashboard).
118
132
  sessions: [],
@@ -192,8 +206,9 @@
192
206
  }
193
207
 
194
208
  // Custos abaixo de um centavo perderiam todo o significado com 2 casas.
195
- function formatCost(value) {
196
- 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));
197
212
  }
198
213
 
199
214
  // Ex.: '12.4k in / 3.1k out · 88.0k cache · ~$0.42'. Segmentos sem dado são
@@ -307,6 +322,88 @@
307
322
  }
308
323
  }
309
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
+
310
407
  // ---- Polling: intervalo configurável, aba oculta e backoff ----------------
311
408
 
312
409
  function readStoredRefresh() {
@@ -390,6 +487,10 @@
390
487
  return { mode: 'detail', session: null };
391
488
  }
392
489
 
490
+ if (sessions.length === 1) {
491
+ return { mode: 'detail', session: sessions[0] };
492
+ }
493
+
393
494
  return { mode: 'dashboard', session: null };
394
495
  }
395
496
 
@@ -401,7 +502,7 @@
401
502
  const showBack =
402
503
  mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 1;
403
504
  els.backToDashboard.hidden = !showBack;
404
- if (mode === 'dashboard' && state.selectedStoryId !== null) {
505
+ if (mode === 'dashboard' && state.selectedDetail !== null) {
405
506
  closeDrawer();
406
507
  }
407
508
  }
@@ -423,6 +524,7 @@
423
524
  state.snapshot = null;
424
525
  state.detailSessionId = null;
425
526
  state.events = [];
527
+ state.diagnostics = [];
426
528
  state.eventsUrl = null;
427
529
  }
428
530
 
@@ -528,7 +630,15 @@
528
630
  if (!eventsRes.ok) throw new Error('HTTP ' + eventsRes.status);
529
631
  const entries = await eventsRes.json();
530
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
+ }
531
640
  renderHistory();
641
+ renderDrawer(state.snapshot);
532
642
  }
533
643
  setViewMode('detail');
534
644
  state.failures = 0;
@@ -689,6 +799,7 @@
689
799
  renderProgress(snapshot);
690
800
  renderNow(snapshot);
691
801
  renderResilience(snapshot);
802
+ renderConfiguration(snapshot);
692
803
  renderPhases(snapshot);
693
804
  renderNextSteps(snapshot);
694
805
  renderStories(snapshot);
@@ -724,7 +835,15 @@
724
835
 
725
836
  const branch = snapshot.git.branch;
726
837
  const base = snapshot.git.baseBranch;
727
- 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
+ : '';
728
847
 
729
848
  els.statusBadge.textContent = STATUS_LABELS[snapshot.status] || snapshot.status;
730
849
  els.statusBadge.className = 'badge status-' + snapshot.status;
@@ -899,6 +1018,105 @@
899
1018
  els.resilience.appendChild(grid);
900
1019
  }
901
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
+
902
1120
  function renderPhases(snapshot) {
903
1121
  clear(els.phases);
904
1122
  if (snapshot.phases.length === 0) {
@@ -907,6 +1125,7 @@
907
1125
  }
908
1126
  for (const phase of snapshot.phases) {
909
1127
  const item = el('li');
1128
+ item.className = 'detail-row';
910
1129
  item.appendChild(el('span', 'item-icon icon-' + phase.status, PHASE_ICONS[phase.status] || '○'));
911
1130
  const main = el('div', 'item-main');
912
1131
  main.appendChild(el('div', 'item-title', phase.name));
@@ -918,6 +1137,12 @@
918
1137
  formatUsage(phase),
919
1138
  ]);
920
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
+ });
921
1146
  els.phases.appendChild(item);
922
1147
  }
923
1148
  }
@@ -948,6 +1173,7 @@
948
1173
  // antes da issue 38 pode chegar sem ele.
949
1174
  const storyStage = story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
950
1175
  const item = el('li', storyStage === 'executing' ? 'story-executing' : null);
1176
+ item.classList.add('detail-row');
951
1177
  const status = story.passes ? 'completed' : 'pending';
952
1178
  item.appendChild(el('span', 'item-icon icon-' + status, story.passes ? '✓' : '○'));
953
1179
  const main = el('div', 'item-main');
@@ -982,6 +1208,12 @@
982
1208
  formatUsage(story),
983
1209
  ]);
984
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
+ });
985
1217
  els.stories.appendChild(item);
986
1218
  }
987
1219
  }
@@ -1008,7 +1240,7 @@
1008
1240
  card.appendChild(
1009
1241
  el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
1010
1242
  );
1011
- card.addEventListener('click', () => openDrawer(story.id));
1243
+ card.addEventListener('click', () => openDrawer('story', story.id));
1012
1244
  return card;
1013
1245
  }
1014
1246
 
@@ -1039,18 +1271,145 @@
1039
1271
  if (event.key === 'Escape') closeDrawer();
1040
1272
  }
1041
1273
 
1042
- // 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.
1043
1377
  function renderDrawer(snapshot) {
1044
- if (state.selectedStoryId === null) return;
1045
- const story = getStoryById(snapshot, state.selectedStoryId);
1046
- // A story saiu do plano: manter o drawer aberto exibiria dados obsoletos.
1047
- 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)) {
1048
1384
  closeDrawer();
1049
1385
  return;
1050
1386
  }
1051
- els.drawerTitle.textContent = story.id + ' · ' + story.title;
1052
1387
 
1053
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;
1054
1413
  els.drawerBody.appendChild(
1055
1414
  el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
1056
1415
  );
@@ -1100,12 +1459,21 @@
1100
1459
  for (const entry of history) {
1101
1460
  const item = el('li');
1102
1461
  if (entry.at) item.appendChild(el('span', 'log-time', formatClock(entry.at)));
1103
- 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
+ );
1104
1468
  entries.appendChild(item);
1105
1469
  }
1106
1470
  body.appendChild(entries);
1107
1471
  });
1108
1472
  }
1473
+ const executions = executionsFor(snapshot, kind, id);
1474
+ renderExecutionHistory(snapshot, executions);
1475
+ renderProcessLogs(snapshot, executions, 'execute');
1476
+ renderGlobalDiagnostics(kind, id, executions);
1109
1477
  }
1110
1478
 
1111
1479
  function drawerSection(title, fill) {
@@ -1115,8 +1483,8 @@
1115
1483
  els.drawerBody.appendChild(section);
1116
1484
  }
1117
1485
 
1118
- function openDrawer(id) {
1119
- state.selectedStoryId = id;
1486
+ function openDrawer(kind, id) {
1487
+ state.selectedDetail = { kind, id };
1120
1488
  els.drawer.hidden = false;
1121
1489
  els.drawerOverlay.hidden = false;
1122
1490
  document.addEventListener('keydown', onDrawerKeydown);
@@ -1125,15 +1493,18 @@
1125
1493
  }
1126
1494
 
1127
1495
  function closeDrawer() {
1128
- const id = state.selectedStoryId;
1129
- state.selectedStoryId = null;
1496
+ const selected = state.selectedDetail;
1497
+ state.selectedDetail = null;
1130
1498
  els.drawer.hidden = true;
1131
1499
  els.drawerOverlay.hidden = true;
1132
1500
  document.removeEventListener('keydown', onDrawerKeydown);
1133
1501
  clear(els.drawerBody);
1134
1502
  // O card é recriado a cada render, então o foco volta pelo id, não por uma
1135
1503
  // referência guardada na abertura.
1136
- 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;
1137
1508
  if (card) card.focus();
1138
1509
  }
1139
1510
 
@@ -1151,6 +1522,11 @@
1151
1522
  : el('span', 'mono commit-hash', commit.hash);
1152
1523
  item.appendChild(hash);
1153
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));
1154
1530
  item.appendChild(subject);
1155
1531
  els.commits.appendChild(item);
1156
1532
  }
@@ -1290,6 +1666,11 @@
1290
1666
 
1291
1667
  // ---- Inicialização --------------------------------------------------------
1292
1668
 
1669
+ function onThemeChange(select) {
1670
+ setTheme(select.value);
1671
+ storeTheme(state.theme);
1672
+ }
1673
+
1293
1674
  function onRefreshChange(select) {
1294
1675
  state.refreshSeconds = Number(select.value);
1295
1676
  storeRefresh(state.refreshSeconds);
@@ -1315,6 +1696,14 @@
1315
1696
  );
1316
1697
  }
1317
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
+
1318
1707
  els.logFilter.addEventListener('change', () => {
1319
1708
  state.logFilter = els.logFilter.value;
1320
1709
  if (state.snapshot) renderLogs(state.snapshot);
@@ -1337,6 +1726,7 @@
1337
1726
  } else {
1338
1727
  try {
1339
1728
  const health = await fetch('api/health', { cache: 'no-store' }).then((r) => r.json());
1729
+ state.configWritable = list(health.capabilities).includes('config:agent:write');
1340
1730
  const suggested = Number(health.refreshSeconds);
1341
1731
  if (Number.isFinite(suggested) && suggested > 0) state.refreshSeconds = suggested;
1342
1732
  } catch (err) {