issue-flow 0.13.0 → 0.15.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 (163) hide show
  1. package/dist/agent-AT64RXTA.js +30 -0
  2. package/dist/{analyze-WS47UYGN.js → analyze-O2PYDIFK.js} +21 -18
  3. package/dist/{analyze-WS47UYGN.js.map → analyze-O2PYDIFK.js.map} +1 -1
  4. package/dist/{apply-OS62UWRN.js → apply-SSU6QCD4.js} +14 -11
  5. package/dist/{apply-OS62UWRN.js.map → apply-SSU6QCD4.js.map} +1 -1
  6. package/dist/{bench-SA3BOME5.js → bench-ODFPAGW6.js} +30 -26
  7. package/dist/{bench-SA3BOME5.js.map → bench-ODFPAGW6.js.map} +1 -1
  8. package/dist/{chunk-HJGGNLWI.js → chunk-2ZAM45PZ.js} +9 -9
  9. package/dist/{chunk-QCXPNSEL.js → chunk-3BWZO4YI.js} +2 -2
  10. package/dist/{chunk-J62QSVP6.js → chunk-4RXYFWP7.js} +8 -7
  11. package/dist/{chunk-J62QSVP6.js.map → chunk-4RXYFWP7.js.map} +1 -1
  12. package/dist/{chunk-ARFVHC2F.js → chunk-5ZKBIXET.js} +115 -23
  13. package/dist/chunk-5ZKBIXET.js.map +1 -0
  14. package/dist/{chunk-HEF2RMKN.js → chunk-6E2PEBFT.js} +18 -16
  15. package/dist/{chunk-HEF2RMKN.js.map → chunk-6E2PEBFT.js.map} +1 -1
  16. package/dist/{agent-BXRIEKVM.js → chunk-6FXOU2KR.js} +22 -22
  17. package/dist/chunk-6FXOU2KR.js.map +1 -0
  18. package/dist/{chunk-IYV6KNVF.js → chunk-6QYMSNVA.js} +66 -240
  19. package/dist/chunk-6QYMSNVA.js.map +1 -0
  20. package/dist/{chunk-UOCPC2Y2.js → chunk-6SRE4KD7.js} +4 -4
  21. package/dist/{chunk-MIVHK5YI.js → chunk-A35HE4V7.js} +3 -3
  22. package/dist/{chunk-TM67EHSV.js → chunk-ARWNYXFL.js} +19 -19
  23. package/dist/chunk-ARWNYXFL.js.map +1 -0
  24. package/dist/chunk-AYYMW33T.js +163 -0
  25. package/dist/chunk-AYYMW33T.js.map +1 -0
  26. package/dist/chunk-BMHGRYL6.js +890 -0
  27. package/dist/chunk-BMHGRYL6.js.map +1 -0
  28. package/dist/{chunk-PV4OX5TC.js → chunk-F4OK7KEP.js} +11 -11
  29. package/dist/{chunk-QXDNBYQF.js → chunk-FK4IDY6P.js} +21 -10
  30. package/dist/chunk-FK4IDY6P.js.map +1 -0
  31. package/dist/{chunk-KQHB7ZT3.js → chunk-GSZCQUCY.js} +82 -16
  32. package/dist/chunk-GSZCQUCY.js.map +1 -0
  33. package/dist/{chunk-TX5WIVYP.js → chunk-HAKDLR6N.js} +6 -1
  34. package/dist/chunk-HAKDLR6N.js.map +1 -0
  35. package/dist/{chunk-OPNGPUKJ.js → chunk-HGSYD57B.js} +143 -48
  36. package/dist/chunk-HGSYD57B.js.map +1 -0
  37. package/dist/{chunk-PIVGJM6K.js → chunk-KAHQ6F5C.js} +11 -11
  38. package/dist/{chunk-BHJCGWQX.js → chunk-MEFY4B7T.js} +9 -9
  39. package/dist/{chunk-BQDQTIM7.js → chunk-MGCOQU6N.js} +4 -4
  40. package/dist/{chunk-5TB6KLC2.js → chunk-MROS3SKL.js} +8 -8
  41. package/dist/chunk-NVZMBXTE.js +243 -0
  42. package/dist/chunk-NVZMBXTE.js.map +1 -0
  43. package/dist/{chunk-664Y3NDX.js → chunk-PCN7ZQND.js} +8 -8
  44. package/dist/{chunk-IV5SS4JN.js → chunk-QRCGW2ZO.js} +9 -7
  45. package/dist/{chunk-IV5SS4JN.js.map → chunk-QRCGW2ZO.js.map} +1 -1
  46. package/dist/{chunk-7KD66LIM.js → chunk-RPXDBX6R.js} +3 -3
  47. package/dist/{chunk-SNTTOQAD.js → chunk-RWEXK55D.js} +2 -2
  48. package/dist/{chunk-6FDFRNXZ.js → chunk-TBDP6QPJ.js} +25 -20
  49. package/dist/chunk-TBDP6QPJ.js.map +1 -0
  50. package/dist/chunk-UNQYMR3H.js +25 -0
  51. package/dist/chunk-UNQYMR3H.js.map +1 -0
  52. package/dist/{chunk-HQAHNIAR.js → chunk-VR34TXHF.js} +14 -14
  53. package/dist/{chunk-CNBYCMSU.js → chunk-WM3Y7Q4A.js} +27 -18
  54. package/dist/{chunk-CNBYCMSU.js.map → chunk-WM3Y7Q4A.js.map} +1 -1
  55. package/dist/{chunk-NEZZ6KDR.js → chunk-XDKGA6UX.js} +52 -3
  56. package/dist/chunk-XDKGA6UX.js.map +1 -0
  57. package/dist/{chunk-FZDR7CGH.js → chunk-XG2NYDRA.js} +3 -3
  58. package/dist/{chunk-3XLCNVMD.js → chunk-XLVGPXNL.js} +2 -2
  59. package/dist/{chunk-DIG5XKQQ.js → chunk-XQHFC62U.js} +2 -2
  60. package/dist/{chunk-UJ7XWK7J.js → chunk-XRLRKTQA.js} +16 -14
  61. package/dist/{chunk-UJ7XWK7J.js.map → chunk-XRLRKTQA.js.map} +1 -1
  62. package/dist/{chunk-LZ6GFQAG.js → chunk-ZFIXEYUJ.js} +44 -31
  63. package/dist/chunk-ZFIXEYUJ.js.map +1 -0
  64. package/dist/{chunk-37N2RJW2.js → chunk-ZPIVCM7Q.js} +3 -3
  65. package/dist/{chunk-TVN2JVOF.js → chunk-ZXPASFHG.js} +3 -3
  66. package/dist/cli.js +58 -64
  67. package/dist/cli.js.map +1 -1
  68. package/dist/{config-MEM2IWWN.js → config-UMP6DBJB.js} +12 -9
  69. package/dist/{conventions-WV4NDXGH.js → conventions-DKH7AE4T.js} +14 -11
  70. package/dist/{conventions-WV4NDXGH.js.map → conventions-DKH7AE4T.js.map} +1 -1
  71. package/dist/diagnostics-GOGCLTRS.js +23 -0
  72. package/dist/execute-E3BRMDAQ.js +35 -0
  73. package/dist/{generate-MVQI7A45.js → generate-AGBT7EHX.js} +23 -20
  74. package/dist/{generate-MVQI7A45.js.map → generate-AGBT7EHX.js.map} +1 -1
  75. package/dist/{git-YRVRUN3C.js → git-AC542GU2.js} +4 -2
  76. package/dist/init-LLBVJNPE.js +24 -0
  77. package/dist/{operations-LU6JA3BX.js → operations-JQYK2EMT.js} +17 -14
  78. package/dist/{operations-LU6JA3BX.js.map → operations-JQYK2EMT.js.map} +1 -1
  79. package/dist/{permissions-7NMDFU27.js → permissions-6HL52K54.js} +4 -4
  80. package/dist/plan-HSLWDJEZ.js +34 -0
  81. package/dist/{policy-JSYEK5B2.js → policy-IMRXGG7X.js} +13 -10
  82. package/dist/{policy-JSYEK5B2.js.map → policy-IMRXGG7X.js.map} +1 -1
  83. package/dist/pr-6TCZXG6K.js +39 -0
  84. package/dist/pr-review-6MWJO2NP.js +32 -0
  85. package/dist/prd-D6YDYAFX.js +33 -0
  86. package/dist/{ps-TNE44R5V.js → ps-KUUZ6TUL.js} +11 -8
  87. package/dist/{ps-TNE44R5V.js.map → ps-KUUZ6TUL.js.map} +1 -1
  88. package/dist/{recorder-XGF55WXI.js → recorder-BJ2KIAWT.js} +5 -4
  89. package/dist/registry-SB7XTMGE.js +15 -0
  90. package/dist/{resume-QMEPMSZW.js → resume-QNLQSCR5.js} +44 -40
  91. package/dist/{resume-QMEPMSZW.js.map → resume-QNLQSCR5.js.map} +1 -1
  92. package/dist/review-3KJ4BH2V.js +35 -0
  93. package/dist/{routing-2ZUMZPJB.js → routing-TN7GLMGU.js} +13 -10
  94. package/dist/{routing-2ZUMZPJB.js.map → routing-TN7GLMGU.js.map} +1 -1
  95. package/dist/run-5EHIFF2V.js +54 -0
  96. package/dist/run-5EHIFF2V.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-ZEPWTCA6.js} +3 -3
  100. package/dist/state-manager-ZEPWTCA6.js.map +1 -0
  101. package/dist/{usage-VCFLDA5Y.js → usage-QL563I7F.js} +13 -10
  102. package/dist/{usage-VCFLDA5Y.js.map → usage-QL563I7F.js.map} +1 -1
  103. package/dist/{web-XVHLX62B.js → web-PPSMNTQ6.js} +22 -57
  104. package/dist/{web-XVHLX62B.js.map → web-PPSMNTQ6.js.map} +1 -1
  105. package/package.json +1 -1
  106. package/web/AGENTS.md +154 -11
  107. package/web/public/app.css +343 -110
  108. package/web/public/app.js +436 -27
  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 +0 -483
  114. package/dist/chunk-IHIEXUTC.js.map +0 -1
  115. package/dist/chunk-IYV6KNVF.js.map +0 -1
  116. package/dist/chunk-KQHB7ZT3.js.map +0 -1
  117. package/dist/chunk-LZ6GFQAG.js.map +0 -1
  118. package/dist/chunk-NEZZ6KDR.js.map +0 -1
  119. package/dist/chunk-OPNGPUKJ.js.map +0 -1
  120. package/dist/chunk-QXDNBYQF.js.map +0 -1
  121. package/dist/chunk-TM67EHSV.js.map +0 -1
  122. package/dist/chunk-TX5WIVYP.js.map +0 -1
  123. package/dist/execute-NITIHHCK.js +0 -31
  124. package/dist/init-OKWEC3EV.js +0 -21
  125. package/dist/plan-LEJJF3DE.js +0 -31
  126. package/dist/pr-HPVUZ7S3.js +0 -36
  127. package/dist/pr-review-R2QZBT3F.js +0 -29
  128. package/dist/prd-5O6GYCC6.js +0 -30
  129. package/dist/registry-LGPE53QD.js +0 -15
  130. package/dist/review-YKWD6SNN.js +0 -32
  131. package/dist/run-YU4TR62R.js +0 -50
  132. /package/dist/{config-MEM2IWWN.js.map → agent-AT64RXTA.js.map} +0 -0
  133. /package/dist/{chunk-HJGGNLWI.js.map → chunk-2ZAM45PZ.js.map} +0 -0
  134. /package/dist/{chunk-QCXPNSEL.js.map → chunk-3BWZO4YI.js.map} +0 -0
  135. /package/dist/{chunk-UOCPC2Y2.js.map → chunk-6SRE4KD7.js.map} +0 -0
  136. /package/dist/{chunk-MIVHK5YI.js.map → chunk-A35HE4V7.js.map} +0 -0
  137. /package/dist/{chunk-PV4OX5TC.js.map → chunk-F4OK7KEP.js.map} +0 -0
  138. /package/dist/{chunk-PIVGJM6K.js.map → chunk-KAHQ6F5C.js.map} +0 -0
  139. /package/dist/{chunk-BHJCGWQX.js.map → chunk-MEFY4B7T.js.map} +0 -0
  140. /package/dist/{chunk-BQDQTIM7.js.map → chunk-MGCOQU6N.js.map} +0 -0
  141. /package/dist/{chunk-5TB6KLC2.js.map → chunk-MROS3SKL.js.map} +0 -0
  142. /package/dist/{chunk-664Y3NDX.js.map → chunk-PCN7ZQND.js.map} +0 -0
  143. /package/dist/{chunk-7KD66LIM.js.map → chunk-RPXDBX6R.js.map} +0 -0
  144. /package/dist/{chunk-SNTTOQAD.js.map → chunk-RWEXK55D.js.map} +0 -0
  145. /package/dist/{chunk-HQAHNIAR.js.map → chunk-VR34TXHF.js.map} +0 -0
  146. /package/dist/{chunk-FZDR7CGH.js.map → chunk-XG2NYDRA.js.map} +0 -0
  147. /package/dist/{chunk-3XLCNVMD.js.map → chunk-XLVGPXNL.js.map} +0 -0
  148. /package/dist/{chunk-DIG5XKQQ.js.map → chunk-XQHFC62U.js.map} +0 -0
  149. /package/dist/{chunk-37N2RJW2.js.map → chunk-ZPIVCM7Q.js.map} +0 -0
  150. /package/dist/{chunk-TVN2JVOF.js.map → chunk-ZXPASFHG.js.map} +0 -0
  151. /package/dist/{execute-NITIHHCK.js.map → config-UMP6DBJB.js.map} +0 -0
  152. /package/dist/{git-YRVRUN3C.js.map → diagnostics-GOGCLTRS.js.map} +0 -0
  153. /package/dist/{init-OKWEC3EV.js.map → execute-E3BRMDAQ.js.map} +0 -0
  154. /package/dist/{permissions-7NMDFU27.js.map → git-AC542GU2.js.map} +0 -0
  155. /package/dist/{plan-LEJJF3DE.js.map → init-LLBVJNPE.js.map} +0 -0
  156. /package/dist/{pr-HPVUZ7S3.js.map → permissions-6HL52K54.js.map} +0 -0
  157. /package/dist/{pr-review-R2QZBT3F.js.map → plan-HSLWDJEZ.js.map} +0 -0
  158. /package/dist/{prd-5O6GYCC6.js.map → pr-6TCZXG6K.js.map} +0 -0
  159. /package/dist/{recorder-XGF55WXI.js.map → pr-review-6MWJO2NP.js.map} +0 -0
  160. /package/dist/{registry-LGPE53QD.js.map → prd-D6YDYAFX.js.map} +0 -0
  161. /package/dist/{review-YKWD6SNN.js.map → recorder-BJ2KIAWT.js.map} +0 -0
  162. /package/dist/{run-YU4TR62R.js.map → registry-SB7XTMGE.js.map} +0 -0
  163. /package/dist/{state-manager-X3PT3FGT.js.map → review-3KJ4BH2V.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,19 @@
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,
130
+ // Identifica o processo que serviu os assets atuais. Uma troca significa
131
+ // que --restart-web colocou código novo no mesmo origin e exige reload.
132
+ serverInstanceId: null,
116
133
  // Multi-sessão (#35): lista de /api/sessions e seleção explícita do usuário.
117
134
  // selectedSessionId null = modo automático (1 sessão → detalhe; 2+ → dashboard).
118
135
  sessions: [],
@@ -192,8 +209,9 @@
192
209
  }
193
210
 
194
211
  // 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));
212
+ function formatCost(value, approximate) {
213
+ const prefix = approximate === false ? '$' : '~$';
214
+ return prefix + (Math.abs(value) < 0.01 ? value.toFixed(4) : value.toFixed(2));
197
215
  }
198
216
 
199
217
  // Ex.: '12.4k in / 3.1k out · 88.0k cache · ~$0.42'. Segmentos sem dado são
@@ -307,6 +325,88 @@
307
325
  }
308
326
  }
309
327
 
328
+ // ---- Tema ------------------------------------------------------------------
329
+ // Três estados: 'system' segue o SO (o @media decide), 'light'/'dark' forçam.
330
+ // Quem pinta é o CSS; aqui só se define (ou remove) o data-theme da raiz.
331
+
332
+ // Par ler/gravar tolerante a exceção, na mesma forma de readStoredRefresh()
333
+ // e storeRefresh(): armazenamento bloqueado (janela privada, cookies de
334
+ // terceiros desligados) não pode derrubar o painel.
335
+ function readStoredTheme() {
336
+ let raw = null;
337
+ try {
338
+ raw = window.localStorage.getItem(THEME_STORAGE_KEY);
339
+ } catch (err) {
340
+ // localStorage indisponível (ex.: bloqueado) — segue no modo sistema.
341
+ }
342
+ return raw === 'light' || raw === 'dark' ? raw : 'system';
343
+ }
344
+
345
+ function storeTheme(value) {
346
+ try {
347
+ window.localStorage.setItem(THEME_STORAGE_KEY, value);
348
+ } catch (err) {
349
+ // Persistência é conveniência; falha é ignorada. Sem gravar, a escolha
350
+ // continua valendo nesta aba — só não sobrevive ao reload.
351
+ }
352
+ }
353
+
354
+ function applyTheme(theme) {
355
+ // 'system' **remove** o atributo em vez de gravar 'system': é a ausência
356
+ // do data-theme que devolve a decisão ao @media (prefers-color-scheme).
357
+ if (theme === 'light' || theme === 'dark') {
358
+ document.documentElement.setAttribute('data-theme', theme);
359
+ } else {
360
+ document.documentElement.removeAttribute('data-theme');
361
+ }
362
+ }
363
+
364
+ // Os dois headers têm o seu seletor; mudar num precisa refletir no outro.
365
+ // As três opções são estáticas no HTML — aqui só se sincroniza o .value.
366
+ function syncThemeSelects() {
367
+ if (els.themeSelect) els.themeSelect.value = state.theme;
368
+ if (els.themeSelectDashboard) els.themeSelectDashboard.value = state.theme;
369
+ }
370
+
371
+ // Preferência do SO. Só se observa no modo 'system': é ali que a troca de
372
+ // claro para escuro no SO precisa chegar ao painel com a página aberta.
373
+ const systemThemeQuery =
374
+ typeof window.matchMedia === 'function'
375
+ ? window.matchMedia('(prefers-color-scheme: dark)')
376
+ : null;
377
+
378
+ function onSystemThemeChange() {
379
+ // Sincroniza o lado JS do tema com a preferência nova do SO: a raiz (que
380
+ // no modo sistema segue sem data-theme, para o @media decidir) e o .value
381
+ // dos dois seletores. O repaint das cores é do próprio @media, que o
382
+ // navegador reavalia sozinho — por isso não há nada a pintar aqui.
383
+ applyTheme(state.theme);
384
+ syncThemeSelects();
385
+ }
386
+
387
+ // Anexa no modo 'system' e desanexa nos modos forçados: com tema forçado a
388
+ // preferência do SO não vale, e continuar ouvindo seria ruído.
389
+ function watchSystemTheme(enabled) {
390
+ if (!systemThemeQuery) return;
391
+ if (enabled) systemThemeQuery.addEventListener('change', onSystemThemeChange);
392
+ else systemThemeQuery.removeEventListener('change', onSystemThemeChange);
393
+ }
394
+
395
+ function setTheme(theme) {
396
+ state.theme = theme === 'light' || theme === 'dark' ? theme : 'system';
397
+ applyTheme(state.theme);
398
+ syncThemeSelects();
399
+ watchSystemTheme(state.theme === 'system');
400
+ }
401
+
402
+ function initTheme() {
403
+ // O <script> inline do <head> já leu a mesma chave e aplicou o tema antes
404
+ // do primeiro paint; esta é a segunda metade da duplicação deliberada
405
+ // documentada em THEME_STORAGE_KEY — traz a preferência para o state e,
406
+ // com ela, para os dois seletores.
407
+ setTheme(readStoredTheme());
408
+ }
409
+
310
410
  // ---- Polling: intervalo configurável, aba oculta e backoff ----------------
311
411
 
312
412
  function readStoredRefresh() {
@@ -390,6 +490,10 @@
390
490
  return { mode: 'detail', session: null };
391
491
  }
392
492
 
493
+ if (sessions.length === 1) {
494
+ return { mode: 'detail', session: sessions[0] };
495
+ }
496
+
393
497
  return { mode: 'dashboard', session: null };
394
498
  }
395
499
 
@@ -401,7 +505,7 @@
401
505
  const showBack =
402
506
  mode === 'detail' && state.selectedSessionId !== null && state.sessions.length >= 1;
403
507
  els.backToDashboard.hidden = !showBack;
404
- if (mode === 'dashboard' && state.selectedStoryId !== null) {
508
+ if (mode === 'dashboard' && state.selectedDetail !== null) {
405
509
  closeDrawer();
406
510
  }
407
511
  }
@@ -423,9 +527,24 @@
423
527
  state.snapshot = null;
424
528
  state.detailSessionId = null;
425
529
  state.events = [];
530
+ state.diagnostics = [];
426
531
  state.eventsUrl = null;
427
532
  }
428
533
 
534
+ function serverInstanceChanged(response) {
535
+ const instanceId = response.headers.get('X-Issue-Flow-Instance');
536
+ if (!instanceId) return false; // servidor anterior à identidade de instância
537
+ if (state.serverInstanceId === null) {
538
+ state.serverInstanceId = instanceId;
539
+ return false;
540
+ }
541
+ if (state.serverInstanceId !== instanceId) {
542
+ window.location.reload();
543
+ return true;
544
+ }
545
+ return false;
546
+ }
547
+
429
548
  function requestPoll() {
430
549
  clearTimer();
431
550
  if (state.polling) {
@@ -458,6 +577,7 @@
458
577
  state.pollAgain = false;
459
578
  try {
460
579
  const sessionsRes = await fetch('api/sessions', { cache: 'no-store' });
580
+ if (serverInstanceChanged(sessionsRes)) return;
461
581
  if (!sessionsRes.ok) throw new Error('HTTP ' + sessionsRes.status);
462
582
  const sessions = await sessionsRes.json();
463
583
  if (!Array.isArray(sessions)) throw new Error('sessions payload invalid');
@@ -528,7 +648,15 @@
528
648
  if (!eventsRes.ok) throw new Error('HTTP ' + eventsRes.status);
529
649
  const entries = await eventsRes.json();
530
650
  state.events = Array.isArray(entries) ? entries : [];
651
+ const diagnosticUrl =
652
+ 'api/diagnostics?session=' + encodeURIComponent(state.detailSessionId || '');
653
+ const diagnosticsRes = await fetch(diagnosticUrl, { cache: 'no-store' });
654
+ if (diagnosticsRes.ok) {
655
+ const diagnostics = await diagnosticsRes.json();
656
+ state.diagnostics = Array.isArray(diagnostics) ? diagnostics : [];
657
+ }
531
658
  renderHistory();
659
+ renderDrawer(state.snapshot);
532
660
  }
533
661
  setViewMode('detail');
534
662
  state.failures = 0;
@@ -689,6 +817,7 @@
689
817
  renderProgress(snapshot);
690
818
  renderNow(snapshot);
691
819
  renderResilience(snapshot);
820
+ renderConfiguration(snapshot);
692
821
  renderPhases(snapshot);
693
822
  renderNextSteps(snapshot);
694
823
  renderStories(snapshot);
@@ -724,7 +853,15 @@
724
853
 
725
854
  const branch = snapshot.git.branch;
726
855
  const base = snapshot.git.baseBranch;
727
- els.branchLine.textContent = branch ? (base ? branch + ' ← ' + base : branch) : '';
856
+ const branchMode =
857
+ snapshot.git.branchCreated === false
858
+ ? 'branch atual · não criada pelo Issue Flow'
859
+ : snapshot.git.branchCreated === true
860
+ ? 'criada pelo Issue Flow'
861
+ : 'origem da branch não informada';
862
+ els.branchLine.textContent = branch
863
+ ? (base ? branch + ' ← ' + base : branch) + ' · ' + branchMode
864
+ : '';
728
865
 
729
866
  els.statusBadge.textContent = STATUS_LABELS[snapshot.status] || snapshot.status;
730
867
  els.statusBadge.className = 'badge status-' + snapshot.status;
@@ -899,6 +1036,105 @@
899
1036
  els.resilience.appendChild(grid);
900
1037
  }
901
1038
 
1039
+ function configSourceLabel(source) {
1040
+ const labels = {
1041
+ default: 'default do Issue Flow',
1042
+ global: 'configuração global',
1043
+ project: 'configuração do projeto',
1044
+ env: 'variável de ambiente',
1045
+ cli: 'override da execução',
1046
+ fallback: 'fallback',
1047
+ };
1048
+ return labels[source] || source || 'não informado';
1049
+ }
1050
+
1051
+ function renderConfiguration(snapshot) {
1052
+ clear(els.configuration);
1053
+ const config = snapshot.configuration;
1054
+ if (!config) {
1055
+ els.configuration.appendChild(el('p', 'empty', 'Configuração não capturada nesta execução.'));
1056
+ return;
1057
+ }
1058
+
1059
+ const summary = el('dl', 'now-grid');
1060
+ nowRow(
1061
+ summary,
1062
+ 'Harness padrão',
1063
+ (config.defaultProvider.value || '—') + ' · ' + configSourceLabel(config.defaultProvider.source),
1064
+ );
1065
+ nowRow(
1066
+ summary,
1067
+ 'Modelo padrão',
1068
+ (config.defaultModel.value || 'default do provider') +
1069
+ ' · ' +
1070
+ configSourceLabel(config.defaultModel.source),
1071
+ );
1072
+ nowRow(summary, 'Fallbacks', list(config.fallbacks).join(' → ') || 'nenhum configurado');
1073
+ nowRow(summary, 'Precedência', list(config.precedence).join(' → '));
1074
+ els.configuration.appendChild(summary);
1075
+
1076
+ const phases = el('div', 'config-phase-grid');
1077
+ for (const phase of list(config.phases)) {
1078
+ const row = el('button', 'config-phase-row');
1079
+ row.type = 'button';
1080
+ row.appendChild(el('span', 'mono', phase.phase));
1081
+ row.appendChild(el('span', null, phase.provider.value || '—'));
1082
+ row.appendChild(el('span', null, phase.model.value || 'default do provider'));
1083
+ row.appendChild(
1084
+ el(
1085
+ 'span',
1086
+ 'muted',
1087
+ configSourceLabel(phase.provider.source) + ' / ' + configSourceLabel(phase.model.source),
1088
+ ),
1089
+ );
1090
+ row.addEventListener('click', () => openDrawer('phase', phase.phase));
1091
+ phases.appendChild(row);
1092
+ }
1093
+ els.configuration.appendChild(phases);
1094
+
1095
+ const form = el('form', 'config-form');
1096
+ const provider = el('select');
1097
+ provider.setAttribute('aria-label', 'Harness padrão para execuções futuras');
1098
+ for (const id of ['claude', 'codex', 'cursor', 'antigravity']) {
1099
+ const option = el('option', null, id);
1100
+ option.value = id;
1101
+ provider.appendChild(option);
1102
+ }
1103
+ provider.value = config.defaultProvider.value || 'claude';
1104
+ const model = el('input');
1105
+ model.type = 'text';
1106
+ model.placeholder = 'modelo (vazio = default)';
1107
+ model.value = config.defaultModel.value || '';
1108
+ const save = el('button', 'config-save', 'Salvar preferência global');
1109
+ save.type = 'submit';
1110
+ const feedback = el('span', 'muted');
1111
+ form.appendChild(provider);
1112
+ form.appendChild(model);
1113
+ form.appendChild(save);
1114
+ form.appendChild(feedback);
1115
+ form.addEventListener('submit', async (event) => {
1116
+ event.preventDefault();
1117
+ save.disabled = true;
1118
+ feedback.textContent = 'salvando…';
1119
+ try {
1120
+ const response = await fetch('api/config/agent', {
1121
+ method: 'POST',
1122
+ headers: { 'Content-Type': 'application/json' },
1123
+ body: JSON.stringify({ provider: provider.value, model: model.value }),
1124
+ });
1125
+ const result = await response.json();
1126
+ if (!response.ok) throw new Error(result.error || 'falha ao salvar');
1127
+ feedback.textContent = 'salvo para execuções futuras';
1128
+ } catch (error) {
1129
+ feedback.textContent = error.message || 'falha ao salvar';
1130
+ } finally {
1131
+ save.disabled = false;
1132
+ }
1133
+ });
1134
+ form.hidden = !state.configWritable;
1135
+ els.configuration.appendChild(form);
1136
+ }
1137
+
902
1138
  function renderPhases(snapshot) {
903
1139
  clear(els.phases);
904
1140
  if (snapshot.phases.length === 0) {
@@ -907,6 +1143,7 @@
907
1143
  }
908
1144
  for (const phase of snapshot.phases) {
909
1145
  const item = el('li');
1146
+ item.className = 'detail-row';
910
1147
  item.appendChild(el('span', 'item-icon icon-' + phase.status, PHASE_ICONS[phase.status] || '○'));
911
1148
  const main = el('div', 'item-main');
912
1149
  main.appendChild(el('div', 'item-title', phase.name));
@@ -918,6 +1155,12 @@
918
1155
  formatUsage(phase),
919
1156
  ]);
920
1157
  if (side) item.appendChild(el('span', 'item-side', side));
1158
+ item.tabIndex = 0;
1159
+ item.setAttribute('role', 'button');
1160
+ item.addEventListener('click', () => openDrawer('phase', phase.name));
1161
+ item.addEventListener('keydown', (event) => {
1162
+ if (event.key === 'Enter' || event.key === ' ') openDrawer('phase', phase.name);
1163
+ });
921
1164
  els.phases.appendChild(item);
922
1165
  }
923
1166
  }
@@ -948,6 +1191,7 @@
948
1191
  // antes da issue 38 pode chegar sem ele.
949
1192
  const storyStage = story.stage !== null && story.stage !== undefined ? story.stage : 'pending';
950
1193
  const item = el('li', storyStage === 'executing' ? 'story-executing' : null);
1194
+ item.classList.add('detail-row');
951
1195
  const status = story.passes ? 'completed' : 'pending';
952
1196
  item.appendChild(el('span', 'item-icon icon-' + status, story.passes ? '✓' : '○'));
953
1197
  const main = el('div', 'item-main');
@@ -982,6 +1226,12 @@
982
1226
  formatUsage(story),
983
1227
  ]);
984
1228
  if (side) item.appendChild(el('span', 'item-side', side));
1229
+ item.tabIndex = 0;
1230
+ item.setAttribute('role', 'button');
1231
+ item.addEventListener('click', () => openDrawer('story', story.id));
1232
+ item.addEventListener('keydown', (event) => {
1233
+ if (event.key === 'Enter' || event.key === ' ') openDrawer('story', story.id);
1234
+ });
985
1235
  els.stories.appendChild(item);
986
1236
  }
987
1237
  }
@@ -1008,7 +1258,7 @@
1008
1258
  card.appendChild(
1009
1259
  el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
1010
1260
  );
1011
- card.addEventListener('click', () => openDrawer(story.id));
1261
+ card.addEventListener('click', () => openDrawer('story', story.id));
1012
1262
  return card;
1013
1263
  }
1014
1264
 
@@ -1039,18 +1289,145 @@
1039
1289
  if (event.key === 'Escape') closeDrawer();
1040
1290
  }
1041
1291
 
1042
- // Reidrata a partir do id a cada render: o drawer aberto sobrevive ao poll.
1292
+ function executionsFor(snapshot, kind, id) {
1293
+ return list(snapshot.executions).filter((execution) => {
1294
+ if (kind === 'phase') return execution.purpose === id;
1295
+ return list(execution.storyIds).includes(id);
1296
+ });
1297
+ }
1298
+
1299
+ function executionCost(execution) {
1300
+ const cost = execution.cost || {};
1301
+ if (cost.status === 'reported' || cost.status === 'estimated') {
1302
+ return formatCost(cost.amount, cost.status === 'estimated');
1303
+ }
1304
+ return cost.reason ? 'não informado (' + cost.reason + ')' : 'não informado';
1305
+ }
1306
+
1307
+ function renderExecutionHistory(snapshot, executions) {
1308
+ drawerSection('Tentativas, revisões e correções', (body) => {
1309
+ if (executions.length === 0) {
1310
+ body.appendChild(el('p', 'empty', 'Nenhuma invocação associada.'));
1311
+ return;
1312
+ }
1313
+ const timeline = el('ol', 'execution-timeline');
1314
+ for (const execution of executions.slice().reverse()) {
1315
+ const item = el('li', 'execution-entry');
1316
+ const title = el('div', 'execution-entry-head');
1317
+ title.appendChild(
1318
+ el(
1319
+ 'span',
1320
+ 'badge status-' + (execution.status === 'completed' ? 'completed' : execution.status),
1321
+ execution.status,
1322
+ ),
1323
+ );
1324
+ title.appendChild(
1325
+ el(
1326
+ 'strong',
1327
+ null,
1328
+ execution.purpose + ' · tentativa ' + execution.attempt + ' · ' + execution.trigger,
1329
+ ),
1330
+ );
1331
+ item.appendChild(title);
1332
+ const grid = el('dl', 'now-grid execution-grid');
1333
+ nowRow(grid, 'Harness', execution.agent?.harness || '—');
1334
+ nowRow(grid, 'Modelo', execution.agent?.model?.resolved || execution.agent?.model?.requested || '—');
1335
+ nowRow(grid, 'Início', formatClock(execution.startedAt));
1336
+ nowRow(grid, 'Fim', execution.finishedAt ? formatClock(execution.finishedAt) : 'em andamento');
1337
+ nowRow(grid, 'Duração', metric(execution.durationMs) !== null ? formatDuration(execution.durationMs / 1000) : '—');
1338
+ nowRow(grid, 'Tokens', formatUsage(execution.usage || {}) || '—');
1339
+ nowRow(grid, 'Custo', executionCost(execution));
1340
+ if (execution.correctionCycle) nowRow(grid, 'Correção', 'ciclo ' + execution.correctionCycle);
1341
+ if (execution.verdict?.status) nowRow(grid, 'Veredito', execution.verdict.status);
1342
+ item.appendChild(grid);
1343
+ if (execution.failure?.message) item.appendChild(el('p', 'item-error', execution.failure.message));
1344
+ timeline.appendChild(item);
1345
+ }
1346
+ body.appendChild(timeline);
1347
+ });
1348
+ }
1349
+
1350
+ function renderProcessLogs(snapshot, executions, phase) {
1351
+ const ids = new Set(executions.map((execution) => execution.id));
1352
+ const logs = list(snapshot.processLogs).filter(
1353
+ (entry) => ids.has(entry.executionId) || (ids.size === 0 && entry.phase === phase),
1354
+ );
1355
+ drawerSection('Saída do processo', (body) => {
1356
+ const details = el('details', 'process-output');
1357
+ const summary = el('summary', null, logs.length + ' linha(s) sanitizada(s)');
1358
+ details.appendChild(summary);
1359
+ if (logs.length === 0) {
1360
+ details.appendChild(el('p', 'empty', 'Nenhuma saída capturada.'));
1361
+ } else {
1362
+ const output = el('pre', 'process-output-body');
1363
+ output.textContent = logs
1364
+ .slice(-200)
1365
+ .map((entry) => formatClock(entry.at) + ' ' + entry.message)
1366
+ .join('\n');
1367
+ details.appendChild(output);
1368
+ }
1369
+ body.appendChild(details);
1370
+ });
1371
+ }
1372
+
1373
+ function renderGlobalDiagnostics(kind, id, executions) {
1374
+ const ids = new Set(executions.map((execution) => execution.id));
1375
+ const entries = state.diagnostics.filter((entry) => {
1376
+ if (ids.has(entry.executionId)) return true;
1377
+ if (kind === 'phase') return entry.phase === id;
1378
+ return typeof entry.story === 'string' && entry.story.split(',').includes(id);
1379
+ });
1380
+ if (entries.length === 0) return;
1381
+ drawerSection('Diagnóstico global persistente', (body) => {
1382
+ const details = el('details', 'process-output');
1383
+ details.appendChild(el('summary', null, entries.length + ' registro(s) em ~/.issue-flow/logs'));
1384
+ const output = el('pre', 'process-output-body');
1385
+ output.textContent = entries
1386
+ .slice(0, 200)
1387
+ .map((entry) => formatClock(entry.timestamp) + ' ' + entry.level + ' ' + entry.message)
1388
+ .join('\n');
1389
+ details.appendChild(output);
1390
+ body.appendChild(details);
1391
+ });
1392
+ }
1393
+
1394
+ // Reidrata a partir de kind/id a cada render: o drawer sobrevive ao poll.
1043
1395
  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) {
1396
+ if (state.selectedDetail === null) return;
1397
+ const kind = state.selectedDetail.kind;
1398
+ const id = state.selectedDetail.id;
1399
+ const story = kind === 'story' ? getStoryById(snapshot, id) : null;
1400
+ const phase = kind === 'phase' ? list(snapshot.phases).find((entry) => entry.name === id) : null;
1401
+ if ((kind === 'story' && story === null) || (kind === 'phase' && !phase)) {
1048
1402
  closeDrawer();
1049
1403
  return;
1050
1404
  }
1051
- els.drawerTitle.textContent = story.id + ' · ' + story.title;
1052
1405
 
1053
1406
  clear(els.drawerBody);
1407
+ if (kind === 'phase') {
1408
+ els.drawerTitle.textContent = 'Fase · ' + phase.name;
1409
+ els.drawerBody.appendChild(el('span', 'badge status-' + phase.status, phase.status));
1410
+ const grid = el('dl', 'now-grid drawer-summary-grid');
1411
+ nowRow(grid, 'Início', phase.startedAt ? formatClock(phase.startedAt) : '—');
1412
+ nowRow(grid, 'Fim', phase.endedAt ? formatClock(phase.endedAt) : '—');
1413
+ nowRow(grid, 'Duração', metric(phase.durationSeconds) !== null ? formatDuration(phase.durationSeconds) : '—');
1414
+ nowRow(grid, 'Uso total', formatUsage(phase) || '—');
1415
+ const configured = list(snapshot.configuration?.phases).find((entry) => entry.phase === phase.name);
1416
+ if (configured) {
1417
+ nowRow(grid, 'Harness efetivo', configured.provider.value || '—');
1418
+ nowRow(grid, 'Origem', configSourceLabel(configured.provider.source));
1419
+ nowRow(grid, 'Modelo efetivo', configured.model.value || 'default do provider');
1420
+ }
1421
+ els.drawerBody.appendChild(grid);
1422
+ if (phase.error) els.drawerBody.appendChild(el('p', 'item-error', phase.error));
1423
+ const executions = executionsFor(snapshot, kind, id);
1424
+ renderExecutionHistory(snapshot, executions);
1425
+ renderProcessLogs(snapshot, executions, phase.name);
1426
+ renderGlobalDiagnostics(kind, id, executions);
1427
+ return;
1428
+ }
1429
+
1430
+ els.drawerTitle.textContent = story.id + ' · ' + story.title;
1054
1431
  els.drawerBody.appendChild(
1055
1432
  el('span', 'badge story-status-' + story.status, STORY_STATUS_LABELS[story.status]),
1056
1433
  );
@@ -1100,12 +1477,21 @@
1100
1477
  for (const entry of history) {
1101
1478
  const item = el('li');
1102
1479
  if (entry.at) item.appendChild(el('span', 'log-time', formatClock(entry.at)));
1103
- item.appendChild(document.createTextNode(text(entry.message)));
1480
+ item.appendChild(
1481
+ document.createTextNode(
1482
+ (STORY_STAGE_LABELS[entry.stage] || entry.stage || '') +
1483
+ (entry.detail ? ' · ' + entry.detail : ''),
1484
+ ),
1485
+ );
1104
1486
  entries.appendChild(item);
1105
1487
  }
1106
1488
  body.appendChild(entries);
1107
1489
  });
1108
1490
  }
1491
+ const executions = executionsFor(snapshot, kind, id);
1492
+ renderExecutionHistory(snapshot, executions);
1493
+ renderProcessLogs(snapshot, executions, 'execute');
1494
+ renderGlobalDiagnostics(kind, id, executions);
1109
1495
  }
1110
1496
 
1111
1497
  function drawerSection(title, fill) {
@@ -1115,8 +1501,8 @@
1115
1501
  els.drawerBody.appendChild(section);
1116
1502
  }
1117
1503
 
1118
- function openDrawer(id) {
1119
- state.selectedStoryId = id;
1504
+ function openDrawer(kind, id) {
1505
+ state.selectedDetail = { kind, id };
1120
1506
  els.drawer.hidden = false;
1121
1507
  els.drawerOverlay.hidden = false;
1122
1508
  document.addEventListener('keydown', onDrawerKeydown);
@@ -1125,15 +1511,18 @@
1125
1511
  }
1126
1512
 
1127
1513
  function closeDrawer() {
1128
- const id = state.selectedStoryId;
1129
- state.selectedStoryId = null;
1514
+ const selected = state.selectedDetail;
1515
+ state.selectedDetail = null;
1130
1516
  els.drawer.hidden = true;
1131
1517
  els.drawerOverlay.hidden = true;
1132
1518
  document.removeEventListener('keydown', onDrawerKeydown);
1133
1519
  clear(els.drawerBody);
1134
1520
  // O card é recriado a cada render, então o foco volta pelo id, não por uma
1135
1521
  // referência guardada na abertura.
1136
- const card = id ? els.kanban.querySelector('[data-story-id="' + id + '"]') : null;
1522
+ const card =
1523
+ selected && selected.kind === 'story'
1524
+ ? els.kanban.querySelector('[data-story-id="' + selected.id + '"]')
1525
+ : null;
1137
1526
  if (card) card.focus();
1138
1527
  }
1139
1528
 
@@ -1151,6 +1540,11 @@
1151
1540
  : el('span', 'mono commit-hash', commit.hash);
1152
1541
  item.appendChild(hash);
1153
1542
  const subject = el('div', 'item-main item-title', commit.subject);
1543
+ const meta = itemSideText([
1544
+ commit.storyId || '',
1545
+ commit.committedAt ? formatClock(commit.committedAt) : '',
1546
+ ]);
1547
+ if (meta) subject.appendChild(el('span', 'muted commit-meta', meta));
1154
1548
  item.appendChild(subject);
1155
1549
  els.commits.appendChild(item);
1156
1550
  }
@@ -1290,6 +1684,11 @@
1290
1684
 
1291
1685
  // ---- Inicialização --------------------------------------------------------
1292
1686
 
1687
+ function onThemeChange(select) {
1688
+ setTheme(select.value);
1689
+ storeTheme(state.theme);
1690
+ }
1691
+
1293
1692
  function onRefreshChange(select) {
1294
1693
  state.refreshSeconds = Number(select.value);
1295
1694
  storeRefresh(state.refreshSeconds);
@@ -1315,6 +1714,14 @@
1315
1714
  );
1316
1715
  }
1317
1716
 
1717
+ els.themeSelect.addEventListener('change', () => onThemeChange(els.themeSelect));
1718
+ if (els.themeSelectDashboard) {
1719
+ els.themeSelectDashboard.addEventListener('change', () =>
1720
+ onThemeChange(els.themeSelectDashboard),
1721
+ );
1722
+ }
1723
+ initTheme();
1724
+
1318
1725
  els.logFilter.addEventListener('change', () => {
1319
1726
  state.logFilter = els.logFilter.value;
1320
1727
  if (state.snapshot) renderLogs(state.snapshot);
@@ -1332,17 +1739,19 @@
1332
1739
  // Default do seletor vem da configuração do servidor (/api/health);
1333
1740
  // a escolha do usuário em localStorage tem precedência.
1334
1741
  const stored = readStoredRefresh();
1335
- if (stored !== null) {
1336
- state.refreshSeconds = stored;
1337
- } else {
1338
- try {
1339
- const health = await fetch('api/health', { cache: 'no-store' }).then((r) => r.json());
1340
- const suggested = Number(health.refreshSeconds);
1341
- if (Number.isFinite(suggested) && suggested > 0) state.refreshSeconds = suggested;
1342
- } catch (err) {
1343
- // Sem /api/health segue o default local (5s).
1742
+ try {
1743
+ const healthRes = await fetch('api/health', { cache: 'no-store' });
1744
+ serverInstanceChanged(healthRes);
1745
+ const health = await healthRes.json();
1746
+ state.configWritable = list(health.capabilities).includes('config:agent:write');
1747
+ const suggested = Number(health.refreshSeconds);
1748
+ if (stored === null && Number.isFinite(suggested) && suggested > 0) {
1749
+ state.refreshSeconds = suggested;
1344
1750
  }
1751
+ } catch (err) {
1752
+ // Sem /api/health segue o default local (5s).
1345
1753
  }
1754
+ if (stored !== null) state.refreshSeconds = stored;
1346
1755
  buildRefreshSelect();
1347
1756
 
1348
1757
  window.setInterval(renderTimers, 1000);